문제

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")
도움이 되었습니까?

해결책

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.

다른 팁

Is there a combinator for "post processing" a parser result

It depends on what you want to do if you fail. You can always do |>> to get your DateTime out. Failing it is equally interesting, I think your example could be (given a parser sp that gets the correct string, note it would be of type Parser<string,'u>)

sp >>= (fun s -> match DateTime.TryParse s with
                 | true,result -> preturn result
                 | false,_ -> fail)

Here I am taking in the resultant string and calling the TryParse method, and returning either a preturn or a fail depending on whether it succeeds. I couldn't find any of the methods that worked exactly like that.

Note that >>=? would cause a backtrack if it failed.

Is it possible to apply a predicate to a string (as satisfy does for char)?

You would have to call the predicate for every character (2, 20, 201) which is usually not ideal. I am pretty sure you could whip up something like this if you wanted, but I don't think it is ideal for that reason, not to mention handling partial matches becomes harder.

Is there a better approach for parsing date/time?

The biggest factor is "What do you know about the date/time?" If you know it is in that syntax exactly then you should be able to use a post process and be fine (since hopefully the error case will be rare)

If you aren't sure, for instance if PM is optional, but would be unambiguously detailed, then you will probably want to break up the definition and combine it at the end. Note that here I have relaxed the character counts a little, you could add some opt to get even more relaxed, or replace the pint32 with digit and manual conversions.

let pipe6 = //Implementation left as an exercise
let dash = skipChar '-'
let space = skipChar ' '
let colon = skipChar ':'
pipe6 (pint32 .>> dash) //Year
      (pint32 .>> dash) //Month
      (pint32 .>> space) //Day
      (pint32 .>> colon) //Hour
      (pint32 .>> space) //Minute
      (anyString) //AM/PM
      (fun year month day hour minute amPm ->
          DateTime(year, month, day,
                   hour + (if amPm.Equals("PM", StringComparison.InvariantCultureIgnoreCase)
                          then 12 else 0),
                   minute, 0)) //No seconds

Writing all that out I am not sure if you are better or worse off...

I've used next code to parse given date string into DataTime object.

2000-01-01 12:34:56,789

 let pipe7 p1 p2 p3 p4 p5 p6 p7 f =
        p1 >>= fun x1 ->
         p2 >>= fun x2 ->
          p3 >>= fun x3 ->
           p4 >>= fun x4 ->
            p5 >>= fun x5 ->
             p6 >>= fun x6 ->
              p7 >>= fun x7 -> preturn (f x1 x2 x3 x4 x5 x6 x7)

 let int_ac = pint32 .>> anyChar

 let pDateStr : Parser<DateTime, unit> = pipe7 int_ac int_ac int_ac int_ac int_ac int_ac int_ac (fun y m d h mi s mil -> new DateTime(y,m,d,h,mi,s,mil))
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top