%---------------------------------------------------------------------------%
% 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 .. 60 (60 is for a positive leap second)
:- 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 .. 60
    %     (to account for one positive leap second being added to a day)
    %
    %   - 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_time 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
    % the 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_time, 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;
    % - any date or time component is outside its valid range; or
    % - the absolute value of the year cannot be represented by Mercury's
    %   int type.
    %
:- 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 time zone used is the system local
    % time zone.
    %
:- 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 date_times can represent times
    % with leap seconds (second 60), durations ignore them. A day is
    % always treated as exactly 86,400 seconds, even though UTC days
    % containing leap seconds are 86,399 or 86,401 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). Each number precedes its
    % designator character. The string always starts with either 'P' or '-P',
    % and the 'T' separates the date components from the time components.
    %
    % A component may be omitted entirely, in which case it is treated as zero.
    % For example, "P1Y" is one year, with the months and days components zero.
    % Omitting a component means leaving out both its number and its
    % designator. A designator with no number before it, such as the 'M' in
    % "P1YM", is not permitted.
    %
    % The 'T' separator must be present if and only if at least one time
    % component (hours, minutes or seconds) is present. A 'T' with no time
    % component after it (e.g. "P1DT") is not permitted.
    %
    % 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.
    %
    % This predicates fails if
    % - the string does not conform to the above format;
    % - the fractional part of the seconds component has more than
    %   six digits; or
    % - any of the duration components cannot be represented by Mercury's
    %   int type.
    %
    % 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_between(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 saving 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_between(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 time zone and daylight saving phase;
    % to find the duration between dates in different time zones or daylight
    % saving phases, first convert them both to UTC.
    %
    % Note that due to month-end clamping, duration_between/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_between(date_time, date_time) = duration.

:- func duration(date_time, date_time) = duration.
:- pragma obsolete(func(duration/2), [duration_between/2]).

    % fixed_duration_between(DateTimeA, DateTimeB) = Duration:
    %
    % Return the duration from DateTimeA to DateTimeB, expressed using only
    % fixed-length units: days, hours, minutes, seconds and microseconds.
    % The years and months components of the returned duration are always
    % zero. 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 time zone and daylight saving phase;
    % to find the duration between dates in different time zones or daylight
    % saving phases, first convert them both to UTC.
    %
:- func fixed_duration_between(date_time, date_time) = duration.

:- func day_duration(date_time, date_time) = duration.
:- pragma obsolete(func(day_duration/2), [fixed_duration_between/2]).

%---------------------------------------------------------------------------%
%
% 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_time 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.

%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%

:- implementation.

:- import_module char.
:- import_module int.
:- import_module list.
:- import_module require.
:- import_module string.
:- import_module time.

%---------------------------------------------------------------------------%

:- type date_time
    --->    date_time(
                dt_year             :: int,
                dt_month            :: int,
                dt_day              :: int,
                dt_hour             :: int,
                dt_minute           :: int,
                dt_second           :: int,
                dt_microsecond      :: int
            ).

:- type duration
    --->    duration(
                dur_months          :: int,
                dur_days            :: int,
                dur_seconds         :: int,
                dur_microseconds    :: int
            ).

%---------------------------------------------------------------------------%

year(DateTime) = DateTime ^ dt_year.
month(DateTime) = det_int_to_month(DateTime ^ dt_month).
day_of_month(DateTime) = DateTime ^ dt_day.
day_of_week(DateTime) = compute_day_of_week(DateTime).
hour(DateTime) = DateTime ^ dt_hour.
minute(DateTime) = DateTime ^ dt_minute.
second(DateTime) = DateTime ^ dt_second.
microsecond(DateTime) = DateTime ^ dt_microsecond.

:- func compute_day_of_week(date_time) = day_of_week.

compute_day_of_week(DateTime) = DayOfWeek :-
    % We compute the day of the week by working out the Julian day modulo 7.
    JDN = julian_day_number(DateTime),
    Mod = JDN mod 7,
    DayOfWeek = det_day_of_week_from_mod(Mod).

:- func det_day_of_week_from_mod(int) = day_of_week.

det_day_of_week_from_mod(Mod) = DayOfWeek :-
    ( if day_of_week_num(DayOfWeek0, Mod) then
        DayOfWeek = DayOfWeek0
    else
        unexpected($pred, "invalid mod: " ++ int_to_string(Mod))
    ).

:- pred day_of_week_num(day_of_week, int).
:- mode day_of_week_num(in, out) is det.
:- mode day_of_week_num(out, in) is semidet.

day_of_week_num(monday, 0).
day_of_week_num(tuesday, 1).
day_of_week_num(wednesday, 2).
day_of_week_num(thursday, 3).
day_of_week_num(friday, 4).
day_of_week_num(saturday, 5).
day_of_week_num(sunday, 6).

int_to_month(1, january).
int_to_month(2, february).
int_to_month(3, march).
int_to_month(4, april).
int_to_month(5, may).
int_to_month(6, june).
int_to_month(7, july).
int_to_month(8, august).
int_to_month(9, september).
int_to_month(10, october).
int_to_month(11, november).
int_to_month(12, december).

det_int_to_month(Int) =
    ( if int_to_month(Int, Month) then
        Month
    else
        unexpected($pred, "invalid month: " ++ int_to_string(Int))
    ).

int0_to_month(Int, Month) :-
    int_to_month(Int + 1, Month).

det_int0_to_month(Int) =
    ( if int0_to_month(Int, Month) then
        Month
    else
        unexpected($pred, "invalid month: " ++ int_to_string(Int))
    ).

month_to_int(Month) = Int :-
    int_to_month(Int, Month).

month_to_int0(Month) = Int :-
    int0_to_month(Int, Month).

days_in_month(Year, Month) =
    max_day_in_month_for(Year, month_to_int(Month)).

is_leap_year(Year) :-
    % The rule numbers here refer to the comment on the declaration
    % of this predicate, duplicated here:
    % - 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.
    %
    % Note that while the description is clearest if we go from the
    % largest divisors to the smallest (because this allows us to avoid
    % talking about exceptions nested within exceptions), efficiency
    % is better served by going from the smallest divisors to the largest.
    ( if Year /\ 3 \= 0 then
        % Rule 4: Year is not divisible by 4, so it is not a leap year.
        fail
    else
        % Year is divisible by 4. Is it divisible by 100?
        ( if Year `unchecked_rem` 100 \= 0 then
            % Rule 3: Year is not divisible by 100, but is divisible
            % by 4, so it is a leap year.
            true
        else
            % Year is divisible by 100. Is it divisible by 400?
            ( if Year `unchecked_rem` 400 \= 0 then
                % Rule 2: Year is not divisible by 400, but is divisible
                % by 100, so it is not a leap year.
                fail
            else
                % Rule 1: Year is divisible by 400, so it is a leap year.
                true
            )
        )
    ).

%---------------------------------------------------------------------------%

init_date_time(Year, Month, Day, Hour, Minute, Second, MicroSecond,
        DateTime) :-
    Day >= 1,
    Day =< days_in_month(Year, Month),
    Hour >= 0,
    Hour < 24,
    Minute >= 0,
    Minute < 60,
    Second >= 0,
    Second < 61,
    MicroSecond >= 0,
    MicroSecond < 1_000_000,
    DateTime = date_time(Year, month_to_int(Month), Day, Hour, Minute, Second,
        MicroSecond).

init_date(Year, Month, Day, Hour, Minute, Second, MicroSecond, DateTime) :-
    init_date_time(Year, Month, Day, Hour, Minute, Second, MicroSecond,
        DateTime).

det_init_date_time(Year, Month, Day, Hour, Minute, Second, MicroSecond)
        = DateTime :-
    ( if
        init_date_time(Year, Month, Day, Hour, Minute, Second, MicroSecond,
            DateTime0)
    then
        DateTime = DateTime0
    else
        Msg = string.format("invalid date_time: %i-%i-%i %i:%i:%i",
            [i(Year), i(month_to_int(Month)), i(Day), i(Hour),
            i(Minute), i(Second)]),
        unexpected($pred, Msg)
    ).

det_init_date(Year, Month, Day, Hour, Minute, Second, MicroSecond) =
    det_init_date_time(Year, Month, Day, Hour, Minute, Second, MicroSecond).

unpack_date_time(DateTime, Year, Month, Day, Hour, Minute, Second,
        MicroSecond) :-
    DateTime = date_time(Year, IntMonth, Day, Hour, Minute, Second,
        MicroSecond),
    Month = det_int_to_month(IntMonth).

unpack_date(DateTime, Year, Month, Day, Hour, Minute, Second,
        MicroSecond) :-
    unpack_date_time(DateTime, Year, Month, Day, Hour, Minute, Second,
        MicroSecond).

%---------------------------------------------------------------------------%

date_time_from_string(Str, DateTime) :-
    some [!Chars] (
        !:Chars = string.to_char_list(Str),
        ( if read_next_char((-), !Chars) then
            read_int_and_return_num_digits(Year0, YearChars, !Chars),
            Year = -Year0
        else
            read_int_and_return_num_digits(Year, YearChars, !Chars)
        ),
        YearChars >= 4,
        read_next_char((-), !Chars),
        read_int_and_return_num_digits(Month, 2, !Chars),
        Month >= 1,
        Month =< 12,
        read_next_char((-), !Chars),
        read_int_and_return_num_digits(Day, 2, !Chars),
        Day >= 1,
        Day =< max_day_in_month_for(Year, Month),
        read_next_char(' ', !Chars),
        read_int_and_return_num_digits(Hour, 2, !Chars),
        Hour >= 0,
        Hour =< 23,
        read_next_char((:), !Chars),
        read_int_and_return_num_digits(Minute, 2, !Chars),
        Minute >= 0,
        Minute =< 59,
        read_next_char((:), !Chars),
        read_int_and_return_num_digits(Second, 2, !Chars),
        Second >= 0,
        Second < 61,
        read_microseconds(MicroSecond, !Chars),
        !.Chars = [],
        DateTime = date_time(Year, Month, Day, Hour, Minute, Second,
            MicroSecond)
    ).

date_from_string(Str, Date) :-
    date_time_from_string(Str, Date).

det_date_time_from_string(Str) = DateTime :-
    ( if date_time_from_string(Str, DateTime0) then
        DateTime = DateTime0
    else
        unexpected($pred, "invalid date_time: " ++ Str)
    ).

det_date_from_string(Str) = det_date_time_from_string(Str).

date_time_to_string(DateTime) = Str :-
    unpack_date_time(DateTime, Year0, Month, Day, Hour, Minute, Second,
        MicroSecond),
    ( if Year0 < 0 then
        SignStr = "-",
        Year = -Year0
    else
        SignStr = "",
        Year = Year0
    ),
    MicroSecondStr = microsecond_string(MicroSecond),
    Str = string.format("%s%04d-%02d-%02d %02d:%02d:%02d%s",
        [s(SignStr), i(Year), i(month_to_int(Month)), i(Day),
        i(Hour), i(Minute), i(Second), s(MicroSecondStr)]).

date_to_string(DateTime) = date_time_to_string(DateTime).

%---------------------------------------------------------------------------%

current_local_time(Now, !IO) :-
    time.time(TimeT, !IO),
    time.localtime(TimeT, TM, !IO),
    Now = tm_to_date(TM).

current_utc_time(Now, !IO) :-
    time.time(TimeT, !IO),
    TM = time.gmtime(TimeT),
    Now = tm_to_date(TM).

:- func tm_to_date(time.tm) = date_time.

tm_to_date(TM) = DateTime :-
    TM = tm(TMYear, TMMonth, TMDay, TMHour, TMMinute, TMSecond, _, _, _),
    Year = 1900 + TMYear,
    Month = TMMonth + 1,
    Day = TMDay,
    Hour = TMHour,
    Minute = TMMinute,
    Second = TMSecond,
    DateTime = date_time(Year, Month, Day, Hour, Minute, Second, 0).

julian_day_number(date_time(Year, Month, Day, _, _, _, _)) = JDN :-
    % The algorithm is described at
    % https://en.wikipedia.org/wiki/Julian_day.
    A = (14 - Month) div 12,
    Y = Year + 4800 - A,
    M = Month + 12 * A - 3,
    JDN = Day + ( 153 * M + 2 ) div 5 + 365 * Y + Y div 4 - Y div 100 +
        Y div 400 - 32045.

unix_epoch = date_time(1970, 1, 1, 0, 0, 0, 0).

same_date(A, B) :-
    A = date_time(Year, Month, Day, _, _, _, _),
    B = date_time(Year, Month, Day, _, _, _, _).

%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%

years(Dur) = Dur ^ dur_months // 12.
months(Dur) = Dur ^ dur_months rem 12.
days(Dur) = Dur ^ dur_days.
hours(Dur) = Dur ^ dur_seconds // 3600.
minutes(Dur) = (Dur ^ dur_seconds rem 3600) // 60.
seconds(Dur) = Dur ^ dur_seconds rem 60.
microseconds(Dur) = Dur ^ dur_microseconds.

init_duration(Years, Months, Days, Hours, Minutes, Seconds, MicroSeconds)
        = Duration :-
    ( if
        (
            Years >= 0,
            Months >= 0,
            Days >= 0,
            Hours >= 0,
            Minutes >= 0,
            Seconds >= 0,
            MicroSeconds >= 0
        ;
            Years =< 0,
            Months =< 0,
            Days =< 0,
            Hours =< 0,
            Minutes =< 0,
            Seconds =< 0,
            MicroSeconds =< 0
        )
    then
        % Internally, the inputs are normalised into four components:
        % months, days, seconds and microseconds.

        % Normalise months: convert years into months, and add that to
        % the count of months.
        NormMonths = Years * 12 + Months,

        % Normalise microseconds: carry any full seconds up, and keep the
        % remaining microseconds.
        SecondsToCarry = MicroSeconds // microseconds_per_second,
        NormMicroSeconds = MicroSeconds rem microseconds_per_second,
        TotalSeconds = Hours * 3600 + Minutes * 60 + Seconds + SecondsToCarry,

        % Normalise seconds: carry any full days up, and keep the remaining
        % seconds.
        DaysToCarry = TotalSeconds // seconds_per_day,
        NormSeconds = TotalSeconds rem seconds_per_day,
        NormDays = Days + DaysToCarry,

        Duration = duration(NormMonths, NormDays, NormSeconds,
            NormMicroSeconds)
    else
        unexpected($pred, "some components negative and some positive")
    ).

:- func seconds_per_day = int.

seconds_per_day = 86_400.

:- func microseconds_per_second = int.

microseconds_per_second = 1_000_000.

unpack_duration(Duration,
    years(Duration), months(Duration), days(Duration), hours(Duration),
    minutes(Duration), seconds(Duration), microseconds(Duration)).

zero_duration = duration(0, 0, 0, 0).

negate(duration(Months, Days, Seconds, MicroSeconds)) =
    duration(-Months, -Days, -Seconds, -MicroSeconds).

%---------------------------------------------------------------------------%

duration_from_string(Str, Duration) :-
    some [!Chars] (
        !:Chars = string.to_char_list(Str),
        read_sign(Sign, !Chars),
        read_next_char('P', !Chars),
        !.Chars \= [],
        read_years(Years, !Chars),
        read_months(Months, !Chars),
        read_days(Days, !Chars),
        ( if read_next_char('T', !.Chars, TimePart) then
            TimePart = [_ | _],
            read_hours(Hours, TimePart, !:Chars),
            read_minutes(Minutes, !Chars),
            read_seconds_and_microseconds(Seconds, MicroSeconds, !Chars),
            !.Chars = [],
            Duration = init_duration(Sign * Years, Sign * Months,
                Sign * Days, Sign * Hours, Sign * Minutes, Sign * Seconds,
                Sign * MicroSeconds)
        else
            !.Chars = [],
            Duration = init_duration(Sign * Years, Sign * Months, Sign * Days,
                0, 0, 0, 0)
        )
    ).

det_duration_from_string(Str) = Duration :-
    ( if duration_from_string(Str, Duration0) then
        Duration = Duration0
    else
        unexpected($pred, "invalid duration: " ++ Str)
    ).

%---------------------------------------------------------------------------%

duration_to_string(duration(Months, Days, Seconds, MicroSeconds) @ Duration)
        = Str :-
    ( if
        Months = 0,
        Days = 0,
        Seconds = 0,
        MicroSeconds = 0
    then
        % At least one component must appear in the string.
        % The choice of days is arbitrary.
        Str = "P0D"
    else
        ( if
            Months >= 0,
            Days >= 0,
            Seconds >= 0,
            MicroSeconds >= 0
        then
            Sign = 1,
            SignStr = ""
        else if
            Months =< 0,
            Days =< 0,
            Seconds =< 0,
            MicroSeconds =< 0
        then
            Sign = -1,
            SignStr = "-"
        else
            unexpected($pred, "duration components have mixed signs")
        ),
        ( if
            Seconds = 0,
            MicroSeconds = 0
        then
            TimePart = []
        else
            TimePart = ["T",
                string_if_nonzero(Sign * hours(Duration), "H"),
                string_if_nonzero(Sign * minutes(Duration), "M"),
                seconds_duration_string(Sign * seconds(Duration),
                    Sign * microseconds(Duration))
            ]
        ),
        Str = string.append_list([
            SignStr, "P",
            string_if_nonzero(Sign * years(Duration), "Y"),
            string_if_nonzero(Sign * months(Duration), "M"),
            string_if_nonzero(Sign * days(Duration), "D")] ++ TimePart)
    ).

:- func string_if_nonzero(int, string) = string.

string_if_nonzero(X, Suffix) =
    ( if X = 0 then
        ""
    else
        int_to_string(X) ++ Suffix
    ).

:- func seconds_duration_string(seconds, microseconds) = string.

seconds_duration_string(Seconds, MicroSeconds) = Str :-
    ( if Seconds = 0, MicroSeconds = 0 then
        Str = ""
    else
        Str = string.from_int(Seconds) ++
            microsecond_string(MicroSeconds) ++ "S"
    ).

:- func microsecond_string(microseconds) = string.

microsecond_string(MicroSeconds) = Str :-
    ( if MicroSeconds > 0 then
        Str = rstrip_pred(unify('0'),
            string.format(".%06d", [i(MicroSeconds)]))
    else
        Str = ""
    ).

%---------------------------------------------------------------------------%
%
% Adding durations to date times.
%
% The following is a fairly direct translation of the algorithm at
% https://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes.
%

add_duration(D, S, !:E) :-
    some [!Temp, !Carry] (
        % Months
        !:Temp = S ^ dt_month + D ^ dur_months,
        EMonth = modulo(!.Temp, 1, 13),
        !:Carry = fquotient(!.Temp, 1, 13),
        % Years
        EYear = S ^ dt_year + !.Carry,
        % Microseconds
        !:Temp = S ^ dt_microsecond + D ^ dur_microseconds,
        EMicrosecond = modulo(!.Temp, microseconds_per_second),
        !:Carry = div(!.Temp, microseconds_per_second),
        % Seconds
        !:Temp = S ^ dt_second + D ^ dur_seconds + !.Carry,
        ESecond = modulo(!.Temp, 60),
        !:Carry = div(!.Temp, 60),
        % Minutes
        !:Temp = S ^ dt_minute + !.Carry,
        EMinute = int.mod(!.Temp, 60),
        !:Carry = int.div(!.Temp, 60),
        % Hours
        !:Temp = S ^ dt_hour + !.Carry,
        EHour = int.mod(!.Temp, 24),
        !:Carry = int.div(!.Temp, 24),
        % Days
        MaxDaysInMonth = max_day_in_month_for(EYear, EMonth),
        ( if S ^ dt_day > MaxDaysInMonth then
            TempDays = MaxDaysInMonth
        else if S ^ dt_day < 1 then
            TempDays = 1
        else
            TempDays = S ^ dt_day
        ),
        EDay = TempDays + D ^ dur_days + !.Carry,
        !:E = date_time(EYear, EMonth, EDay, EHour, EMinute, ESecond,
            EMicrosecond),
        add_duration_loop(D, S, !E)
    ).

:- pred add_duration_loop(duration::in, date_time::in, date_time::in,
    date_time::out) is det.

add_duration_loop(D, S, !E) :-
    ( if !.E ^ dt_day < 1 then
        !E ^ dt_day := !.E ^ dt_day +
            max_day_in_month_for(!.E ^ dt_year, !.E ^ dt_month - 1),
        Carry = -1,
        Temp = !.E ^ dt_month + Carry,
        !E ^ dt_month := modulo(Temp, 1, 13),
        !E ^ dt_year := !.E ^ dt_year + fquotient(Temp, 1, 13),
        disable_warning [suspicious_recursion] (
            add_duration_loop(D, S, !E)
        )
    else if
        MaxDaysInMonth = max_day_in_month_for(!.E ^ dt_year, !.E ^ dt_month),
        !.E ^ dt_day > MaxDaysInMonth
    then
        !E ^ dt_day := !.E ^ dt_day - MaxDaysInMonth,
        Carry = 1,
        Temp = !.E ^ dt_month + Carry,
        !E ^ dt_month := modulo(Temp, 1, 13),
        !E ^ dt_year := !.E ^ dt_year + fquotient(Temp, 1, 13),
        disable_warning [suspicious_recursion] (
            add_duration_loop(D, S, !E)
        )
    else
        true
    ).

:- func fquotient(int, int, int) = int.

fquotient(A, Low, High) = int.div(A - Low, High - Low).

:- func modulo(int, int) = int.

modulo(A, B) = A - div(A, B) * B.

:- func modulo(int, int, int) = int.

modulo(A, Low, High) = modulo(A - Low, High - Low) + Low.

:- func max_day_in_month_for(int, int) = int.

max_day_in_month_for(YearValue, MonthValue) = Max :-
    M = int.mod(MonthValue - 1, 12) + 1,
    Y = YearValue + int.div(MonthValue - 1, 12),
    ( if
        (
            ( M = 1 ; M = 3 ; M = 5 ; M = 7 ; M = 8 ; M = 10 ; M = 12 ),
            Max0 = 31
        ;
            ( M = 4 ; M = 6 ; M = 9 ; M = 11 ),
            Max0 = 30
        ;
            M = 2,
            ( if is_leap_year(Y) then
                Max0 = 29
            else
                Max0 = 28
            )
        )
    then
        Max = Max0
    else
        % This should never happen.
        unexpected($pred, "unexpected value for M: " ++ string(M))
    ).

%---------------------------------------------------------------------------%

duration_leq(DurationA, DurationB) :-
    % Partial relation on durations. This algorithm is described at
    % https://www.w3.org/TR/xmlschema-2/#duration.
    list.all_true(
        ( pred(TestDateTime::in) is semidet :-
            add_duration(DurationA, TestDateTime, DateTimeA),
            add_duration(DurationB, TestDateTime, DateTimeB),
            compare(CompRes, DateTimeA, DateTimeB),
            ( CompRes = (<) ; CompRes = (=) )
        ), test_dates).

    % Returns date_times used to compare durations.
    %
:- func test_dates = list(date_time).

test_dates = [
    date_time(1696, 9, 1, 0, 0, 0, 0),
    date_time(1697, 2, 1, 0, 0, 0, 0),
    date_time(1903, 3, 1, 0, 0, 0, 0),
    date_time(1903, 7, 1, 0, 0, 0, 0)
].

local_time_offset(Offset, !IO) :-
    time.time(TimeT, !IO),
    time.localtime(TimeT, LocalTM, !IO),
    GMTM = time.gmtime(TimeT),
    LocalTime = tm_to_date(LocalTM),
    GMTime = tm_to_date(GMTM),
    Offset = duration_between(GMTime, LocalTime).

%---------------------------------------------------------------------------%
%
% Computing the duration between two dates.
%

duration_between(DateTimeA, DateTimeB) = Duration :-
    compare(CompResult, DateTimeA, DateTimeB),
    (
        CompResult = (<),
        % DateTimeA is the earlier date, so the duration between is
        % non-negative.
        Duration = greedy_difference_from_earlier(DateTimeA, DateTimeB)
    ;
        CompResult = (=),
        Duration = zero_duration
    ;
        CompResult = (>),
        % DateTimeA is the later date, so the duration between is non-positive.
        % Compute the magnitude of the duration anchored at the later date
        % and then negate it.
        Magnitude = greedy_difference_from_later(DateTimeB, DateTimeA),
        Duration = negate(Magnitude)
    ).

duration(DateTimeA, DateTimeB) = duration_between(DateTimeA, DateTimeB).

    % greedy_difference_from_earlier(Earlier, Later) = Duration:
    % greedy_difference_from_later(Earlier, Later) = Duration:
    %
    % Both functions compute the non-negative difference between Earlier and
    % Later and have the precondition that Earlier < Later. They differ only in
    % how the whole months in that difference are counted, which can give
    % different durations because months vary in length:
    %
    % - greedy_difference_from_earlier counts the largest whole number of
    %   months for which Earlier + that many months =< Later, then measures the
    %   remaining, less-than-a-month difference forwards to Later. The result D
    %   satisfies add_duration(D, Earlier) = Later.
    %
    % - greedy_difference_from_later counts the largest whole number of months
    %   for which Later - that many months >= Earlier, then measures the
    %   remaining difference backwards to Earlier. The result D satisfies
    %   add_duration(negate(D), Later) = Earlier.
    %
    % In each function the month count is found by a trial addition of the
    % year-and-month difference, decremented by one if that oversteps because
    % the days and time of day have not yet been accounted for. The remaining
    % difference is found using do_fixed_duration_between/2.
    % Both the month count and the remainder will be non-negative.
    % This satisfies the requirement that the components of a duration do not
    % have mixed signs.
    %
:- func greedy_difference_from_earlier(date_time, date_time) = duration.

greedy_difference_from_earlier(Earlier, Later) = Duration :-
    MonthsUpperBound = 12 * (Later ^ dt_year - Earlier ^ dt_year)
        + (Later ^ dt_month - Earlier ^ dt_month),
    add_duration(init_duration(0, MonthsUpperBound, 0, 0, 0, 0, 0),
        Earlier, Candidate),
    ( if compare((>), Candidate, Later) then
        Months = MonthsUpperBound - 1,
        add_duration(init_duration(0, Months, 0, 0, 0, 0, 0), Earlier,
            Landing)
    else
        Months = MonthsUpperBound,
        Landing = Candidate
    ),
    % Landing =< Later, and they differ by less than a month, so the remainder
    % has a zero months component.
    Remainder = do_fixed_duration_between(Landing, Later),
    Remainder = duration(_, RemDays, RemSeconds, RemMicroSeconds),
    Duration = duration(Months, RemDays, RemSeconds, RemMicroSeconds).

:- func greedy_difference_from_later(date_time, date_time) = duration.

greedy_difference_from_later(Earlier, Later) = Duration :-
    MonthsUpperBound = 12 * (Later ^ dt_year - Earlier ^ dt_year)
        + (Later ^ dt_month - Earlier ^ dt_month),
    add_duration(init_duration(0, -MonthsUpperBound, 0, 0, 0, 0, 0),
        Later, Candidate),
    ( if compare((<), Candidate, Earlier) then
        Months = MonthsUpperBound - 1,
        add_duration(init_duration(0, -Months, 0, 0, 0, 0, 0), Later, Landing)
    else
        Months = MonthsUpperBound,
        Landing = Candidate
    ),
    % Landing >= Earlier, differing by less than a month.
    Remainder = do_fixed_duration_between(Earlier, Landing),
    Remainder = duration(_, RemDays, RemSeconds, RemMicroSeconds),
    Duration = duration(Months, RemDays, RemSeconds, RemMicroSeconds).

    % subtract_ints_with_borrow(BorrowAmount, Val1, Val2, Val, Borrow):
    %
    % Subtract Val2 from Val1, possibly borrowing BorrowAmount if Val1 < Val2.
    % If an amount is borrowed, then set Borrow to 1; otherwise, set it to 0.
    %
:- pred subtract_ints_with_borrow(int::in, int::in, int::in, int::out,
    int::out) is det.

subtract_ints_with_borrow(BorrowVal, Val1, Val2, Diff, Borrow) :-
    ( if Val1 >= Val2 then
        Borrow = 0,
        Diff = Val1 - Val2
    else
        Borrow = 1,
        Diff = BorrowVal + Val1 - Val2
    ).

fixed_duration_between(DateTimeA, DateTimeB) = Duration :-
    compare(CompResult, DateTimeA, DateTimeB),
    (
        CompResult = (<),
        Duration = do_fixed_duration_between(DateTimeA, DateTimeB)
    ;
        CompResult = (=),
        Duration = zero_duration
    ;
        CompResult = (>),
        Duration0 = do_fixed_duration_between(DateTimeB, DateTimeA),
        Duration = negate(Duration0)
    ).

day_duration(DateTimeA, DateTimeB) =
    fixed_duration_between(DateTimeA, DateTimeB).

    % do_fixed_duration_between(Earlier, Later) = Duration:
    %
    % Return the non-negative difference between Earlier and Later expressed in
    % fixed-length units only (days and smaller). It is a precondition that
    % Earlier =< Later.
    %
:- func do_fixed_duration_between(date_time, date_time) = duration.

do_fixed_duration_between(Earlier, Later) = Duration :-
    LaterMicroSeconds = Later ^ dt_microsecond,
    EarlierMicroSeconds = Earlier ^ dt_microsecond,
    subtract_ints_with_borrow(microseconds_per_second,
        LaterMicroSeconds, EarlierMicroSeconds,
        MicroSeconds, BorrowFromSeconds),

    LaterSecondsLessBorrow = Later ^ dt_second - BorrowFromSeconds,
    EarlierSeconds = Earlier ^ dt_second,
    subtract_ints_with_borrow(60,
        LaterSecondsLessBorrow, EarlierSeconds, Seconds, BorrowFromMinutes),

    LaterMinutesLessBorrow = Later ^ dt_minute - BorrowFromMinutes,
    EarlierMinutes = Earlier ^ dt_minute,
    subtract_ints_with_borrow(60,
        LaterMinutesLessBorrow, EarlierMinutes, Minutes, BorrowFromHours),

    LaterHoursLessBorrow = Later ^ dt_hour - BorrowFromHours,
    EarlierHours = Earlier ^ dt_hour,
    subtract_ints_with_borrow(24,
        LaterHoursLessBorrow, EarlierHours, Hours, BorrowFromDays),

    LaterDay = julian_day_number(Later),
    EarlierDay = julian_day_number(Earlier),
    Days = LaterDay - BorrowFromDays - EarlierDay,

    Duration = init_duration(0, 0, Days, Hours, Minutes, Seconds,
        MicroSeconds).

%---------------------------------------------------------------------------%

foldl_days(Pred, !.Curr, End, !Acc) :-
    compare(Res, !.Curr, End),
    (
        ( Res = (<)
        ; Res = (=)
        ),
        Pred(!.Curr, !Acc),
        add_duration(init_duration(0, 0, 1, 0, 0, 0, 0), !Curr),
        disable_warning [suspicious_recursion] (
            foldl_days(Pred, !.Curr, End, !Acc)
        )
    ;
        Res = (>)
    ).

foldl2_days(Pred, !.Curr, End, !Acc1, !Acc2) :-
    compare(Res, !.Curr, End),
    (
        ( Res = (<)
        ; Res = (=)
        ),
        Pred(!.Curr, !Acc1, !Acc2),
        add_duration(init_duration(0, 0, 1, 0, 0, 0, 0), !Curr),
        disable_warning [suspicious_recursion] (
            foldl2_days(Pred, !.Curr, End, !Acc1, !Acc2)
        )
    ;
        Res = (>)
    ).

foldl3_days(Pred, !.Curr, End, !Acc1, !Acc2, !Acc3) :-
    compare(Res, !.Curr, End),
    (
        ( Res = (<)
        ; Res = (=)
        ),
        Pred(!.Curr, !Acc1, !Acc2, !Acc3),
        add_duration(init_duration(0, 0, 1, 0, 0, 0, 0), !Curr),
        disable_warning [suspicious_recursion] (
            foldl3_days(Pred, !.Curr, End, !Acc1, !Acc2, !Acc3)
        )
    ;
        Res = (>)
    ).

%---------------------------------------------------------------------------%
%
% Parsing predicates.
%

:- pred read_next_char(char::out, list(char)::in, list(char)::out) is semidet.

read_next_char(Char, [Char | Rest], Rest).

    % read_int_and_return_num_digits(Int, NumDigits, !Chars):
    %
    % Int is a non-negative integer of NumDigits digits at the start
    % of the given character list.
    % Int and NumDigits are both zero if the start of the character list does
    % not contain a digit.
    % Fails if the integer is outside the bounds of what can be represented by
    % Mercury's int type.
    %
:- pred read_int_and_return_num_digits(int::out, int::out,
    list(char)::in, list(char)::out) is semidet.

read_int_and_return_num_digits(Int, NumDigits, !Chars) :-
    % See the comment for string.do_base_string_to_positive_int_loop/8
    % for an explanation of how we check for overflow here.
    CutOff = max_int `unchecked_quotient` 10,
    CutLimit = max_int `unchecked_rem` 10,
    read_int_and_return_num_digits_loop(CutOff, CutLimit, 0, Int,
        0, NumDigits, !Chars).

:- pred read_int_and_return_num_digits_loop(int::in, int::in,
    int::in, int::out, int::in, int::out, list(char)::in, list(char)::out)
    is semidet.

read_int_and_return_num_digits_loop(CutOff, CutLimit, Acc0, Acc,
        !NumDigits, !Chars) :-
    ( if
        !.Chars = [Char | !:Chars],
        decimal_digit_to_int(Char, Digit)
    then
        % Fail on overflow.
        ( Acc0 < CutOff
        ; Acc0 = CutOff, Digit =< CutLimit
        ),
        NextAcc = (10 * Acc0) + Digit,
        read_int_and_return_num_digits_loop(CutOff, CutLimit, NextAcc, Acc,
            !.NumDigits + 1, !:NumDigits, !Chars)
    else
        Acc = Acc0
    ).

:- pred read_microseconds(microseconds::out, list(char)::in, list(char)::out)
    is det.

read_microseconds(MicroSeconds, !Chars) :-
    ( if
        read_next_char((.), !Chars),
        read_int_and_return_num_digits(Fraction, FractionDigits, !Chars),
        FractionDigits > 0,
        FractionDigits < 7
    then
        MicroSeconds = int.pow(10, 6 - FractionDigits) * Fraction
    else
        MicroSeconds = 0
    ).

%---------------------------------------------------------------------------%
%
% Duration parsing predicates.
%

    % Read an optional leading minus sign. Set sign to -1 if one is present
    % (and is consumed), and 1 otherwise (consuming nothing).
    %
:- pred read_sign(int::out, list(char)::in, list(char)::out) is det.

read_sign(Sign, !Chars) :-
    ( if !.Chars = [(-) | !:Chars] then
        Sign = -1
    else
        Sign = 1
    ).

    % read_years, read_months, read_days, read_hours and read_minutes each read
    % one optional duration component using
    % read_int_and_given_char_or_return_zero, with the designator character
    % 'Y', 'M', 'D', 'H' and 'M' respectively.
    % Months and minutes share the designator 'M', and are distinguished only
    % by position: months are read before the 'T' separator and minutes after
    % it.
    %
:- pred read_years(int::out, list(char)::in, list(char)::out) is semidet.

read_years(Years, !Chars) :-
    read_int_and_given_char_or_return_zero('Y', Years, !Chars).

:- pred read_months(int::out, list(char)::in, list(char)::out) is semidet.

read_months(Months, !Chars) :-
    read_int_and_given_char_or_return_zero('M', Months, !Chars).

:- pred read_days(int::out, list(char)::in, list(char)::out) is semidet.

read_days(Days, !Chars) :-
    read_int_and_given_char_or_return_zero('D', Days, !Chars).

:- pred read_hours(int::out, list(char)::in, list(char)::out) is semidet.

read_hours(Hours, !Chars) :-
    read_int_and_given_char_or_return_zero('H', Hours, !Chars).

:- pred read_minutes(int::out, list(char)::in, list(char)::out) is semidet.

read_minutes(Minutes, !Chars) :-
    read_int_and_given_char_or_return_zero('M', Minutes, !Chars).

    % read_int_and_given_char_or_return_zero(Char, Int, !Chars):
    %
    % Read one optional "<integer><Char>" duration component from the front of
    % !.Chars, where <integer> is a run of decimal digits.
    % If !.Chars begins with Char preceded by at least one digit, then set Int
    % to the value of those digits and !:Chars to the characters after Char.
    % If !.Chars begins with Char with no digits before it, then fail.
    % Otherwise, set Int to zero and leave !:Chars unchanged.
    %
:- pred read_int_and_given_char_or_return_zero(char::in, int::out,
    list(char)::in, list(char)::out) is semidet.

read_int_and_given_char_or_return_zero(Char, Int, !Chars) :-
    ( if
        read_int_and_return_num_digits(Int0, NumDigits, !Chars),
        !.Chars = [Char | !:Chars]
    then
        NumDigits > 0,
        Int = Int0
    else
        Int = 0
    ).

    % read_seconds_and_microseconds(Seconds, MicroSeconds, !Chars):
    %
    % Read the optional seconds component from the front of !.Chars: a run of
    % at least one decimal digit, an optional fractional part, and the
    % designator 'S' (for example "9S" or "9.0003S").
    % If !.Chars begins with such a component, then set Seconds to the whole
    % part, MicroSeconds to the fractional part expressed in microseconds, and
    % !:Chars to the characters after the 'S'.
    % Otherwise, set both Seconds and MicroSeconds to zero and leave !:Chars
    % unchanged.
    %
:- pred read_seconds_and_microseconds(seconds::out, microseconds::out,
    list(char)::in, list(char)::out) is det.

read_seconds_and_microseconds(Seconds, MicroSeconds, !Chars) :-
    ( if
        read_int_and_return_num_digits(Seconds0, NumDigits, !Chars),
        NumDigits > 0,
        read_microseconds(MicroSeconds0, !Chars),
        read_next_char('S', !Chars)
    then
        Seconds = Seconds0,
        MicroSeconds = MicroSeconds0
    else
        Seconds = 0,
        MicroSeconds = 0
    ).

%---------------------------------------------------------------------------%
:- end_module calendar.
%---------------------------------------------------------------------------%
