Advice on FParsec

Tag: f# , fparsec Author: pspabcd345by Date: 2012-04-18

I have the following subexpression to parse 'quotes' which have the following format

"5.75 @ 5.95"

I therefore have this parsec expression to parse it

let pquote x = (sepBy (pfloat) ((spaces .>> (pchar '/' <|>  pchar '@' )>>. spaces))) x

It works fine.. except when there is a trailing space in my input, as the separator expression starts to consume content.So I wrapped it around an attempt, which works and seems, from what I understand, more or less what this was meant to be.

let pquote x = (sepBy (pfloat) (attempt (spaces .>> (pchar '/' <|>  pchar '@' )>>. spaces))) x

As I dont know fparsec so well, I wonder if there are any better way to write this. it seems a bit heavy (while still being very manageable of course)

Best Answer

let s1 = "5.75         @             5.95              "
let s2 = "5.75/5.95   "
let pquote: Parser<_> =
    pfloat
    .>> spaces .>> skipAnyOf ['@'; '/'] .>> spaces
    .>>. pfloat
    .>> spaces

Notes:

  1. I've made spaces optional everywhere spaces skips any sequence of zero or more whitespaces, so there's no need to use opt - thanks @Daniel;
  2. type Parser<'t> = Parser<'t, UserState> - I define it this way in order to avoid "value restriction" error; you may remove it;
  3. Also, don't forget the following if your program may run on a system with default language settings having decimal comma: System.Threading.Thread.CurrentThread.CurrentCulture <- Globalization.CultureInfo.GetCultureInfo "en-US" this won't work, thanks @Stephan
  4. I would not use sepBy unless I have a value list of unknown size.
  5. If you don't really need the value returned (e.g. '@' characters), it is recommended to use skip* functions instead p* for performance considerations.

UPD added slash as separator

comments:

this #2 was a pain. good trick to know. all useful comments..
your project is crazy. you are crazy. good to see some parsec used here, testimony of quality I guess.
spaces parses zero or more whitespaces--no need to use opt.
@Daniel thank you very much, good point.
@bytebuster The pfloat parser only accepts decimal points and then parses the number with the System.Globalization.CultureInfo.InvariantCulture, so I don't think you need to set the CurrentThread.CurrentCulture.

Other Answer1

I would probably do something like this, which returns float * float:

let ws = spaces
let quantity = pfloat .>> ws
let price = pfloat .>> ws
let quoteSep = pstring "@" .>> ws
let quote = quantity .>> quoteSep .>>. price //`.>> eof` (if final parser)

It's typical for each parser to consume trailing whitespace. Just make sure your top-level parser includes eof.

Other Answer2

Assuming that you could have more than two float in the input and '/' and '@' are delimiters:

let ws = spaces
let str_ws s = pstring s .>> ws
let float_ws = pfloat .>> ws
let pquote = sepBy float_ws (str_ws "/" <|> str_ws "@")

Talking about handling whitespaces, this section in FParsec tutorial is really helpful.