%--------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%--------------------------------------------------%
% Copyright (C) 2009-2010 The University of Melbourne.
% Copyright (C) 2013-2019, 2025-2026 The Mercury team.
% This file is distributed under the terms specified in COPYING.LIB.
%--------------------------------------------------%
%
% File: calendar.m.
% Main authors: maclarty
% Stability: high.
%
% This module provides a representation of points in time,
% a representation of durations (differences between two points in time),
% and operations on those representations.
%
% This module identifies points in time by a date_time specifying a day,
% and a time within that day. It uses dates from the proleptic Gregorian
% calendar, which is a version of the Gregorian calendar that has been
% extended backward in time to dates before its introduction in 1582.
% (https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar contains
% a detailed description.) This is the calendar that is currently used
% by most of the world.
%
% This module allows times to be represented at microsecond resolution,
% though of course not all sources of time information are that precise.
%
%--------------------------------------------------%
%--------------------------------------------------%
:- module calendar.
:- interface.
:- import_module io.
%--------------------------------------------------%
% A point on the proleptic Gregorian calendar, to the nearest microsecond.
% A date_time carries no time zone information; it is the responsibility
% of code that creates and uses date_times to ensure that any two
% date_times passed to the same operation refer to the same time zone.
% To convert between local time and UTC, see local_time_offset/3.
%
:- type date_time.
% A deprecated name for date_time.
% In a future release, this name will be used for date values without
% a time component.
%
:- type date == date_time.
% Date_time 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 .. 999,999
:- 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_time.
%
:- func year(date_time) = year.
:- func month(date_time) = month.
:- func day_of_month(date_time) = day_of_month.
:- func day_of_week(date_time) = day_of_week.
:- func hour(date_time) = hour.
:- func minute(date_time) = minute.
:- func second(date_time) = second.
:- func microsecond(date_time) = 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.
% Throw an exception if Int is not in 1-12.
%
:- func det_int_to_month(int) = month.
% int0_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.
% Throw 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.
% days_in_month(Year, Month) = Days:
%
% Return the number of days in Month of Year in the proleptic
% Gregorian calendar.
%
:- func days_in_month(year, month) = int.
% is_leap_year(Year):
%
% Succeed if-and-only-if Year is a leap year in the proleptic
% Gregorian calendar.
%
% The rules are:
% - A year divisible by 400 is a leap year.
% - A year not divisible by 400 but divisible by 100 is NOT a leap year.
% - A year not divisible by 100 but divisible by 4 IS a leap year.
% - A year not divisible by 4 is NOT a leap year.
%
:- pred is_leap_year(year::in) is semidet.
%--------------------------------------------------%
% init_date_time(Year, Month, Day, Hour, Minute, Second, MicroSecond,
% DateTime):
%
% Initialise a new date_time from the given components. Fails if any of the
% following conditions are not met:
%
% - Day is in the range 1 .. N,
% where N is the number of days in Month of Year
%
% - Hour is in the range 0 .. 23
%
% - Minute is in the range 0 .. 59
%
% - Second is in the range 0 .. 61
% (to account for up to two leap seconds being added in a year)
%
% - MicroSecond is in the range 0 .. 999,999
%
% This predicate accepts all values for Year.
%
:- pred init_date_time(year::in, month::in, day_of_month::in, hour::in,
minute::in, second::in, microsecond::in, date_time::out) is semidet.
:- pred init_date(year::in, month::in, day_of_month::in, hour::in,
minute::in, second::in, microsecond::in, date_time::out) is semidet.
:- pragma obsolete(pred(init_date/8), [init_date_time/8]).
% As above, but throw an exception if the date is invalid.
%
:- func det_init_date_time(year, month, day_of_month, hour, minute, second,
microsecond) = date_time.
:- func det_init_date(year, month, day_of_month, hour, minute, second,
microsecond) = date_time.
:- pragma obsolete(func(det_init_date/7), [det_init_date_time/7]).
% Retrieve all the components of a date_time.
%
:- pred unpack_date_time(date_time::in,
year::out, month::out, day_of_month::out, hour::out, minute::out,
second::out, microsecond::out) is det.
:- pred unpack_date(date_time::in,
year::out, month::out, day_of_month::out, hour::out, minute::out,
second::out, microsecond::out) is det.
:- pragma obsolete(pred(unpack_date/8), [unpack_date_time/8]).
%--------------------------------------------------%
% Convert a string of the form "[-]YYYY-MM-DD HH:MM:SS.mmmmmm" to a
% date_time.
%
% The year must have at least four digits. This requirement comes from
% ISO standard 8601, and its main intention is to prevent repeats of
% the Y2K problem (see https://en.wikipedia.org/wiki/Year_2000_problem).
% It also prevents possible confusion between the year part of the date,
% and the month or the day parts.
%
% Since some simulation programs may want to handle date_times in the far
% future, the predicate accepts years with more than four digits.
%
% The microseconds component (.mmmmmm) is optional. If present,
% it may have between one and six digits.
%
% This predicate fails if the string does not conform to the above format,
% or if any date or time component is outside its valid range.
%
:- pred date_time_from_string(string::in, date_time::out) is semidet.
:- pred date_from_string(string::in, date_time::out) is semidet.
:- pragma obsolete(pred(date_from_string/2), [date_time_from_string/2]).
% As above, but throw an exception if the string is not a valid date_time.
%
:- func det_date_time_from_string(string) = date_time.
:- func det_date_from_string(string) = date_time.
:- pragma obsolete(func(det_date_from_string/1),
[det_date_time_from_string/1]).
% Convert a date_time to a string of the form "[-]YYYY-MM-DD HH:MM:SS.mmmmmm".
% If the microseconds component of the date_time is zero, then omit the
% ".mmmmmm" part.
%
:- func date_time_to_string(date_time) = string.
:- func date_to_string(date_time) = string.
:- pragma obsolete(func(date_to_string/1), [date_time_to_string/1]).
%--------------------------------------------------%
% current_local_time(Now, !IO):
%
% Return the current local time as a date_time. The microseconds component
% of the returned date_time is always zero, as the underlying system call
% has only second-level resolution. The timezone used is the system local
% timezone.
%
:- pred current_local_time(date_time::out, io::di, io::uo) is det.
% current_utc_time(Now, !IO):
%
% Return the current UTC time as a date_time. The microseconds component of
% the returned date_time is always zero, as the underlying system call has
% only second-level resolution.
%
:- pred current_utc_time(date_time::out, io::di, io::uo) is det.
% julian_day_number(DateTime) = JDN:
%
% Return the Julian day number for DateTime on the proleptic Gregorian
% calendar. The Julian day number is the integer number of days since
% the start of the Julian period (noon on 1 January, 4713 BC in the
% proleptic Julian calendar). The time-of-day components of DateTime are
% ignored; the result is the Julian day number for the date at noon.
%
:- func julian_day_number(date_time) = int.
% Return the Unix epoch, 1970-01-01 00:00:00.
%
:- func unix_epoch = date_time.
% same_date(A, B):
%
% Succeed if-and-only-if A and B refer to the exact same day.
% Their time components are ignored. A and B should refer to
% the same time zone; comparing dates in different time zones
% may give incorrect results, because the same point in time can
% fall on different days in different time zones.
%
:- pred same_date(date_time::in, date_time::in) is semidet.
%--------------------------------------------------%
%
% Durations.
%
% A period of time measured in years, months, days, hours, minutes,
% seconds and microseconds.
%
% A duration may be positive (moving a date forward in time) or negative
% (moving a date backward in time). All non-zero components must share the
% same sign; a duration with a mix of positive and negative components
% cannot be constructed.
%
% Years and months are context-dependent units whose length in absolute
% time varies with the dates they are applied to. A year is treated as
% 12 months, and a month is 28-31 days depending on the calendar month
% and year. In contrast, days and smaller units are fixed-length:
% 1 day = 86,400 seconds (leap seconds are ignored; see below).
%
% When adding a year or month component causes the day to fall outside
% the target month, it is clamped to the last day of that month.
% This applies equally to positive and negative durations. For example:
%
% - Adding 1 month to January 31 gives February 28 (29 in a leap year)
% - Adding 1 year to February 29, 2020 gives February 28, 2021
% - Adding -1 month to March 31 gives February 28 (29 in a leap year)
% - Adding -1 year to February 29, 2020 gives February 28, 2019
%
% Note on leap seconds: although individual dates can represent times
% with leap seconds (seconds 60-61), durations ignore them. A day is
% always treated as exactly 86,400 seconds, even though UTC days
% containing leap seconds are 86,401 or 86,402 seconds long.
%
% Durations are stored internally using four components only: months, days,
% seconds and microseconds. When a duration is constructed by
% init_duration/7, the seven input components are normalised into these
% four.
%
% - Years are converted to months and added to the months component
% - Microseconds are divided into whole seconds (which are carried over)
% and a microseconds remainder.
% - Hours, minutes, seconds, and any carried seconds are combined into a
% total number of seconds.
% - Whole days in that seconds total are carried into the days component,
% and the remainder becomes the seconds component.
%
% Days are never folded into months (a month does not have a fixed number
% of days), and months are never folded into years during normalisation.
% As a result, the duration component access functions may return values
% that differ from the init_duration/7 arguments. For example:
%
% - init_duration(1, 18, 0, 0, 0, 0, 0) => years = 2, months = 6
% - init_duration(0, 0, 0, 25, 0, 0, 0) => days = 1, hours = 1
%
:- 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 the components of a duration.
%
% Years and months are derived from the single combined months total
% in the duration:
%
% The years function returns total months // 12
% The months function returns total months rem 12
%
% Hours, minutes and seconds are derived from the single combined seconds
% total in the duration:
%
% The hours function returns total seconds // 3600
% The minutes function returns total seconds rem 3600 // 60
% The seconds function returns total seconds rem 60
%
% Days and microseconds are each derived from their own component
% and returned directly.
%
% The days function returns total days
% The microseconds function returns total microseconds
%
% For positive durations:
% months/1 returns a value in the range 0 .. 11
% days/1 returns a value in the range 0 .. max_int
% hours/1 returns a value in the range 0 .. 23
% minutes/1 returns a value in the range 0 .. 59
% seconds/1 returns a value in the range 0 .. 59
% microseconds/1 returns a value in the range 0 .. 999,999
%
% For negative durations:
% months/1 returns a value in the range -11 .. 0
% days/1 returns a value in the range min_int .. 0
% hours/1 returns a value in the range -23 .. 0
% minutes/1 returns a value in the range -59 .. 0
% seconds/1 returns a value in the range -59 .. 0
% microseconds/1 returns a value in the range -999,999 .. 0
%
:- 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 from the given components.
% All non-zero components must have the same sign (they must be entirely
% positive or entirely negative). This function throws an exception if
% two non-zero components have different signs.
%
% For example, all of the following are valid:
%
% - init_duration(1, 2, 15, 0, 0, 0, 0) (all positive or zero)
% - init_duration(0, 0, -3, -12, 0, 0, 0) (all negative or zero)
% - init_duration(0, 0, 0, 0, 0, 0, 0) (all zero)
%
% But the following contain non-zero components with mixed signs and will
% throw an exception:
%
% - init_duration(0, 1, -5, 0, 0, 0, 0)
% - init_duration(0, 0, 0, 2, -30, 0, 0)
%
% If you need a fixed absolute time period that is independent of calendar
% context, then use only the days, hours, minutes, seconds and microseconds
% components.
%
:- 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 have 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 units must follow
% the numbers, not precede them. The duration string always starts with
% either 'P' or '-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 seconds component may include a fraction component using a period.
% This fraction component cannot include more than six digits, since
% the maximum resolution of a duration is a microsecond.
%
% Fail if the string does not conform to the above format, or if the
% fractional part of the seconds component has more than six digits.
%
% 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 a negative duration of 1 month and 2 days can be written as:
%
% -P1M2D
%
% Note that this predicate normalises its input, so that (for example)
% duration_to_string(det_duration_from_string("P1Y18M100DT10H15M90.0003S"))
% will return "P2Y6M100DT10H16M30.0003S".
%
:- pred duration_from_string(string::in, duration::out) is semidet.
% As above, but throw 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_duration(Duration, DateTime0, DateTime):
%
% Add Duration to DateTime0 to yield DateTime, clamping the day to the end
% of the month if the month or year component of the duration causes it to
% fall out of range.
% (See the documentation of the type duration/0 for the clamping rules.)
%
:- pred add_duration(duration::in, date_time::in, date_time::out) is det.
% duration_leq(DurationA, DurationB):
%
% Succeed if-and-only-if DurationA is less than or equal to DurationB.
% This relation is a partial order: some pairs of durations are
% incomparable, because their relative size depends on the date they
% are applied to (e.g. 1 month vs. 30 days may compare differently
% in different months).
%
% DurationA is considered less than or equal to DurationB if adding
% DurationA to each of the following dates yields a result no later
% than adding DurationB to the same date. These dates are chosen to
% exercise all possible combinations of leap-year and variable
% month-length boundaries:
%
% 1696-09-01 00:00:00
% 1697-02-01 00:00:00
% 1903-03-01 00:00:00
% 1903-07-01 00:00:00
%
% The predicate fails if DurationA is greater than DurationB for any
% of the above dates, including the case where the two durations are
% incomparable (i.e. DurationA yields an earlier result for some test
% dates but a later result for others).
%
:- pred duration_leq(duration::in, duration::in) is semidet.
% local_time_offset(Offset, !IO):
%
% Offset is the difference between local and UTC time, that is, the
% value of duration(UTC, Local), where Local and UTC are the local and UTC
% representations of the same point in time. Offset reflects the system's
% current daylight savings state at the time of the call.
%
% To convert UTC time to local time, add Offset to UTC using
% add_duration/3. To convert local time to UTC, negate Offset using
% negate/1, and add the result to the local time.
%
:- pred local_time_offset(duration::out, io::di, io::uo) is det.
% duration(DateTimeA, DateTimeB) = Duration:
%
% Return the duration from DateTimeA to DateTimeB using a greedy algorithm
% that maximises each component in this order: years, months, days, hours,
% minutes, seconds, microseconds. The result is positive if DateTimeB is
% after DateTimeA and negative if DateTimeB is before DateTimeA. Leap
% seconds are ignored.
%
% The dates should be in the same timezone and daylight savings phase;
% to find the duration between dates in different timezones or daylight
% savings phases, first convert them both to UTC.
%
% Note that due to month-end clamping, duration/2 is not always the
% inverse of add_duration/3. For example, the duration from 2001-01-31
% to 2001-02-28 is 1 month, but adding -1 month to 2001-02-28 yields
% 2001-01-28, not 2001-01-31.
%
:- func duration(date_time, date_time) = duration.
% As for duration/2, but the year and month components of the returned
% duration are always zero; the result is expressed in days, hours,
% minutes, seconds and microseconds only.
%
:- func day_duration(date_time, date_time) = duration.
%--------------------------------------------------%
%
% Folds over ranges of date_times.
%
% foldl_days(Pred, Start, End, !Acc):
%
% Call Pred for each date_time in the range Start to End (inclusive),
% passing an accumulator. Each date_time in the range is generated by
% adding a duration of one day to the previous date using add_duration/3.
%
:- pred foldl_days(pred(date_time, A, A), date_time, date_time, 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_time, A, A, B, B), date_time, date_time,
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_time, A, A, B, B, C, C), date_time, date_time,
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.
%--------------------------------------------------%
%--------------------------------------------------%