Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parsing date and time with FParsec

Within a simple query language I'd like to recognize date and time literals, preferably without using delimiters. For example,

CreationDate = 2013-05-13 5:30 PM

I could use a combinator to detect the basic syntax (e.g., yyyy-MM-dd hh:mm tt), but then it needs to be passed to DateTime.TryParse for full validation.

A few questions:

  • Is there a combinator for "post processing" a parser result, e.g., pstring "1/2/2000" |> (fun s -> try OK(DateTime.Parse s) with _ -> Fail("not a date"))
  • Is it possible to apply a predicate to a string (as satisfy does to char)?
  • Is there a better approach for parsing date/time?

UPDATE

Using Guvante's and Stephan's examples, I came up with this:

let dateTimeLiteral =
  let date sep = pipe5 pint32 sep pint32 sep pint32 (fun a _ b _ c -> a, b, c)
  let time = 
    (pint32 .>>. (skipChar ':' >>. pint32)) .>>. 
      (opt (stringCIReturn " am" false <|> stringCIReturn " pm" true))
  (date (pstring "/") <|> date (pstring "-")) .>>. 
    (opt (skipChar ' ' >>. time)) .>> ws
    >>=? (fun ((a, b, c), tt) ->
      let y, m, d = if a > 12 then a, b, c else c, a, b
      let h, n =
        match tt with
        | Some((h, n), tt) ->
          match tt with
          | Some true -> (match h with 12 -> h | _ -> h + 12), n
          | Some false -> (match h with 12 -> h - 12 | _ -> h), n
          | None -> h, n
        | None -> 0, 0
      try preturn (System.DateTime(y, m, d, h, n, 0)) |>> DateTime 
      with _ -> fail "Invalid date/time format")
like image 240
Daniel Avatar asked May 13 '13 20:05

Daniel


1 Answers

You can easily build a custom combinator or parser that validates parsed input.

If you only want to use combinators ("Haskell-style"), you could use

let pDateString = pstring "1/2/2000"

let pDate1 = 
    pDateString 
    >>= fun str ->            
           try preturn (System.DateTime.Parse(str))               
           with _ -> fail "Date format error"

as Guvante just proposed.

If you want to avoid construction temporary parsers (see preturn ... and pfail ... above), you can just let the function accept a second parameter and directly return Reply values:

let pDate2 = 
    pDateString 
    >>= fun str stream ->            
           try Reply(System.DateTime.Parse(str))               
           with _ -> Reply(Error, messageError "Date format error")

If you want the error location to be at the beginning of the malformed date string, you could replace >>= with >>=?. Note that this also has consequences for error recovery.

If you want to have full control, you can write the parser only using the lower level API, starting with a basic version like the following:

let pDate3 = 
    fun stream ->
        let reply = pDateString stream
        if reply.Status = Ok then        
            try Reply(System.DateTime.Parse(reply.Result))               
            with _ -> Reply(Error, messageError "Date format error")
        else
           Reply(reply.Status, reply.Error)

This last version would also allow you to replace the pDateString parser with code that directly accesses the CharStream interface, which could give you some additional flexibility or performance.

like image 149
Stephan Tolksdorf Avatar answered Sep 30 '22 09:09

Stephan Tolksdorf