type error = Malformed of string let pp_error ppf = function | Malformed detail -> Format.fprintf ppf "malformed timestamp: %s" detail let encode timestamp = let time = Unix.gmtime (float_of_int (Recovery.timestamp_to_unix_seconds timestamp)) in Printf.sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ" (time.Unix.tm_year + 1900) (time.Unix.tm_mon + 1) time.Unix.tm_mday time.Unix.tm_hour time.Unix.tm_min time.Unix.tm_sec let digit text index = let value = Char.code text.[index] - Char.code '0' in if value >= 0 && value <= 9 then Some value else None let number text start length = let rec loop index value = if index = start + length then Some value else match digit text index with | None -> None | Some value' -> loop (index + 1) ((value * 10) + value') in loop start 0 (* Return the number of days from 1970-01-01 for a Gregorian date. *) let days_from_civil year month day = let year = year - if month <= 2 then 1 else 0 in let era = if year >= 0 then year / 400 else (year - 399) / 400 in let year_of_era = year - (era * 400) in let month = month + if month > 2 then -3 else 9 in let day_of_year = (((153 * month) + 2) / 5) + day - 1 in let day_of_era = (year_of_era * 365) + (year_of_era / 4) - (year_of_era / 100) + day_of_year in (era * 146097) + day_of_era - 719468 let decode text = let malformed detail = Error (Malformed detail) in if String.length text <> 20 then malformed "expected YYYY-MM-DDTHH:MM:SSZ" else if text.[4] <> '-' || text.[7] <> '-' || text.[10] <> 'T' || text.[13] <> ':' || text.[16] <> ':' || text.[19] <> 'Z' then malformed "expected YYYY-MM-DDTHH:MM:SSZ" else match ( number text 0 4, number text 5 2, number text 8 2, number text 11 2, number text 14 2, number text 17 2 ) with | Some year, Some month, Some day, Some hour, Some minute, Some second -> if month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59 then malformed "timestamp component out of range" else let seconds = (days_from_civil year month day * 86_400) + (hour * 3_600) + (minute * 60) + second in let timestamp = Recovery.timestamp_of_unix_seconds seconds in if String.equal (encode timestamp) text then Ok timestamp else malformed "invalid calendar date" | _ -> malformed "timestamp contains a non-digit"