|
@@ -0,0 +1,189 @@
|
|
|
|
+structure Util = struct
|
|
|
|
+
|
|
|
|
+(* <https://tools.ietf.org/html/rfc3339> *)
|
|
|
|
+
|
|
|
|
+(* e.g. "2017-10-01T18:49:11+02:00", or your own timestamp: *)
|
|
|
|
+(* date "+%FT%T`date +%z | sed -E -e 's/(...)(..)/\1:\2/'`" *)
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+fun cmpList cmp ([], []) = EQUAL
|
|
|
|
+ | cmpList cmp ([], y :: r) = LESS
|
|
|
|
+ | cmpList cmp (x :: r, []) = GREATER
|
|
|
|
+ | cmpList cmp (x :: r1, y :: r2) =
|
|
|
|
+ case cmp (x, y)
|
|
|
|
+ of EQUAL => cmpList cmp (r1, r2)
|
|
|
|
+ | unequal => unequal
|
|
|
|
+
|
|
|
|
+fun fixOffset offset =
|
|
|
|
+ let val smlnj_version = SMLNJVersion.version
|
|
|
|
+ in case cmpList Int.compare
|
|
|
|
+ (#version_id smlnj_version,
|
|
|
|
+ [110, 90])
|
|
|
|
+ of LESS => ~ offset
|
|
|
|
+ | _ => offset
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+fun dateFromRFC3339 rfc3339 =
|
|
|
|
+ if size rfc3339 < 20
|
|
|
|
+ then raise Fail (
|
|
|
|
+ "fromRFC3339: expecting at least 20 characters, got " ^
|
|
|
|
+ (Int.toString (size rfc3339)))
|
|
|
|
+ else
|
|
|
|
+ let
|
|
|
|
+ val sz = size rfc3339
|
|
|
|
+ fun get_seperator name seplist pos =
|
|
|
|
+ let val c = String.sub (rfc3339, pos)
|
|
|
|
+ in if List.exists (fn s => c = s) seplist
|
|
|
|
+ then c
|
|
|
|
+ else raise Fail (
|
|
|
|
+ "fromRFC3339: error parsing " ^ name ^
|
|
|
|
+ " at position " ^ (Int.toString pos) ^
|
|
|
|
+ ", expecting one of the characters '" ^
|
|
|
|
+ (implode seplist) ^ "', got '" ^
|
|
|
|
+ (Char.toString c) ^ "'")
|
|
|
|
+ end
|
|
|
|
+ fun get_digits name from to =
|
|
|
|
+ let val sz = to - from + 1
|
|
|
|
+ val s = substring (rfc3339, from, sz)
|
|
|
|
+ in if List.all Char.isDigit (explode s)
|
|
|
|
+ then valOf (Int.fromString s)
|
|
|
|
+ else raise Fail (
|
|
|
|
+ "fromRFC3339: Format error at position " ^
|
|
|
|
+ (Int.toString from) ^ ", expecting " ^
|
|
|
|
+ (Int.toString sz) ^ " digits, got \"" ^ s ^ "\"")
|
|
|
|
+ end
|
|
|
|
+ fun skip_fraction from =
|
|
|
|
+ if sz <= from
|
|
|
|
+ then raise Fail (
|
|
|
|
+ "fromRFC3339: expecting fraction, got end of string")
|
|
|
|
+ else
|
|
|
|
+ let val rest = String.extract (rfc3339, from, NONE)
|
|
|
|
+ val (_, sum, count) =
|
|
|
|
+ foldl (fn (c , dsc as (done, sum, count)) =>
|
|
|
|
+ if done
|
|
|
|
+ then dsc
|
|
|
|
+ else if Char.isDigit c
|
|
|
|
+ then (false,
|
|
|
|
+ 10 * sum + (ord c - ord #"0"),
|
|
|
|
+ count + 1)
|
|
|
|
+ else (true, sum, count))
|
|
|
|
+ (false, 0, 0)
|
|
|
|
+ (String.explode rest)
|
|
|
|
+ in if count > 0
|
|
|
|
+ then from + count
|
|
|
|
+ else raise Fail (
|
|
|
|
+ "fromRFC3339: Format error at position " ^
|
|
|
|
+ (Int.toString from) ^ ", expecting fraction digits, " ^
|
|
|
|
+ "got \"" ^ rest ^ "\"")
|
|
|
|
+ end
|
|
|
|
+ fun int2month i =
|
|
|
|
+ let open Date
|
|
|
|
+ val months = #[ Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec ]
|
|
|
|
+ in Vector.sub (months, i - 1)
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+ val year = get_digits "year" 0 3
|
|
|
|
+ val _ = get_seperator "month" [#"-"] 4
|
|
|
|
+ val monthint = get_digits "month" 5 6
|
|
|
|
+ val month = int2month monthint
|
|
|
|
+ val _ = get_seperator "day" [#"-"] 7
|
|
|
|
+ val day = get_digits "day" 8 9
|
|
|
|
+ val _ = get_seperator "hour" [#"T", #"t"] 10
|
|
|
|
+ val hour = get_digits "hour" 11 12
|
|
|
|
+ val _ = get_seperator "minute" [#":"] 13
|
|
|
|
+ val minute = get_digits "minute" 14 15
|
|
|
|
+ val _ = get_seperator "second" [#":"] 16
|
|
|
|
+ val second = get_digits "second" 17 18
|
|
|
|
+ val offsetpos =
|
|
|
|
+ case String.sub (rfc3339, 19)
|
|
|
|
+ of #"Z" => 19
|
|
|
|
+ | #"z" => 19
|
|
|
|
+ | #"+" => 19
|
|
|
|
+ | #"-" => 19
|
|
|
|
+ | #"." => skip_fraction 20
|
|
|
|
+ | c => raise Fail (
|
|
|
|
+ "rfc3339: mission impossible at position 19, got '" ^
|
|
|
|
+ (Char.toString c) ^ "'")
|
|
|
|
+ val offsettype =
|
|
|
|
+ if sz <= offsetpos
|
|
|
|
+ then raise Fail (
|
|
|
|
+ "fromRFC3339: expecting offsettype, got end of string")
|
|
|
|
+ else get_seperator "offsettype" [#"Z", #"z", #"+", #"-"] offsetpos
|
|
|
|
+ val offsettime =
|
|
|
|
+ if offsettype = #"Z" orelse offsettype = #"z"
|
|
|
|
+ then if sz = offsetpos + 1
|
|
|
|
+ then Time.zeroTime
|
|
|
|
+ else raise Fail (
|
|
|
|
+ "fromRFC3339: expecting end of string, got \"" ^
|
|
|
|
+ (String.extract (rfc3339, offsetpos + 1, NONE)) ^ "\"")
|
|
|
|
+ else if sz = offsetpos + 6
|
|
|
|
+ then
|
|
|
|
+ let val offsethour = get_digits "offsethour" (offsetpos + 1) (offsetpos + 2)
|
|
|
|
+ val _ = get_seperator "offsetminute" [#":"] (offsetpos + 3)
|
|
|
|
+ val offsetminute = get_digits "offsetminute" (offsetpos + 4) (offsetpos + 5)
|
|
|
|
+ val offsetseconds' = 60 * (60 * offsethour + offsetminute)
|
|
|
|
+ val offsetseconds = fixOffset (
|
|
|
|
+ if offsettype = #"+"
|
|
|
|
+ then offsetseconds'
|
|
|
|
+ else ~ offsetseconds')
|
|
|
|
+ in Time.fromSeconds (Int.toLarge offsetseconds)
|
|
|
|
+ end
|
|
|
|
+ else raise Fail (
|
|
|
|
+ "fromRFC3339: expecting hh:mm, got \"" ^
|
|
|
|
+ (String.extract (rfc3339, offsetpos + 1, NONE)) ^ "\"")
|
|
|
|
+
|
|
|
|
+ in Date.date
|
|
|
|
+ { year = year, month = month, day = day,
|
|
|
|
+ hour = hour, minute = minute, second = second,
|
|
|
|
+ offset = SOME offsettime }
|
|
|
|
+ end
|
|
|
|
+
|
|
|
|
+ val timeFromRFC3339 = Date.toTime o dateFromRFC3339
|
|
|
|
+
|
|
|
|
+end (* structure Util *)
|
|
|
|
+
|
|
|
|
+(*
|
|
|
|
+Control.Print.printDepth := 23;
|
|
|
|
+
|
|
|
|
+fromRFC3339 "2017-10-01T18:49:11+02:00";
|
|
|
|
+fromRFC3339 "2017-10-01T16:49:11Z";
|
|
|
|
+fromRFC3339 "2017-10-01T18:49:11.123-05:00";
|
|
|
|
+
|
|
|
|
+fromRFC3339 "2017-10-01T18:49:11.666+02:00";
|
|
|
|
+fromRFC3339 "2017-10-01T18:49:11.+02:00";
|
|
|
|
+fromRFC3339 "2017-10-01T16:49:11Z+02:00";
|
|
|
|
+fromRFC3339 "2017-10-01T16:49:11+0200";
|
|
|
|
+
|
|
|
|
+val d = fromRFC3339 "2017-10-01T18:49:11+02:00";
|
|
|
|
+val t = Date.toTime d;
|
|
|
|
+TIME {usec=1506876551000000} : Time.time
|
|
|
|
+% date -r 1506876551
|
|
|
|
+So. 1 Okt. 2017 18:49:11 CEST
|
|
|
|
+timestring2time "2017-10-01T18:49:11+02:00";
|
|
|
|
+TIME {usec=1506862151000000} : Time.time
|
|
|
|
+% date -r 1506862151
|
|
|
|
+So. 1 Okt. 2017 14:49:11 CEST
|
|
|
|
+
|
|
|
|
+*)
|
|
|
|
+
|
|
|
|
+(* RFC3339, 5.6:
|
|
|
|
+
|
|
|
|
+ date-fullyear = 4DIGIT
|
|
|
|
+ date-month = 2DIGIT ; 01-12
|
|
|
|
+ date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
|
|
|
|
+ ; month/year
|
|
|
|
+ time-hour = 2DIGIT ; 00-23
|
|
|
|
+ time-minute = 2DIGIT ; 00-59
|
|
|
|
+ time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
|
|
|
|
+ ; rules
|
|
|
|
+ time-secfrac = "." 1*DIGIT
|
|
|
|
+ time-numoffset = ("+" / "-") time-hour ":" time-minute
|
|
|
|
+ time-offset = "Z" / time-numoffset
|
|
|
|
+
|
|
|
|
+ partial-time = time-hour ":" time-minute ":" time-second
|
|
|
|
+ [time-secfrac]
|
|
|
|
+ full-date = date-fullyear "-" date-month "-" date-mday
|
|
|
|
+ full-time = partial-time time-offset
|
|
|
|
+
|
|
|
|
+ date-time = full-date "T" full-time
|
|
|
|
+*)
|