123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666open!ImportopenStd_internalopenDigit_string_helpersletis_leap_year~year=(yearmod4=0&¬(yearmod100=0))||yearmod400=0(* Create a local private date type to ensure that all dates are created via
Date.create_exn.
*)moduleStable=structmoduleV1=structmoduleWithout_comparable=structmoduleT:sigtypet[@@immediate][@@derivingbin_io,hash,typerep]valcreate_exn:y:int->m:Month.Stable.V1.t->d:int->tvalyear:t->intvalmonth:t->Month.Stable.V1.tvalday:t->intvaldays_in_month:year:int->month:Month.t->intvalto_int:t->intvalof_int_exn:int->tvalinvalid_value__for_internal_use_only:tend=struct(* We used to store dates like this:
type t = { y: int; m: Month.Stable.V1.t; d: int; }
In the below we make sure that the bin_io representation is
identical (and the stable unit tests check this)
In memory we use the following much more compact representation:
2 bytes year
1 byte month
1 byte day
all packed into a single immediate int (so from 4 words down to 1).
*)typet=int[@@derivinghash,typerep,bin_shape~basetype:"899ee3e0-490a-11e6-a10a-a3734f733566"]letcreate0~year~month~day=(* create_exn's validation make sure that each value fits *)(yearlsl16)lor(Month.to_intmonthlsl8)lorday;;letyeart=tlsr16letmontht=Month.of_int_exn((tlsr8)land0xff)letdayt=tland0xffletdays_in_month~year~month=match(month:Month.t)with|Jan|Mar|May|Jul|Aug|Oct|Dec->31|Apr|Jun|Sep|Nov->30|Feb->ifis_leap_year~yearthen29else28;;letcreate_exn~y:year~m:month~d:day=(* year, month, and day need to be passed as parameters to avoid allocating
a closure (see unit test below) *)letinvalid~year~month~daymsg=invalid_argf!"Date.create_exn ~y:%d ~m:%{Month} ~d:%d error: %s"yearmonthdaymsg()inifyear<0||year>9999theninvalid~year~month~day"year outside of [0..9999]";ifday<=0theninvalid~year~month~day"day <= 0";letdays_in_month=days_in_month~year~monthinifday>days_in_monththeninvalid~year~month~day(sprintf"%d day month violation"days_in_month);create0~year~month~day;;(* We don't use Make_binable here, because that would go via an immediate
tuple or record. That is exactly the 32 bytes we worked so hard above to
get rid of. We also don't want to just bin_io the integer directly
because that would mean a new bin_io format. *)letbin_read_tbuf~pos_ref=letyear=Int.bin_read_tbuf~pos_refinletmonth=Month.Stable.V1.bin_read_tbuf~pos_refinletday=Int.bin_read_tbuf~pos_refincreate0~year~month~day;;let__bin_read_t___buf~pos_ref=(* __bin_read_t is only needed for variants *)Bin_prot.Common.raise_variant_wrong_type"Date.t"!pos_ref;;letbin_reader_t={Bin_prot.Type_class.read=bin_read_t;vtag_read=__bin_read_t__};;letbin_size_tt=Int.bin_size_t(yeart)+Month.bin_size_t(montht)+Int.bin_size_t(dayt);;letbin_write_tbuf~post=letpos=Int.bin_write_tbuf~pos(yeart)inletpos=Month.bin_write_tbuf~pos(montht)inInt.bin_write_tbuf~pos(dayt);;letbin_writer_t={Bin_prot.Type_class.size=bin_size_t;write=bin_write_t}letbin_t={Bin_prot.Type_class.reader=bin_reader_t;writer=bin_writer_t;shape=bin_shape_t};;letto_intt=tletof_int_exnn=create_exn~y:(yearn)~m:(monthn)~d:(dayn)letinvalid_value__for_internal_use_only=0let%test"invalid value"=Exn.does_raise(fun()->(of_int_exninvalid_value__for_internal_use_only:t));;endincludeT(** YYYY-MM-DD *)letto_string_iso8601_extendedt=letbuf=Bytes.create10inwrite_4_digit_intbuf~pos:0(yeart);Bytes.setbuf4'-';write_2_digit_intbuf~pos:5(Month.to_int(montht));Bytes.setbuf7'-';write_2_digit_intbuf~pos:8(dayt);Bytes.unsafe_to_string~no_mutation_while_string_reachable:buf;;letto_string=to_string_iso8601_extended(** YYYYMMDD *)letto_string_iso8601_basict=letbuf=Bytes.create8inwrite_4_digit_intbuf~pos:0(yeart);write_2_digit_intbuf~pos:4(Month.to_int(montht));write_2_digit_intbuf~pos:6(dayt);Bytes.unsafe_to_string~no_mutation_while_string_reachable:buf;;(** MM/DD/YYYY *)letto_string_americant=letbuf=Bytes.create10inwrite_2_digit_intbuf~pos:0(Month.to_int(montht));Bytes.setbuf2'/';write_2_digit_intbuf~pos:3(dayt);Bytes.setbuf5'/';write_4_digit_intbuf~pos:6(yeart);Bytes.unsafe_to_string~no_mutation_while_string_reachable:buf;;letparse_year4strpos=read_4_digit_intstr~posletparse_monthstrpos=Month.of_int_exn(read_2_digit_intstr~pos)letparse_daystrpos=read_2_digit_intstr~pos(** YYYYMMDD *)letof_string_iso8601_basicstr~pos=ifpos+8>String.lengthstrtheninvalid_arg"Date.of_string_iso8601_basic: pos + 8 > string length";create_exn~y:(parse_year4strpos)~m:(parse_monthstr(pos+4))~d:(parse_daystr(pos+6));;(* WARNING: if you are going to change this function in a material way, be sure you
understand the implications of working in Stable *)letof_strings=letinvalid()=failwith("invalid date: "^s)inletensureb=ifnotbtheninvalid()inletmonth_num~year~month~day=create_exn~y:(parse_year4syear)~m:(parse_monthsmonth)~d:(parse_daysday)inletmonth_abrv~year~month~day=create_exn~y:(parse_year4syear)~m:(Month.of_string(String.subs~pos:month~len:3))~d:(parse_daysday)inifString.containss'/'then(lety,m,d=matchString.splits~on:'/'with|[a;b;c]->ifString.lengtha=4thena,b,c(* y/m/d *)elsec,a,b(* m/d/y *)|_->invalid()inletyear=Int.of_stringyinletyear=ifyear>=100thenyearelseifyear<75then2000+yearelse1900+yearinletmonth=Month.of_int_exn(Int.of_stringm)inletday=Int.of_stringdincreate_exn~y:year~m:month~d:day)elseifString.containss'-'then((* yyyy-mm-dd *)ensure(String.lengths=10&&Char.(=)s.[4]'-'&&Char.(=)s.[7]'-');month_num~year:0~month:5~day:8)elseifString.containss' 'thenifString.lengths=11&&Char.(=)s.[2]' '&&Char.(=)s.[6]' 'then(* DD MMM YYYY *)month_abrv~day:0~month:3~year:7else((* YYYY MMM DD *)ensure(String.lengths=11&&Char.(=)s.[4]' '&&Char.(=)s.[8]' ');month_abrv~day:9~month:5~year:0)elseifString.lengths=9then(* DDMMMYYYY *)month_abrv~day:0~month:2~year:5elseifString.lengths=8then(* assume YYYYMMDD *)month_num~year:0~month:4~day:6elseinvalid();;letof_strings=tryof_stringswith|exn->invalid_argf"Date.of_string (%s): %s"s(Exn.to_stringexn)();;moduleSexpable=structmoduleOld_date=structtypet={y:int;m:int;d:int}[@@derivingsexp]letto_datet=T.create_exn~y:t.y~m:(Month.of_int_exnt.m)~d:t.dendlett_of_sexp=function|Sexp.Atoms->of_strings|Sexp.List_assexp->Old_date.to_date(Old_date.t_of_sexpsexp);;lett_of_sexps=tryt_of_sexpswith|Of_sexp_error_asexn->raiseexn|Invalid_argumenta->of_sexp_erroras;;letsexp_of_tt=Sexp.Atom(to_stringt)endincludeSexpableletcomparet1t2=letn=Int.compare(yeart1)(yeart2)inifn<>0thennelse(letn=Month.compare(montht1)(montht2)inifn<>0thennelseInt.compare(dayt1)(dayt2));;include(valComparator.Stable.V1.make~compare~sexp_of_t)endincludeWithout_comparableincludeComparable.Stable.V1.Make(Without_comparable)endmoduleOption=structmoduleV1=structtypet=int[@@derivingbin_io,bin_shape~basetype:"826a3e79-3321-451a-9707-ed6c03b84e2f",compare,hash,typerep]letnone=V1.(to_intinvalid_value__for_internal_use_only)letis_nonet=t=noneletis_somet=not(is_nonet)letsome_is_representable_=trueletsomet=V1.to_inttletunchecked_value=V1.of_int_exnletto_optiont=ifis_sometthenSome(unchecked_valuet)elseNoneletof_optionopt=matchoptwith|None->none|Somev->somev;;letvalue_exnt=ifis_sometthenunchecked_valuetelseraise_s[%message[%here]"Date.Option.value_exn none"];;letvaluet~default=ifis_sometthenunchecked_valuetelsedefaultletsexp_of_tt=to_optiont|>Option.sexp_of_tV1.sexp_of_tlett_of_sexpsexp=(Option.t_of_sexpV1.t_of_sexp)sexp|>of_optionendendendmoduleWithout_comparable=Stable.V1.Without_comparableincludeWithout_comparablemoduleC=Comparable.Make_binable_using_comparator(Without_comparable)includeCmoduleO=structinclude(C:Comparable.Infixwithtypet:=t)endinclude(Hashable.Make_binable(structincludeTincludeSexpableincludeBinableletcompare(a:t)(b:t)=compareabend):Hashable.S_binablewithtypet:=t)includePretty_printer.Register(structtypenonrect=tletmodule_name="Core_kernel.Date"letto_string=to_stringend)letunix_epoch=create_exn~y:1970~m:Jan~d:1(* The Days module is used for calculations that involve adding or removing a known number
of days from a date. Internally the date is translated to a day number, the days are
added, and the new date is returned. Those interested in the math can read:
http://alcor.concordia.ca/~gpkatch/gdate-method.html
note: unit tests are in lib_test/time_test.ml
*)moduleDays:sigtypedate=ttypet[@@immediate]valof_date:date->tvalto_date:t->datevaldiff:t->t->intvaladd_days:t->int->tvalunix_epoch:tendwithtypedate:=t=structopenInttypet=intletof_yeary=(365*y)+(y/4)-(y/100)+(y/400)letof_datedate=letm=(Month.to_int(monthdate)+9)%12inlety=yeardate-(m/10)inof_yeary+(((m*306)+5)/10)+(daydate-1);;letc_10_000=Int63.of_int10_000letc_14_780=Int63.of_int14_780letc_3_652_425=Int63.of_int3_652_425letto_datedays=lety=letopenInt63into_int_exn(((c_10_000*of_intdays)+c_14_780)/c_3_652_425)inletddd=days-of_yearyinlety,ddd=ifddd<0then(lety=y-1iny,days-of_yeary)elsey,dddinletmi=((100*ddd)+52)/3_060inlety=y+((mi+2)/12)inletm=((mi+2)%12)+1inletd=ddd-(((mi*306)+5)/10)+1increate_exn~y~m:(Month.of_int_exnm)~d;;letunix_epoch=of_dateunix_epochletadd_daystdays=t+daysletdifft1t2=t1-t2endletadd_daystdays=Days.to_date(Days.add_days(Days.of_datet)days)letdifft1t2=Days.diff(Days.of_datet1)(Days.of_datet2)letadd_monthstn=lettotal_months=Month.to_int(montht)+ninlety=yeart+(total_months/%12)inletm=total_months%12in(* correct for december *)lety,m=ifInt.(=)m0theny-1,m+12elsey,minletm=Month.of_int_exnmin(* handle invalid dates for months with fewer number of days *)letrectry_created=trycreate_exn~y~m~dwith|_exn->assert(Int.(>=)d1);try_create(d-1)intry_create(dayt);;letadd_yearstn=add_monthst(n*12)(* http://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Purely_mathematical_methods
note: unit tests in lib_test/time_test.ml
*)letday_of_week=lettable=[|0;3;2;5;0;3;5;1;4;6;2;4|]infunt->letm=Month.to_int(montht)inlety=ifInt.(<)m3thenyeart-1elseyeartinDay_of_week.of_int_exn((y+(y/4)-(y/100)+(y/400)+table.(m-1)+dayt)%7);;(* http://en.wikipedia.org/wiki/Ordinal_date *)letnon_leap_year_table=[|0;31;59;90;120;151;181;212;243;273;304;334|]letleap_year_table=[|0;31;60;91;121;152;182;213;244;274;305;335|]letordinal_datet=lettable=ifis_leap_year~year:(yeart)thenleap_year_tableelsenon_leap_year_tableinletoffset=table.(Month.to_int(montht)-1)indayt+offset;;letlast_week_of_yeary=letfirst_of_year=create_exn~y~m:Jan~d:1inletistday=Day_of_week.equal(day_of_weekt)dayinifisfirst_of_yearThu||(is_leap_year~year:y&&isfirst_of_yearWed)then53else52;;(* See http://en.wikipedia.org/wiki/ISO_week_date or ISO 8601 for the details of this
algorithm.
Uses a [~f] argument to avoid allocating a tuple when called by [week_number].
*)letcall_with_week_and_yeart~f=letordinal=ordinal_datetinletweekday=Day_of_week.iso_8601_weekday_number(day_of_weekt)in(* [ordinal - weekday + 4] is the ordinal of this week's Thursday, then (n + 6) / 7 is
division by 7 rounding up *)letweek=(ordinal-weekday+10)/7inletyear=yeartinifInt.(<)week1thenf~week:(last_week_of_year(year-1))~year:(year-1)elseifInt.(>)week(last_week_of_yearyear)thenf~week:1~year:(year+1)elsef~week~year;;letweek_number_and_yeart=call_with_week_and_yeart~f:(fun~week~year->week,year)letweek_numbert=call_with_week_and_yeart~f:(fun~week~year:_->week)letis_weekendt=Day_of_week.is_sun_or_sat(day_of_weekt)letis_weekdayt=not(is_weekendt)letis_business_dayt~is_holiday=is_weekdayt&¬(is_holidayt)letrecdiff_weekend_dayst1t2=ift1<t2then-diff_weekend_dayst2t1else((* Basic date diff *)letdiff=difft1t2in(* Compute the number of Saturday -> Sunday crossings *)letd1=day_of_weekt1inletd2=day_of_weekt2inletnum_satsun_crossings=ifInt.(<)(Day_of_week.to_intd1)(Day_of_week.to_intd2)then1+(diff/7)elsediff/7in(num_satsun_crossings*2)+(ifDay_of_week.(=)d2Day_of_week.Sunthen1else0)+ifDay_of_week.(=)d1Day_of_week.Sunthen-1else0);;letdiff_weekdayst1t2=difft1t2-diff_weekend_dayst1t2letadd_days_skippingt~skipn=letstep=ifInt.(>=)n0then1else-1inletreclooptk=lett_next=add_dayststepinifskiptthenloopt_nextkelseifInt.(=)k0thentelseloopt_next(k-1)inloopt(absn);;letrecfirst_day_satisfyingt~step~condition=ifconditiontthentelsefirst_day_satisfying(add_dayststep)~step~condition;;letnext_day_satisfyingt~step~condition=letnext_day=add_dayststepinfirst_day_satisfyingnext_day~step~condition;;letfollowing_weekdayt=next_day_satisfyingt~step:1~condition:is_weekdayletprevious_weekdayt=next_day_satisfyingt~step:(-1)~condition:is_weekdayletround_forward_to_weekdayt=first_day_satisfyingt~step:1~condition:is_weekdayletround_backward_to_weekdayt=first_day_satisfyingt~step:(-1)~condition:is_weekdayletround_forward_to_business_dayt~is_holiday=first_day_satisfyingt~step:1~condition:(is_business_day~is_holiday);;letround_backward_to_business_dayt~is_holiday=first_day_satisfyingt~step:(-1)~condition:(is_business_day~is_holiday);;letadd_weekdaystn=add_days_skippingt~skip:is_weekendnletadd_weekdays_rounding_in_direction_of_step=add_weekdaysletadd_weekdays_rounding_forwardtn=add_days_skipping(round_forward_to_weekdayt)~skip:is_weekendn;;letadd_weekdays_rounding_backwardtn=add_days_skipping(round_backward_to_weekdayt)~skip:is_weekendn;;letadd_business_dayst~is_holidayn=add_days_skippingtn~skip:(fund->is_weekendd||is_holidayd);;letadd_business_days_rounding_in_direction_of_step=add_business_daysletadd_business_days_rounding_forwardt~is_holidayn=add_days_skipping(round_forward_to_business_day~is_holidayt)n~skip:(fund->not(is_business_day~is_holidayd));;letadd_business_days_rounding_backwardt~is_holidayn=add_days_skipping(round_backward_to_business_day~is_holidayt)n~skip:(fund->not(is_business_day~is_holidayd));;letdates_between~min:t1~max:t2=letreclooptl=ift<t1thenlelseloop(add_dayst(-1))(t::l)inloopt2[];;letweekdays_between~min~max=letall_dates=dates_between~min~maxinOption.value_map(List.hdall_dates)~default:[]~f:(funfirst_date->(* to avoid a system call on every date, we just get the weekday for the first
date and use it to get all the other weekdays *)letfirst_weekday=day_of_weekfirst_dateinletdate_and_weekdays=List.mapiall_dates~f:(funidate->date,Day_of_week.shiftfirst_weekdayi)inList.filter_mapdate_and_weekdays~f:(fun(date,weekday)->ifDay_of_week.is_sun_or_satweekdaythenNoneelseSomedate));;letbusiness_dates_between~min~max~is_holiday=weekdays_between~min~max|>List.filter~f:(fund->not(is_holidayd));;letfirst_strictly_aftert~on:dow=letdow=Day_of_week.to_intdowinlettplus1=add_dayst1inletcur=Day_of_week.to_int(day_of_weektplus1)inletdiff=(dow+7-cur)mod7inadd_daystplus1diff;;moduleFor_quickcheck=structopenQuickcheckletgen_uniform_incld1d2=ifd1>d2thenraise_s[%message"Date.gen_uniform_incl: bounds are crossed"~lower_bound:(d1:t)~upper_bound:(d2:t)];Generator.map(Int.gen_uniform_incl0(diffd2d1))~f:(fundays->add_daysd1days);;letgen_incld1d2=Generator.weighted_union[1.,Generator.returnd1;1.,Generator.returnd2;18.,gen_uniform_incld1d2];;letquickcheck_generator=gen_incl(of_string"1900-01-01")(of_string"2100-01-01")letquickcheck_observer=Observer.create(funt~size:_~hash->hash_fold_thasht)letquickcheck_shrinker=Shrinker.empty()endletquickcheck_generator=For_quickcheck.quickcheck_generatorletgen_incl=For_quickcheck.gen_inclletgen_uniform_incl=For_quickcheck.gen_uniform_inclletquickcheck_observer=For_quickcheck.quickcheck_observerletquickcheck_shrinker=For_quickcheck.quickcheck_shrinkermodulePrivate=structletleap_year_table=leap_year_tableletnon_leap_year_table=non_leap_year_tableletordinal_date=ordinal_dateendmoduleOption=structmoduleStable=Stable.OptionincludeStable.V1moduleOptional_syntax=structmoduleOptional_syntax=structletis_none=is_noneletunsafe_value=unchecked_valueendendletquickcheck_generator=Quickcheck.Generator.map(Option.quickcheck_generatorquickcheck_generator)~f:of_option;;letquickcheck_shrinker=Quickcheck.Shrinker.map(Option.quickcheck_shrinkerquickcheck_shrinker)~f:of_option~f_inverse:to_option;;letquickcheck_observer=Quickcheck.Observer.of_hash(modulestructtypenonrect=t[@@derivinghash]end);;includeComparable.Make_plain(structtypenonrect=t[@@derivingcompare,sexp_of]end)end