2016-02-22 15 views
9

Aktualnie pracuję nad projektem za pomocą F #. Jestem całkiem nowy w zakresie programowania funkcjonalnego i chociaż dobrze wiem, że elementy listy są niezmienne, nadal mam problem:Jak manipulować elementami listy w F #

Mam listę ciągów w formacie

["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"] 

Co chciałbym zrobić, to włączyć każdy element listy do własnej listy bez początkowego oddzielone przecinkami ciąg. Wyjście powinno wyglądać tak:

["1"; "2"; "3"; "4"; "5"] 
["1"; "2"] 
["1"] 

mam znaleźć mnóstwo sposobów, aby złączyć elementy listy i moje najlepsze domysły dotąd (rozkładania, albo coś w tym rodzaju) były bezowocne. Każda pomoc lub punkt we właściwym kierunku zostanie doceniona. Dzięki!

+3

Najpierw podziel problem na poszczególne elementy listy. Zacznij od wyodrębnienia listy liczb całkowitych z ciągu znaków w tym formacie (czy mogę zaproponować wyrażenia regularne). Od tego momentu jest to tylko kwestia mapowania tego przez każdy ciąg na liście (List.map). – Jwosty

Odpowiedz

4

Możesz to osiągnąć dzięki wbudowanemu API manipulacji łańcuchami znaków w .NET. Nie musisz zrobić to szczególnie fantazyjne, ale pomaga zapewnić pewne szczupły, curried adaptery nad string API:

open System 

let removeWhitespace (x : string) = x.Replace(" ", "") 

let splitOn (separator : string) (x : string) = 
    x.Split([| separator |], StringSplitOptions.RemoveEmptyEntries) 

let trim c (x : string) = x.Trim [| c |] 

Jedyną nieco kłopotliwe krokiem jest raz użyłeś splitOn podzielić na [|"(states"; "1,2,3,4,5))"|]"(states, (1,2,3,4,5))" . Teraz masz tablicę z dwoma elementami i chcesz drugi element. Możesz to zrobić, najpierw pobierając Seq.tail z tej tablicy, odrzucając pierwszy element, a następnie pobierając otrzymaną sekwencję, dając ci pierwszy element z pozostałej sekwencji.

Stosując te klocki, można wyodrębnić żądane dane tak:

let result = 
    ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"] 
    |> List.map (
     removeWhitespace 
     >> splitOn ",(" 
     >> Seq.tail 
     >> Seq.head 
     >> trim ')' 
     >> splitOn "," 
     >> Array.toList) 

Wynik:

val result : string list list = [["1"; "2"; "3"; "4"; "5"]; ["1"; "2"]; ["1"]] 

Najbardziej niebezpieczne jest to kombinacja Seq.tail >> Seq.head. Może się nie powieść, jeśli lista wejściowa zawiera mniej niż dwa elementy. Bezpieczniejsze alternatywą byłoby użyć czegoś jak poniższa funkcja trySecond pomocnika:

let trySecond xs = 
    match xs |> Seq.truncate 2 |> Seq.toList with 
    | [_; second] -> Some second 
    | _ -> None 

Używając tej funkcji, można przepisać funkcji ekstrakcji danych będzie nieco bardziej wytrzymałe:

let result' = 
    ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"] 
    |> List.map (removeWhitespace >> splitOn ",(" >> trySecond) 
    |> List.choose id 
    |> List.map (trim ')' >> splitOn "," >> Array.toList) 

Rezultatem jest takie same jak wczesniej.

+1

Nieważne, ale jeden prawdopodobnie powinien zastąpić "Seq.tail >> Seq.head' z' Seq.item 1' - jest o wiele mniej subtelny (ale [z pożytkiem więcej] oczywiście nadal jest podatny na uderzenie indeksu zasięgu)? –

+1

I jako kompletny na bok, można napisać trySecond jako 'let trySecond = funkcja | xs, gdy xs |> Seq.length> 1 -> xs |> Seq.item 1 |> Some | _ -> Brak 'lub' let trySecond = funkcja | xs gdy Seq.length xs> 1 -> Seq.item 1 xs |> Some | _ -> None' (Chociaż perf z 'Seq.length' może być gorszy w porównaniu z' Seq.toList' w zależności od tego, w jaki sposób ten drugi materializuje się w przypadku długich kolekcji) –

+1

Nie jest 'x |> List.map fn |> List.choose id' to samo co 'x |> List.choose fn'? –

1

Ten fragment powinien zrobić o was zapytać:

let values = ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"] 

let mapper (value:string) = 
    let index = value.IndexOf('(', 2) + 1; 
    value.Substring(index, value.Length - index - 2).Split(',') |> Array.toList 

values |> List.map mapper 

wyjściowa:

val it : string list list = [["1"; "2"; "3"; "4"; "5"]; ["1"; "2"]; ["1"]] 

Jak widzę to każda pozycja na ciebie oryginalna lista jest krotką z string i krotką int o zmiennej wielkości, w każdym razie to, co powyższy kod nie jest usuwanie pierwszego elementuna krotki i następnie wykorzystać pozostały o zmiennej wielkości tuple (liczby wewnątrz parens), a następnie wywołaj funkcję .Net string.Split() i zamienia wynikową tablicę na listę. Nadzieję, że to pomaga

4

Jak sugeruje @JWosty uruchomić za pomocą jednego elementu listy i dopasować go za pomocą wyrażeń regularnych.

let text = "(states, (1,2,3,4,5))" 
// Match all numbers into group "number" 
let pattern = @"^\(\w+,\s*\((?:(?<number>\d+),)*(?<number>\d+)\)$" 
let numberMatch = System.Text.RegularExpressions.Regex.Match(text, pattern) 
let values = 
    numberMatch.Groups.["number"].Captures // get all matches from the group 
    |> Seq.cast<Capture> // cast each item because regex captures are non-generic (i.e. IEnumerable instead of IEnumerable<'a>) 
    |> Seq.map (fun m -> m.Value) // get the matched (string) value for each capture 
    |> Seq.map int // parse as int 
    |> Seq.toList // listify 

Robi to na liście tekstów wejściowych jest tylko kwestia przechodząc tę ​​logikę do List.map.

Co lubię tego rozwiązania jest to, że nie używa magii liczb, ale rdzeń jest to po prostu wyrażenie regularne. Parsowanie każdego dopasowania jako liczby całkowitej jest całkiem bezpieczne, ponieważ dopasowujemy tylko cyfry.

2

Podobny do odpowiedzi Luiso, ale powinien unikać wyjątków. Zauważ, że dzielę się na '(' i ')', dzięki czemu mogę wyizolować krotkę. Następnie próbuję uzyskać krotkę tylko przed podzieleniem jej na ',', aby uzyskać końcowy wynik. Używam dopasowywania wzorców, aby uniknąć wyjątków.

open System 

let values = ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"] 


let new_list = values |> List.map(fun i -> i.Split([|'(';')'|], StringSplitOptions.RemoveEmptyEntries)) 
          |> List.map(fun i -> i|> Array.tryItem(1)) 
          |> List.map(function x -> match x with 
                | Some i -> i.Split(',') |> Array.toList 
                | None -> []) 

printfn "%A" new_list 

daje:

[["1"; "2"; "3"; "4"; "5"]; ["1"; "2"]; ["1"]] 
5

Można też po prostu użyć Char.IsDigit (przynajmniej na podstawie danych próby) tak:

open System 

// Signature is string -> string list 
let getDigits (input : string) = 
    input.ToCharArray() 
    |> Array.filter Char.IsDigit 
    |> Array.map (fun c -> c.ToString()) 
    |> List.ofArray 

// signature is string list -> string list list 
let convertToDigits input = 
    input 
    |> List.map getDigits 

i testowanie go w F # Interaktywny:

> let sampleData = ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"];; 

val sampleData : string list = 
    ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"] 

> let test = convertToDigits sampleData;; 

val test : string list list = [["1"; "2"; "3"; "4"; "5"]; ["1"; "2"]; ["1"]] 

UWAGA: Jeśli masz więcej niż 1-cyfrową liczbę, zostanie podzielona na poszczególne elementy na liście. Jeśli tego nie chcesz, musisz użyć regex lub string.split lub czegoś innego.

7

Dla zabawy, oto zarys tego, jak analizować łańcuchy przy użyciu FParsec, biblioteki kombinatorowej parsera.

Po pierwsze, trzeba importować kilka modułów:

open FParsec.Primitives 
open FParsec.CharParsers 

Następnie można zdefiniować parser, który będzie pasował wszystkie ciągi zamknięte w nawiasach:

let betweenParentheses p s = between (pstring "(") (pstring ")") p s 

To będzie pasował dowolny ciąg znaków w nawiasach, takie jak "(42)", "(foo)", "(1,2,3,4,5)" itd., w zależności od określonego parsera p przekazanego jako pierwszy argument.

W celu analizowania numery jak "(1,2,3,4,5)" lub "(1,2)", można połączyć betweenParentheses z FParsec wbudowanej sepBy i pint32:

let pnumbers s = betweenParentheses (sepBy pint32 (pstring ",")) s 

pint32 jest parser liczb całkowitych i sepBy jest parser, który odczytuje listę wartości, oddzielone ciągiem znaków - w tym przypadku ",".

W celu analizowania całego „grupa” wartości, takie jak "(states, (1,2,3,4,5))" lub "(alpha, (1,2))", można ponownie użyć betweenParentheses i pnumbers:

let pgroup s = 
    betweenParentheses 
     (manyTill anyChar (pstring ",") >>. spaces >>. pnumbers) s 

Połączenie manyTill analizuje każdą wartość char aż napotka ,. Następnie parser pgroup oczekuje dowolnej liczby spacji, a następnie formatu zdefiniowanego przez pnumbers.

Wreszcie, można zdefiniować funkcję, która uruchamia pgroup parser na sznurku:

// string -> int32 list option 
let parseGroup s = 
    match run pgroup s with 
    | Success (result, _, _) -> Some result 
    | Failure _    -> None 

Ponieważ ta funkcja zwraca opcję można wykorzystać List.choose mapować struny, które mogą być analizowane:

> ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"] 
    |> List.choose parseGroup;; 
val it : int32 list list = [[1; 2; 3; 4; 5]; [1; 2]; [1]] 

Używanie FParsec jest najprawdopodobniej przesadą, chyba że masz bardziej elastyczne reguły formatowania niż te, które można łatwo rozwiązać za pomocą standardowego interfejsu API .NET string.

Powiązane problemy