%--------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%--------------------------------------------------%
% Copyright (C) 2009-2010 The University of Melbourne.
% Copyright (C) 2013-2019, 2025 The Mercury team.
% This file is distributed under the terms specified in COPYING.LIB.
%--------------------------------------------------%
%
% File: calendar.m.
% Main authors: maclarty
% Stability: high.
%
% Proleptic Gregorian calendar utilities.
%
% The Gregorian calendar is the calendar that is currently used by most of
% the world. In this calendar, a year is a leap year if it is divisible by
% 4, but not divisible by 100. The only exception is if the year is divisible
% by 400, in which case it is a leap year. For example 1900 is not leap year,
% while 2000 is. The proleptic Gregorian calendar is an extension of the
% Gregorian calendar backward in time to before it was first introduced in
% 1582.
%
%--------------------------------------------------%
%--------------------------------------------------%
:- module calendar.
:- interface.
:- import_module io.
%--------------------------------------------------%
% A point on the Proleptic Gregorian calendar, to the nearest microsecond.
%
:- type date.
% A more meaningful name for the above.
%
:- type date_time == date.
% Date components.
%
:- type year == int. % Year 0 is 1 BC, -1 is 2 BC, etc.
:- type day_of_month == int. % 1..31 depending on the month and year
:- type hour == int. % 0..23
:- type minute == int. % 0..59
:- type second == int. % 0..61 (60 and 61 are for leap seconds)
:- type microsecond == int. % 0..999999
:- type month
---> january
; february
; march
; april
; may
; june
; july
; august
; september
; october
; november
; december.
:- type day_of_week
---> monday
; tuesday
; wednesday
; thursday
; friday
; saturday
; sunday.
%--------------------------------------------------%
% Functions to retrieve the components of a date.
%
:- func year(date) = year.
:- func month(date) = month.
:- func day_of_month(date) = day_of_month.
:- func day_of_week(date) = day_of_week.
:- func hour(date) = hour.
:- func minute(date) = minute.
:- func second(date) = second.
:- func microsecond(date) = microsecond.
% int_to_month(Int, Month):
% Int is the number of Month where months are numbered from 1-12.
%
:- pred int_to_month(int, month).
:- mode int_to_month(in, out) is semidet.
:- mode int_to_month(out, in) is det.
% det_int_to_month(Int) returns the month corresponding to Int.
% Throws an exception if Int is not in 1-12.
%
:- func det_int_to_month(int) = month.
% int_to_month(Int, Month):
% Int is the number of Month where months are numbered from 0-11.
%
:- pred int0_to_month(int, month).
:- mode int0_to_month(in, out) is semidet.
:- mode int0_to_month(out, in) is det.
% det_int0_to_month(Int) returns the month corresponding to Int.
% Throws an exception if Int is not in 0-11.
%
:- func det_int0_to_month(int) = month.
% month_to_int(Month) returns the number of Month where months are
% numbered from 1-12.
%
:- func month_to_int(month) = int.
% month_to_int0(Month) returns the number of Month where months are
% numbered from 0-11.
%
:- func month_to_int0(month) = int.
%--------------------------------------------------%
% init_date(Year, Month, Day, Hour, Minute, Second, MicroSecond, Date):
% Initialize a new date. Fails if the given date is invalid.
%
:- pred init_date(year::in, month::in, day_of_month::in, hour::in,
minute::in, second::in, microsecond::in, date::out) is semidet.
% Same as above, but throws an exception if the date is invalid.
%
:- func det_init_date(year, month, day_of_month, hour, minute, second,
microsecond) = date.
% Retrieve all the components of a date.
%
:- pred unpack_date(date::in,
year::out, month::out, day_of_month::out, hour::out, minute::out,
second::out, microsecond::out) is det.
%--------------------------------------------------%
% Convert a string of the form "YYYY-MM-DD HH:MM:SS.mmmmmm" to a date.
% The microseconds component (.mmmmmm) is optional.
%
:- pred date_from_string(string::in, date::out) is semidet.
% Same as above, but throws an exception if the string is not a valid date.
%
:- func det_date_from_string(string) = date.
% Convert a date to a string of the form "YYYY-MM-DD HH:MM:SS.mmmmmm".
% If the microseconds component of the date is zero, then the
% ".mmmmmm" part is omitted.
%
:- func date_to_string(date) = string.
%--------------------------------------------------%
% Get the current local time.
%
:- pred current_local_time(date::out, io::di, io::uo) is det.
% Get the current UTC time.
%
:- pred current_utc_time(date::out, io::di, io::uo) is det.
% Calculate the Julian day number for a date on the Gregorian calendar.
%
:- func julian_day_number(date) = int.
% Returns 1970/01/01 00:00:00.
%
:- func unix_epoch = date.
% same_date(A, B):
% True if-and-only-if A and B are equal with respect to
% only their date components. The time components are ignored.
%
:- pred same_date(date::in, date::in) is semidet.
%--------------------------------------------------%
%
% Durations.
%
% A period of time measured in years, months, days, hours, minutes,
% seconds and microseconds. Internally a duration is represented
% using only months, days, seconds and microseconds components.
%
:- type duration.
% Duration components.
%
:- type years == int.
:- type months == int.
:- type days == int.
:- type hours == int.
:- type minutes == int.
:- type seconds == int.
:- type microseconds == int.
% Functions to retrieve duration components.
%
:- func years(duration) = years.
:- func months(duration) = months.
:- func days(duration) = days.
:- func hours(duration) = hours.
:- func minutes(duration) = minutes.
:- func seconds(duration) = seconds.
:- func microseconds(duration) = microseconds.
% init_duration(Years, Months, Days, Hours, Minutes,
% Seconds, MicroSeconds) = Duration.
% Create a new duration. All of the components should either be
% non-negative or non-positive (they can all be zero).
%
:- func init_duration(years, months, days, hours, minutes, seconds,
microseconds) = duration.
% Retrieve all the components of a duration.
%
:- pred unpack_duration(duration::in, years::out, months::out,
days::out, hours::out, minutes::out, seconds::out, microseconds::out)
is det.
% Return the zero length duration.
%
:- func zero_duration = duration.
% Negate a duration.
%
:- func negate(duration) = duration.
%--------------------------------------------------%
% Parse a duration string.
%
% The string should be of the form "PnYnMnDTnHnMnS" where each "n" is a
% non-negative integer representing the number of years (Y), months (M),
% days (D), hours (H), minutes (M) or seconds (S). The duration string
% always starts with 'P' and the 'T' separates the date and time components
% of the duration. A component may be omitted if it is zero, and the 'T'
% separator is not required if all the time components are zero.
% The second component may include a fraction component using a period.
% This fraction component should not have a resolution higher than a
% microsecond.
%
% For example the duration 1 year, 18 months, 100 days, 10 hours, 15
% minutes 90 seconds and 300 microseconds can be written as:
% P1Y18M100DT10H15M90.0003S
% while the duration 1 month and 2 days can be written as:
% P1M2D
%
% Note that internally the duration is represented using only months,
% days, seconds and microseconds, so that
% duration_to_string(det_duration_from_string("P1Y18M100DT10H15M90.0003S"))
% will result in the string "P2Y6M100DT10H16M30.0003S".
%
:- pred duration_from_string(string::in, duration::out) is semidet.
% Same as above, but throws an exception if the duration string is invalid.
%
:- func det_duration_from_string(string) = duration.
% Convert a duration to a string using the same representation
% parsed by duration_from_string.
%
:- func duration_to_string(duration) = string.
%--------------------------------------------------%
% Add a duration to a date.
%
% First the years and months are added to the date.
% If this causes the day to be out of range (e.g. April 31), then it is
% decreased until it is in range (e.g. April 30). Next the remaining
% days, hours, minutes and seconds components are added. These could
% in turn cause the month and year components of the date to change again.
%
:- pred add_duration(duration::in, date::in, date::out) is det.
% This predicate implements a partial order relation on durations.
% DurationA is less than or equal to DurationB if-and-only-if for all
% of the dates list below, adding DurationA to the date results in a date
% less than or equal to the date obtained by adding DurationB.
%
% 1696-09-01 00:00:00
% 1697-02-01 00:00:00
% 1903-03-01 00:00:00
% 1903-07-01 00:00:00
%
% There is only a partial order on durations, because some durations
% cannot be said to be less than, equal to or greater than another duration
% (e.g. 1 month vs. 30 days).
%
:- pred duration_leq(duration::in, duration::in) is semidet.
% Get the difference between local and UTC time as a duration.
%
% local_time_offset(TZ, !IO) is equivalent to:
% current_local_time(Local, !IO),
% current_utc_time(UTC, !IO),
% TZ = duration(UTC, Local)
% except that it is as if the calls to current_utc_time and
% current_local_time occurred at the same instant.
%
% To convert UTC time to local time, add the result of local_time_offset/3
% to UTC (using add_duration/3). To compute UTC given the local time,
% first negate the result of local_time_offset/3 (using negate/1) and then
% add it to the local time.
%
:- pred local_time_offset(duration::out, io::di, io::uo) is det.
% duration(DateA, DateB) = Duration.
% Find the duration between two dates using a "greedy" algorithm.
% The algorithm is greedy in the sense that it will try to maximise each
% component in the returned duration in the following order: years, months,
% days, hours, minutes, seconds, microseconds.
% The returned duration is positive if DateB is after DateA and negative
% if DateB is before DateA.
% Any leap seconds that occurred between the two dates are ignored.
% The dates should be in the same timezone and in the same daylight
% savings phase. To work out the duration between dates in different
% timezones or daylight savings phases, first convert the dates to UTC.
%
% If the seconds components of DateA and DateB are < 60 then
% add_duration(DateA, duration(DateA, DateB), DateB) will hold, but
% add_duration(DateB, negate(duration(DateA, DateB)), DateA) may not hold.
% For example if:
% DateA = 2001-01-31
% DateB = 2001-02-28
% Duration = 1 month
% then the following holds:
% add_duration(duration(DateA, DateB), DateA, DateB)
% but the following does not:
% add_duration(negate(duration(DateA, DateB), DateB, DateA)
% (Adding -1 month to 2001-02-28 will yield 2001-01-28).
%
:- func duration(date, date) = duration.
% Same as above, except that the year and month components of the
% returned duration will always be zero. The duration will be in terms
% of days, hours, minutes, seconds and microseconds only.
%
:- func day_duration(date, date) = duration.
%--------------------------------------------------%
%
% Folds over ranges of dates.
%
% foldl_days(Pred, Start, End, !Acc):
% Calls Pred for each day in the range of dates from Start to End
% with an accumulator.
% Each date in the range is generated by adding a duration of one day
% to the previous date using the add_duration/3 predicate.
% Consequently, the time components of the dates in the range may
% differ if the time components of the given start and end times
% include leap seconds.
%
:- pred foldl_days(pred(date, A, A), date, date, A, A).
:- mode foldl_days(in(pred(in, in, out) is det),
in, in, in, out) is det.
:- mode foldl_days(in(pred(in, mdi, muo) is det),
in, in, mdi, muo) is det.
:- mode foldl_days(in(pred(in, di, uo) is det),
in, in, di, uo) is det.
:- mode foldl_days(in(pred(in, in, out) is semidet),
in, in, in, out) is semidet.
:- mode foldl_days(in(pred(in, mdi, muo) is semidet),
in, in, mdi, muo) is semidet.
:- mode foldl_days(in(pred(in, di, uo) is semidet),
in, in, di, uo) is semidet.
% foldl2_days(Pred, Start, End, !Acc1, !Acc2):
% As above, but with two accumulators.
%
:- pred foldl2_days(pred(date, A, A, B, B), date, date, A, A, B, B).
:- mode foldl2_days(in(pred(in, in, out, in, out) is det),
in, in, in, out, in, out) is det.
:- mode foldl2_days(in(pred(in, in, out, mdi, muo) is det),
in, in, in, out, mdi, muo) is det.
:- mode foldl2_days(in(pred(in, in, out, di, uo) is det),
in, in, in, out, di, uo) is det.
:- mode foldl2_days(in(pred(in, in, out, in, out) is semidet),
in, in, in, out, in, out) is semidet.
:- mode foldl2_days(in(pred(in, in, out, mdi, muo) is semidet),
in, in, in, out, mdi, muo) is semidet.
:- mode foldl2_days(in(pred(in, in, out, di, uo) is semidet),
in, in, in, out, di, uo) is semidet.
% foldl3_days(Pred, Start, End, !Acc1, !Acc2, !Acc3):
% As above, but with three accumulators.
%
:- pred foldl3_days(pred(date, A, A, B, B, C, C), date, date,
A, A, B, B, C, C).
:- mode foldl3_days(in(pred(in, in, out, in, out, in, out) is det),
in, in, in, out, in, out, in, out) is det.
:- mode foldl3_days(in(pred(in, in, out, in, out, mdi, muo) is det),
in, in, in, out, in, out, mdi, muo) is det.
:- mode foldl3_days(in(pred(in, in, out, in, out, di, uo) is det),
in, in, in, out, in, out, di, uo) is det.
:- mode foldl3_days(in(pred(in, in, out, in, out, in, out) is semidet),
in, in, in, out, in, out, in, out) is semidet.
:- mode foldl3_days(in(pred(in, in, out, in, out, mdi, muo) is semidet),
in, in, in, out, in, out, mdi, muo) is semidet.
:- mode foldl3_days(in(pred(in, in, out, in, out, di, uo) is semidet),
in, in, in, out, in, out, di, uo) is semidet.
%--------------------------------------------------%
%--------------------------------------------------%