123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256open!ImportopenStd_internalopenDigit_string_helpersletsuffixeschar=letsprintf=Printf.sprintfin[sprintf"%c"char;sprintf"%cM"char;sprintf"%c.M"char;sprintf"%c.M."char]|>List.concat_map~f:(funsuffix->[String.lowercasesuffix;String.uppercasesuffix]);;letam_suffixes=lazy(suffixes'A')letpm_suffixes=lazy(suffixes'P')(* Avoids the allocation that [List.find] would entail in both both the closure input and
the option output. *)letrecfind_suffixstringsuffixes=matchsuffixeswith|suffix::suffixes->ifString.is_suffixstring~suffixthensuffixelsefind_suffixstringsuffixes|[]->"";;lethas_colonstringpos~until=pos<until&&Char.equal':'string.[pos](* This function defines what we meant by "decimal point", because in some string formats
it means '.' and in some it can be '.' or ','. There's no particular demand for support
for ',', and using just '.' lets us use [Float.of_string] for the decimal substring
without any substitutions. *)letchar_is_decimal_pointstringpos=Char.equal'.'string.[pos]letdecrement_length_if_ends_in_spacestringlen=iflen>0&&Char.equal' 'string.[len-1]thenlen-1elselen;;let[@cold]invalid_stringstring~reason=raise_s[%message"Time.Ofday: invalid string"stringreason];;letcheck_digits_with_underscore_and_return_if_nonzerostringpos~until=letnonzero=reffalseinforpos=postountil-1domatchstring.[pos]with|'0'|'_'->()|'1'..'9'->nonzero:=true|_->invalid_stringstring~reason:"expected digits and/or underscores after decimal point"done;!nonzero;;letcheck_digits_without_underscore_and_return_if_nonzerostringpos~until=letnonzero=reffalseinforpos=postountil-1domatchstring.[pos]with|'0'->()|'1'..'9'->nonzero:=true|_->invalid_stringstring~reason:"expected digits after decimal point"done;!nonzero;;letparsestring~f=letlen=String.lengthstringinletam_or_pm,until=(* discriminate among AM (1:30am), PM (12:30:00 P.M.), or 24-hr (13:00). *)match(find_suffixstring(Lazy.forceam_suffixes),find_suffixstring(Lazy.forcepm_suffixes))with|"",""->`hr_24,len|am,""->`hr_AM,decrement_length_if_ends_in_spacestring(len-String.lengtham)|"",pm->`hr_PM,decrement_length_if_ends_in_spacestring(len-String.lengthpm)|_,_->`hr_24,assertfalse(* Immediately above, it may seem nonsensical to write [`hr_24, assert false] when the
[`hr_24] can never be returned. We do this to help the compiler figure out never to
allocate a tuple in this code: the [let] pattern is syntactically a tuple and every
match clause is syntactically a tuple. *)inletpos=0inletpos,hr,expect_minutes_and_seconds=(* e.g. "1:00" or "1:00:00" *)ifhas_colonstring(pos+1)~untilthenpos+2,read_1_digit_intstring~pos,`Minutes_and_maybe_seconds(* e.g. "12:00" or "12:00:00" *)elseifhas_colonstring(pos+2)~untilthenpos+3,read_2_digit_intstring~pos,`Minutes_and_maybe_seconds(* e.g. "1am"; must have AM or PM (checked below) *)elseifpos+1=untilthenpos+1,read_1_digit_intstring~pos,`Neither_minutes_nor_seconds(* e.g. "12am"; must have AM or PM (checked below) *)elseifpos+2=untilthenpos+2,read_2_digit_intstring~pos,`Neither_minutes_nor_seconds(* e.g. "0930"; must not have seconds *)elsepos+2,read_2_digit_intstring~pos,`Minutes_but_not_secondsinletpos,min,expect_seconds=matchexpect_minutes_and_secondswith|`Neither_minutes_nor_seconds->(* e.g. "12am" *)pos,0,false|(`Minutes_and_maybe_seconds|`Minutes_but_not_seconds)asmaybe_seconds->(* e.g. "12:00:00" *)ifhas_colonstring(pos+2)~untilthen(pos+3,read_2_digit_intstring~pos,matchmaybe_secondswith|`Minutes_and_maybe_seconds->true|`Minutes_but_not_seconds->invalid_stringstring~reason:"expected end of string after minutes")(* e.g. "12:00" *)elseifpos+2=untilthenpos+2,read_2_digit_intstring~pos,falseelseinvalid_stringstring~reason:"expected colon or am/pm suffix with optional space after minutes"inletsec,subsec_pos,subsec_len,subsec_nonzero=matchexpect_secondswith|false->(* e.g. "12am" or "12:00" *)ifpos=untilthen0,pos,0,falseelse(* This case is actually unreachable, based on the various ways that
[expect_seconds] can end up false. *)invalid_stringstring~reason:"BUG: did not expect seconds, but found them"|true->(* e.g. "12:00:00" *)ifpos+2>untilthen(* e.g. "12:00:0" *)invalid_stringstring~reason:"expected two digits of seconds"else(letsec=read_2_digit_intstring~posinletpos=pos+2in(* e.g. "12:00:00" *)ifpos=untilthensec,pos,0,false(* e.g. "12:00:00.123" *)elseifpos<until&&char_is_decimal_pointstringposthen(sec,pos,until-pos,check_digits_with_underscore_and_return_if_nonzerostring(pos+1)~until)elseinvalid_stringstring~reason:"expected decimal point or am/pm suffix after seconds")inlethr=(* NB. We already know [hr] is non-negative, because it's the result of
[read_2_digit_int]. *)matcham_or_pmwith|`hr_AM->(* e.g. "12:00am" *)ifhr<1||hr>12theninvalid_stringstring~reason:"hours out of bounds"elseifhr=12then0elsehr|`hr_PM->(* e.g. "12:00pm" *)ifhr<1||hr>12theninvalid_stringstring~reason:"hours out of bounds"elseifhr=12then12elsehr+12|`hr_24->(matchexpect_minutes_and_secondswith|`Neither_minutes_nor_seconds->invalid_stringstring~reason:"hours without minutes or AM/PM"|`Minutes_but_not_seconds|`Minutes_and_maybe_seconds->ifhr>24theninvalid_stringstring~reason:"hours out of bounds"elseifhr=24&&(min>0||sec>0||subsec_nonzero)theninvalid_stringstring~reason:"time is past 24:00:00"(* e.g. "13:00:00" *)elsehr)inletmin=ifmin>59theninvalid_stringstring~reason:"minutes out of bounds"elsemininletsec=ifsec>60theninvalid_stringstring~reason:"seconds out of bounds"elsesecinletsubsec_len=ifsec=60||notsubsec_nonzerothen0elsesubsec_leninfstring~hr~min~sec~subsec_pos~subsec_len;;letparse_iso8601_extended?pos?lenstr~f=letpos,len=matchOrdered_collection_common.get_pos_len()?pos?len~total_length:(String.lengthstr)with|Result.Okz->z|Result.Errors->failwithf"Ofday.of_string_iso8601_extended: %s"(Error.to_string_machs)()iniflen<2thenfailwith"len < 2"else(lethr=read_2_digit_intstr~posinifhr>24thenfailwith"hour > 24";iflen=2thenfstr~hr~min:0~sec:0~subsec_pos:(pos+len)~subsec_len:0elseiflen<5thenfailwith"2 < len < 5"elseifnot(Char.equalstr.[pos+2]':')thenfailwith"first colon missing"else(letmin=read_2_digit_intstr~pos:(pos+3)inifmin>=60thenfailwith"minute > 60";ifhr=24&&min<>0thenfailwith"24 hours and non-zero minute";iflen=5thenfstr~hr~min~sec:0~subsec_pos:(pos+len)~subsec_len:0elseiflen<8thenfailwith"5 < len < 8"elseifnot(Char.equalstr.[pos+5]':')thenfailwith"second colon missing"else(letsec=read_2_digit_intstr~pos:(pos+6)in(* second can be 60 in the case of a leap second. Unfortunately, what with
non-hour-multiple timezone offsets, we can't say anything about what
the hour or minute must be in that case *)ifsec>60thenfailwithf"invalid second: %i"sec();ifhr=24&&sec<>0thenfailwith"24 hours and non-zero seconds";iflen=8thenfstr~hr~min~sec~subsec_pos:(pos+len)~subsec_len:0elseiflen=9thenfailwith"length = 9"else(matchstr.[pos+8]with|'.'|','->letsubsec_pos=pos+8inletsubsec_len=matchcheck_digits_without_underscore_and_return_if_nonzerostr(subsec_pos+1)~until:(pos+len)with|truewhensec=60->0|truewhenhr=24->failwith"24 hours and non-zero subseconds"|_->len-8infstr~hr~min~sec~subsec_pos~subsec_len|_->failwith"missing subsecond separator"))));;