Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Source file dates.ml
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288(* This file is part of the Dates_calc library. Copyright (C) 2022 Inria,
contributors: Denis Merigoux <denis.merigoux@inria.fr>, Aymeric Fromherz
<aymeric.fromherz@inria.fr>, Raphaël Monat <raphael.monat@inria.fr>
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License. *)[@@@warning"-27"]typedate={year:int;month:int;day:int}(** A valid date in the standard Gregorian calendar. *)typeperiod={years:int;months:int;days:int}(** A period can be any number and combination of days, months, years. *)exceptionInvalidDateexceptionAmbiguousComputationtypedate_rounding=|RoundUp|RoundDown|AbortOnRound(** When choosing [AbortOnRound], functions may raise
[AmbiguousComputation]. *)(** {2 Functions on periods}*)letmake_period~(years:int)~(months:int)~(days:int):period={years;months;days}letformat_period(fmt:Format.formatter)(p:period):unit=Format.fprintffmt"[%d years, %d months, %d days]"p.yearsp.monthsp.daysletperiod_of_stringstr=tryScanf.sscanfstr"[%d years, %d months, %d days]"(funyearsmonthsdays->make_period~years~months~days)withScanf.Scan_failure_->invalid_arg"period_of_string"letadd_periods(d1:period)(d2:period):period={years=d1.years+d2.years;months=d1.months+d2.months;days=d1.days+d2.days;}letsub_periods(d1:period)(d2:period):period={years=d1.years-d2.years;months=d1.months-d2.months;days=d1.days-d2.days;}letmul_period(d1:period)(m:int):period={years=d1.years*m;months=d1.months*m;days=d1.days*m}(** @raise [AmbiguousComputation]
when the period is anything else than a number of days. *)letperiod_to_days(p:period):int=ifp.years<>0||p.months<>0thenraiseAmbiguousComputationelsep.days(** {2 Functions on dates}*)letis_leap_year(year:int):bool=yearmod400=0||(yearmod4=0&&yearmod100<>0)(** @raise [InvalidDate]*)letdays_in_month~(month:int)~(is_leap_year:bool):int=matchmonthwith|1|3|5|7|8|10|12->31|4|6|9|11->30|2->ifis_leap_yearthen29else28|_->raiseInvalidDateletis_valid_date(d:date):bool=tryd.day>=1&&d.day<=days_in_month~month:d.month~is_leap_year:(is_leap_yeard.year)withInvalidDate->false(** @raise [InvalidDate]*)letmake_date~(year:int)~(month:int)~(day:int):date=letd={year;month;day}inifis_valid_datedthendelseraiseInvalidDate(** Returns new [year, month]. Precondition: [1 <= month <= 12] *)letrecadd_months_to_first_of_month_date~(year:int)~(month:int)~(months:int):int*int=letnew_month=month+monthsinif1<=new_month&&new_month<=12thenyear,new_monthelseifnew_month>12thenadd_months_to_first_of_month_date~year:(year+1)~month~months:(months-12)else(* new_month <= 0 *)add_months_to_first_of_month_date~year:(year-1)~month~months:(months+12)(* If the date is valid, does nothing. We expect the month number to be always
valid when calling this. If the date is invalid due to the day number, then
this function rounds down: if the day number is >= days_in_month, to the last
day of the current month. *)letprev_valid_date(d:date):date=assert(1<=d.month&&d.month<=12);assert(1<=d.day&&d.day<=31);ifis_valid_datedthendelse{dwithday=days_in_month~month:d.month~is_leap_year:(is_leap_yeard.year);}(* If the date is valid, does nothing. We expect the month number to be always
valid when calling this. If the date is invalid due to the day number, then
this function rounds down: if the day number is >= days_in_month, to the
first day of the next month. *)letnext_valid_date(d:date):date=assert(1<=d.month&&d.month<=12);assert(1<=d.day&&d.day<=31);ifis_valid_datedthendelseletnew_year,new_month=add_months_to_first_of_month_date~year:d.year~month:d.month~months:1in{year=new_year;month=new_month;day=1}letround_date~(round:date_rounding)(new_date:date)=ifis_valid_datenew_datethennew_dateelsematchroundwith|AbortOnRound->raiseAmbiguousComputation|RoundDown->prev_valid_datenew_date|RoundUp->next_valid_datenew_date(** This function is only ever called from `add_dates` below.
Hence, any call to `add_dates_years` will be followed by a call
to `add_dates_month`. We therefore perform a single rounding
in `add_dates_month`, to avoid introducing additional imprecision here,
and to ensure that adding n years + m months is always equivalent to
adding (12n + m) months *)letadd_dates_years~(round:date_rounding)(d:date)(years:int):date={dwithyear=d.year+years}letadd_dates_month~(round:date_rounding)(d:date)(months:int):date=letnew_year,new_month=add_months_to_first_of_month_date~year:d.year~month:d.month~monthsinletnew_date={dwithyear=new_year;month=new_month}inround_date~roundnew_dateletrecadd_dates_days(d:date)(days:int)=(* Hello, dear reader! Buckle up because it will be a hard ride. The first
thing to do here is to retrieve how many days there are in the current
month of [d]. *)letdays_in_d_month=days_in_month~month:d.month~is_leap_year:(is_leap_yeard.year)in(* Now, we case analyze of the situation. To do that, we add the current days
of the month with [days], and see what happens. Beware, [days] is algebraic
and can be negative! *)letnew_day=d.day+daysinif1<=new_day&&new_day<=days_in_d_monththen(* The first case is the easy one: when you add [days], the new day keeps
being a valid day in the current month. All is good, we simply warp to
that new date without any further changes. *){dwithday=new_day}elseifnew_day>=days_in_d_monththen(* Now, we deal with the case where there is an overflow : you have added
too many days and the current month cannot handle them any more. The
strategy here is to fill the current month, and let the next month handle
the situation via a recursive call. *)letnew_year,new_month=add_months_to_first_of_month_date~year:d.year~month:d.month~months:1inadd_dates_days(* We warp to the first day of the next month! *){year=new_year;month=new_month;day=1}(* Now we compute how many days we still have left to add. Because we have
warped to the next month, we already have added the rest of the days in
the current month: [days_in_d_month - d.day]. But then we switch
months, and that corresponds to adding another day. *)(days-(days_in_d_month-d.day)-1)else(* The last case is symmetrical, we substracted too many days and the
current month can't handle it. So we warp to the previous month and let a
recursive call handle the situation from there. *)letnew_year,new_month=add_months_to_first_of_month_date~year:d.year~month:d.month~months:(-1)inadd_dates_days(* We warp to the last day of the previous month. *){year=new_year;month=new_month;day=days_in_month~month:new_month~is_leap_year:(is_leap_yearnew_year);}(* What remains to be substracted (as [days] is negative) has to be
diminished by the number of days of the date in the current month. *)(days+d.day)(** @raise [AmbiguousComputation] *)letadd_dates?(round:date_rounding=AbortOnRound)(d:date)(p:period):date=letd=add_dates_years~rounddp.yearsin(* NB: after add_dates_years, the date may not be correct.
Rounding will be performed later, by add_dates_month *)letd=add_dates_month~rounddp.monthsinletd=add_dates_daysdp.daysindletcompare_dates(d1:date)(d2:date):int=ifInt.compared1.yeard2.year=0thenifInt.compared1.monthd2.month=0thenInt.compared1.dayd2.dayelseInt.compared1.monthd2.monthelseInt.compared1.yeard2.year(** Respects ISO8601 format. *)letformat_date(fmt:Format.formatter)(d:date):unit=Format.fprintffmt"%04d-%02d-%02d"d.yeard.monthd.dayletdate_of_stringstr=tryScanf.sscanfstr"%04d-%02d-%02d"(funyearmonthday->make_date~year~month~day)withScanf.Scan_failure_->invalid_arg"date_of_string"letfirst_day_of_month(d:date):date=assert(is_valid_dated);make_date~year:d.year~month:d.month~day:1letlast_day_of_month(d:date):date=assert(is_valid_dated);letdays_month=days_in_month~month:d.month~is_leap_year:(is_leap_yeard.year)inmake_date~year:d.year~month:d.month~day:days_monthletneg_period(p:period):period={years=-p.years;months=-p.months;days=-p.days}(** The returned [period] is always expressed as a number of days. *)letrecsub_dates(d1:date)(d2:date):period=ifd1.year=d2.year&&d1.month=d2.monththen(* Easy case: the two dates are in the same month. *)make_period~years:0~months:0~days:(d1.day-d2.day)else(* Otherwise we'll add a month forward if d2 is after d1.*)letcmp=compare_datesd1d2inifcmp<0then(* The case were d1 is after d2 is symmetrical so we handle it via a
recursive call changing the order of the arguments. *)neg_period(sub_datesd2d1)else(* we know cmp != 0 so cmp > 0*)(* We warp d2 to the first day of the next month. *)letnew_d2_year,new_d2_month=add_months_to_first_of_month_date~year:d2.year~month:d2.month~months:1inletnew_d2={year=new_d2_year;month=new_d2_month;day=1}in(* Next we divide the result between the number of days we've added to go
to the end of the month, and the remaining handled by a recursive
call. *)add_periods(make_period~years:0~months:0~days:(* The number of days is the difference between the last day of the
month and the current day of d1, plus one day because we go to
the next month. *)(days_in_month~month:d2.month~is_leap_year:(is_leap_yeard2.year)-d2.day+1))(sub_datesd1new_d2)letdate_to_ymd(d:date):int*int*int=d.year,d.month,d.dayletperiod_to_ymds(p:period):int*int*int=p.years,p.months,p.days