2016-02-03 14 views
5

Próbuję przeanalizować plik, używając FParsec, który składa się z wartości float lub int. Mam dwa problemy, na które nie mogę znaleźć dobrego rozwiązania.Parsowanie int lub float z FParsec

Zarówno pint32 i pfloat powodzeniem analizować ten sam ciąg, ale dają różne odpowiedzi, np pint32 powróci 3 podczas analizowania ciąg "3.0" i pfloat powróci 3.0 podczas analizowania ten sam ciąg. Czy możliwe jest sparsowanie wartości zmiennoprzecinkowej przy użyciu pint32 i czy nie powiedzie się, jeśli ciąg znaków to "3.0"?

Innymi słowy, czy istnieje sposób, aby następujące prace kodu:

let parseFloatOrInt lines = 
    let rec loop intvalues floatvalues lines = 
     match lines with 
     | [] -> floatvalues, intvalues 
     | line::rest -> 
      match run floatWs line with 
      | Success (r, _, _) -> loop intvalues (r::floatvalues) rest 
      | Failure _ -> 
       match run intWs line with 
       | Success (r, _, _) -> loop (r::intvalues) floatvalues rest 
       | Failure _ -> loop intvalues floatvalues rest 

    loop [] [] lines 

Ten fragment kodu będzie prawidłowo umieścić wszystkie wartości zmiennoprzecinkowych na liście floatvalues, ale ponieważ pfloat powraca "3.0" podczas analizowania string "3", wszystkie wartości całkowite zostaną również umieszczone na liście floatvalues.

Powyższy przykład kodu wydaje się nieco niezdarny do mnie, więc zgaduję, że musi być lepszy sposób to zrobić. Zastanawiałem się nad łączeniem ich za pomocą choice, jednak oba parsery muszą zwracać ten sam typ, aby działał. Sądzę, że mógłbym stworzyć dyskryminowany związek z jedną opcją float i jedną dla int i konwertować dane wyjściowe z pint32 i pfloat przy użyciu operatora |>>. Zastanawiam się jednak, czy istnieje lepsze rozwiązanie?

Odpowiedz

3

Jesteś na właściwej drodze myślenia na temat definiowania danych domeny i oddzielenie definicję z analizatorów i ich wykorzystanie na danych źródłowych. Wydaje się to być dobrym podejściem, ponieważ w miarę jak twój prawdziwy projekt będzie się rozwijał, prawdopodobnie będziesz potrzebował więcej typów danych.

Oto jak byłoby to napisać:

/// The resulting type, or DSL 
type MyData = 
    | IntValue of int 
    | FloatValue of float 
    | Error // special case for all parse failures 

// Then, let's define individual parsers: 
let pMyInt = 
    pint32 
    |>> IntValue 

// this is an alternative version of float parser. 
// it ensures that the value has non-zero fractional part. 
// caveat: the naive approach would treat values like 42.0 as integer 
let pMyFloat = 
    pfloat 
    >>= (fun x -> if x % 1 = 0 then fail "Not a float" else preturn (FloatValue x)) 
let pError = 
    // this parser must consume some input, 
    // otherwise combined with `many` it would hang in a dead loop 
    skipAnyChar 
    >>. preturn Error 

// Now, the combined parser: 
let pCombined = 
    [ pMyFloat; pMyInt; pError ] // note, future parsers will be added here; 
            // mind the order as float supersedes the int, 
            // and Error must be the last 
    |> List.map (fun p -> p .>> ws) // I'm too lazy to add whitespase skipping 
            // into each individual parser 
    |> List.map attempt    // each parser is optional 
    |> choice      // on each iteration, one of the parsers must succeed 
    |> many       // a loop 

Uwaga, powyższy kod jest zdolny do pracy z jakichkolwiek źródeł: smyczki, strumieni, czy cokolwiek innego. Twoja prawdziwa aplikacja może wymagać pracy z plikami, ale testowanie jednostek można uprościć, używając tylko string list.

// Now, applying the parser somewhere in the code: 
let maybeParseResult = 
    match run pCombined myStringData with 
    | Success(result, _, _) -> Some result 
    | Failure(_, _, _)  -> None // or anything that indicates general parse failure 

UPD. Edytowałem kod zgodnie z komentarzami. pMyFloat został zaktualizowany w celu zapewnienia, że ​​analizowana wartość ma niezerową część ułamkową.

+0

Tak przy okazji, @bytebuster, mam problemy z uruchomieniem pCombined na łańcuchu testowym, daje to komunikat o błędzie '' Kombinator 'many' został zastosowany do parsera, który się powiódł bez zużywania danych wejściowych i bez zmiany parsera stan w jakikolwiek inny sposób. ". Czy eof musi być obsługiwany? – Chepe

+0

Świetnie, dzięki @bytebuster! Jeszcze jedno, próbowałem uruchomić parser na łańcuchu '" 2 "' po tym, jak usunąłem kombinator 'many', i wtedy dostałem ten sam problem, co opisałem powyżej, a mianowicie, że został on zwrócony jako float, ponieważ float parser przychodzi przed parserem int. Jakieś pomysły, jak temu zaradzić? – Chepe

+0

@ Chep, sprawdź zaktualizowaną wersję. Mamy nadzieję, że odnosi się on do obu problemów. – bytebuster

Powiązane problemy