mirror of
https://github.com/Mercury-Language/mercury.git
synced 2026-04-15 01:13:30 +00:00
1570 lines
53 KiB
Mathematica
1570 lines
53 KiB
Mathematica
%---------------------------------------------------------------------------%
|
|
% 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.
|
|
%
|
|
:- 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.
|
|
|
|
%---------------------------------------------------------------------------%
|
|
%---------------------------------------------------------------------------%
|
|
|
|
:- 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(Date) = Date ^ dt_year.
|
|
month(Date) = det_int_to_month(Date ^ dt_month).
|
|
day_of_month(Date) = Date ^ dt_day.
|
|
day_of_week(Date) = compute_day_of_week(Date).
|
|
hour(Date) = Date ^ dt_hour.
|
|
minute(Date) = Date ^ dt_minute.
|
|
second(Date) = Date ^ dt_second.
|
|
microsecond(Date) = Date ^ dt_microsecond.
|
|
|
|
:- func compute_day_of_week(date) = day_of_week.
|
|
|
|
compute_day_of_week(Date) = DayOfWeek :-
|
|
% We compute the day of the week by working out the Julian day modulo 7.
|
|
JDN = julian_day_number(Date),
|
|
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 < 62,
|
|
MicroSecond >= 0,
|
|
MicroSecond < 1000000,
|
|
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, Date) :-
|
|
some [!Chars] (
|
|
!:Chars = string.to_char_list(Str),
|
|
( if read_char((-), !.Chars, Rest1) then
|
|
!:Chars = Rest1,
|
|
read_int_and_num_chars(Year0, YearChars, !Chars),
|
|
Year = -Year0
|
|
else
|
|
read_int_and_num_chars(Year, YearChars, !Chars)
|
|
),
|
|
YearChars >= 4,
|
|
read_char((-), !Chars),
|
|
read_int_and_num_chars(Month, 2, !Chars),
|
|
Month >= 1,
|
|
Month =< 12,
|
|
read_char((-), !Chars),
|
|
read_int_and_num_chars(Day, 2, !Chars),
|
|
Day >= 1,
|
|
Day =< max_day_in_month_for(Year, Month),
|
|
read_char(' ', !Chars),
|
|
read_int_and_num_chars(Hour, 2, !Chars),
|
|
Hour >= 0,
|
|
Hour =< 23,
|
|
read_char((:), !Chars),
|
|
read_int_and_num_chars(Minute, 2, !Chars),
|
|
Minute >= 0,
|
|
Minute =< 59,
|
|
read_char((:), !Chars),
|
|
read_int_and_num_chars(Second, 2, !Chars),
|
|
Second < 62,
|
|
read_microseconds(MicroSecond, !Chars),
|
|
!.Chars = [],
|
|
Date = 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.
|
|
|
|
tm_to_date(TM) = Date :-
|
|
TM = tm(TMYear, TMMonth, TMDay, TMHour, TMMinute, TMSecond, _, _, _),
|
|
Year = 1900 + TMYear,
|
|
Month = TMMonth + 1,
|
|
Day = TMDay,
|
|
Hour = TMHour,
|
|
Minute = TMMinute,
|
|
Second = TMSecond,
|
|
Date = 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 = 86400.
|
|
|
|
:- func microseconds_per_second = int.
|
|
|
|
microseconds_per_second = 1000000.
|
|
|
|
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_char('P', !Chars),
|
|
read_years(Years, !Chars),
|
|
read_months(Months, !Chars),
|
|
read_days(Days, !Chars),
|
|
( if read_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::in, date::in, date::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(DurA, DurB) :-
|
|
% Partial relation on durations. This algorithm is described at
|
|
% https://www.w3.org/TR/xmlschema-2/#duration.
|
|
list.all_true(
|
|
( pred(TestDate::in) is semidet :-
|
|
add_duration(DurA, TestDate, DateA),
|
|
add_duration(DurB, TestDate, DateB),
|
|
compare(CompRes, DateA, DateB),
|
|
( CompRes = (<) ; CompRes = (=) )
|
|
), test_dates).
|
|
|
|
% Returns dates used to compare durations.
|
|
%
|
|
:- func test_dates = list(date).
|
|
|
|
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(TZ, !IO) :-
|
|
time.time(TimeT, !IO),
|
|
time.localtime(TimeT, LocalTM, !IO),
|
|
GMTM = time.gmtime(TimeT),
|
|
LocalTime = tm_to_date(LocalTM),
|
|
GMTime = tm_to_date(GMTM),
|
|
TZ = duration(GMTime, LocalTime).
|
|
|
|
%---------------------------------------------------------------------------%
|
|
%
|
|
% Computing the duration between two dates.
|
|
%
|
|
|
|
duration(DateA, DateB) = Duration :-
|
|
compare(CompResult, DateB, DateA),
|
|
(
|
|
CompResult = (<),
|
|
greedy_subtract_descending(ascending, DateA, DateB, Duration0),
|
|
Duration = negate(Duration0)
|
|
;
|
|
CompResult = (=),
|
|
Duration = zero_duration
|
|
;
|
|
CompResult = (>),
|
|
greedy_subtract_descending(descending, DateB, DateA, Duration)
|
|
).
|
|
|
|
:- type order
|
|
---> ascending
|
|
; descending.
|
|
|
|
% This predicate has the precondition that DateA < DateB. OriginalOrder is
|
|
% the original order of the date arguments (descending means that in the
|
|
% original call DateA < DateB, while ascending means that in the original
|
|
% call DateA > DateB). This is needed to correctly compute the days
|
|
% component of the resulting duration. The calculation is different
|
|
% depending on the original order, because we want the invariant:
|
|
% add_duration(duration(DateA, DateB), DateA, DateB)
|
|
% to hold, and in the case where DateA > DateB, Duration will be negative.
|
|
%
|
|
:- pred greedy_subtract_descending(order::in, date::in, date::in,
|
|
duration::out) is det.
|
|
|
|
greedy_subtract_descending(OriginalOrder, DateA, DateB, Duration) :-
|
|
some [!Borrow] (
|
|
MicroSecondA = DateA ^ dt_microsecond,
|
|
MicroSecondB = DateB ^ dt_microsecond,
|
|
subtract_ints_with_borrow(microseconds_per_second, MicroSecondA,
|
|
MicroSecondB, MicroSeconds, !:Borrow),
|
|
SecondA = DateA ^ dt_second - !.Borrow,
|
|
SecondB = DateB ^ dt_second,
|
|
subtract_ints_with_borrow(60, SecondA, SecondB, Seconds, !:Borrow),
|
|
MinuteA = DateA ^ dt_minute - !.Borrow,
|
|
MinuteB = DateB ^ dt_minute,
|
|
subtract_ints_with_borrow(60, MinuteA, MinuteB, Minutes, !:Borrow),
|
|
HourA = DateA ^ dt_hour - !.Borrow,
|
|
HourB = DateB ^ dt_hour,
|
|
subtract_ints_with_borrow(24, HourA, HourB, Hours, !:Borrow),
|
|
(
|
|
OriginalOrder = descending,
|
|
add_duration(duration(0, -1, 0, 0), DateA, DateAMinus1Month),
|
|
DaysToBorrow = max_day_in_month_for(DateAMinus1Month ^ dt_year,
|
|
DateAMinus1Month ^ dt_month),
|
|
DateAEndOfMonth = max_day_in_month_for(DateA ^ dt_year,
|
|
DateA ^ dt_month),
|
|
DayA = DateA ^ dt_day - !.Borrow,
|
|
DayB = int.min(DateB ^ dt_day, DateAEndOfMonth)
|
|
;
|
|
OriginalOrder = ascending,
|
|
DaysToBorrow = max_day_in_month_for(DateB ^ dt_year,
|
|
DateB ^ dt_month),
|
|
DateBEndOfMonth = max_day_in_month_for(DateB ^ dt_year,
|
|
DateB ^ dt_month),
|
|
DayA = int.min(DateA ^ dt_day - !.Borrow, DateBEndOfMonth),
|
|
DayB = DateB ^ dt_day
|
|
),
|
|
subtract_ints_with_borrow(DaysToBorrow, DayA, DayB, Days, !:Borrow),
|
|
MonthA = DateA ^ dt_month - !.Borrow,
|
|
MonthB = DateB ^ dt_month,
|
|
subtract_ints_with_borrow(12, MonthA, MonthB, Months, !:Borrow),
|
|
YearA = DateA ^ dt_year - !.Borrow,
|
|
YearB = DateB ^ dt_year,
|
|
( if YearA >= YearB then
|
|
Years = YearA - YearB
|
|
else
|
|
% If this happens, then DateA < DateB, which violates
|
|
% a precondition of this predicate.
|
|
unexpected($pred, "left over years")
|
|
),
|
|
Duration = init_duration(Years, Months, Days, Hours, Minutes, Seconds,
|
|
MicroSeconds)
|
|
).
|
|
|
|
% 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
|
|
).
|
|
|
|
day_duration(DateA, DateB) = Duration :-
|
|
builtin.compare(CompResult, DateB, DateA),
|
|
(
|
|
CompResult = (<),
|
|
Duration0 = do_day_duration(DateB, DateA),
|
|
Duration = negate(Duration0)
|
|
;
|
|
CompResult = (=),
|
|
Duration = zero_duration
|
|
;
|
|
CompResult = (>),
|
|
Duration = do_day_duration(DateA, DateB)
|
|
).
|
|
|
|
:- func do_day_duration(date, date) = duration.
|
|
|
|
do_day_duration(DateA, DateB) = Duration :-
|
|
some [!Borrow] (
|
|
MicroSecond1 = DateB ^ dt_microsecond,
|
|
MicroSecond2 = DateA ^ dt_microsecond,
|
|
subtract_ints_with_borrow(microseconds_per_second,
|
|
MicroSecond1, MicroSecond2, MicroSeconds, !:Borrow),
|
|
Second1 = DateB ^ dt_second - !.Borrow,
|
|
Second2 = DateA ^ dt_second,
|
|
subtract_ints_with_borrow(60, Second1, Second2, Seconds, !:Borrow),
|
|
Minute1 = DateB ^ dt_minute - !.Borrow,
|
|
Minute2 = DateA ^ dt_minute,
|
|
subtract_ints_with_borrow(60, Minute1, Minute2, Minutes, !:Borrow),
|
|
Hour1 = DateB ^ dt_hour - !.Borrow,
|
|
Hour2 = DateA ^ dt_hour,
|
|
subtract_ints_with_borrow(24, Hour1, Hour2, Hours, !:Borrow),
|
|
JDN1 = julian_day_number(DateB),
|
|
JDN2 = julian_day_number(DateA),
|
|
Days = JDN1 - !.Borrow - JDN2,
|
|
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_microseconds(microseconds::out, list(char)::in, list(char)::out)
|
|
is det.
|
|
|
|
read_microseconds(MicroSeconds, !Chars) :-
|
|
( if
|
|
read_char((.), !.Chars, Chars1),
|
|
read_int_and_num_chars(Fraction, FractionDigits, Chars1, !:Chars),
|
|
FractionDigits > 0,
|
|
FractionDigits < 7
|
|
then
|
|
MicroSeconds = int.pow(10, 6 - FractionDigits) * Fraction
|
|
else
|
|
MicroSeconds = 0
|
|
).
|
|
|
|
:- pred read_int_and_num_chars(int::out, int::out,
|
|
list(char)::in, list(char)::out) is det.
|
|
|
|
read_int_and_num_chars(Val, N, !Chars) :-
|
|
read_int_and_num_chars_2(0, Val, 0, N, !Chars).
|
|
|
|
:- pred read_int_and_num_chars_2(int::in, int::out, int::in, int::out,
|
|
list(char)::in, list(char)::out) is det.
|
|
|
|
read_int_and_num_chars_2(!Val, !N, !Chars) :-
|
|
( if
|
|
!.Chars = [Char | Rest],
|
|
decimal_digit_to_int(Char, Digit)
|
|
then
|
|
!:Val = !.Val * 10 + Digit,
|
|
read_int_and_num_chars_2(!Val, !.N + 1, !:N, Rest, !:Chars)
|
|
else
|
|
true
|
|
).
|
|
|
|
:- pred read_sign(int::out, list(char)::in, list(char)::out) is det.
|
|
|
|
read_sign(Sign, !Chars) :-
|
|
( if !.Chars = [(-) | Rest] then
|
|
!:Chars = Rest,
|
|
Sign = -1
|
|
else
|
|
Sign = 1
|
|
).
|
|
|
|
:- pred read_char(char::out, list(char)::in, list(char)::out) is semidet.
|
|
|
|
read_char(Char, [Char | Rest], Rest).
|
|
|
|
:- pred read_years(int::out, list(char)::in, list(char)::out) is det.
|
|
|
|
read_years(Years, !Chars) :-
|
|
read_int_and_char_or_zero(Years, 'Y', !Chars).
|
|
|
|
:- pred read_months(int::out, list(char)::in, list(char)::out) is det.
|
|
|
|
read_months(Months, !Chars) :-
|
|
read_int_and_char_or_zero(Months, 'M', !Chars).
|
|
|
|
:- pred read_days(int::out, list(char)::in, list(char)::out) is det.
|
|
|
|
read_days(Days, !Chars) :-
|
|
read_int_and_char_or_zero(Days, 'D', !Chars).
|
|
|
|
:- pred read_hours(int::out, list(char)::in, list(char)::out) is det.
|
|
|
|
read_hours(Hours, !Chars) :-
|
|
read_int_and_char_or_zero(Hours, 'H', !Chars).
|
|
|
|
:- pred read_minutes(int::out, list(char)::in, list(char)::out) is det.
|
|
|
|
read_minutes(Minutes, !Chars) :-
|
|
read_int_and_char_or_zero(Minutes, 'M', !Chars).
|
|
|
|
:- 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(Seconds0, !.Chars, Chars1),
|
|
read_microseconds(MicroSeconds0, Chars1, Chars2),
|
|
read_char('S', Chars2, Chars3)
|
|
then
|
|
!:Chars = Chars3,
|
|
Seconds = Seconds0,
|
|
MicroSeconds = MicroSeconds0
|
|
else
|
|
Seconds = 0,
|
|
MicroSeconds = 0
|
|
).
|
|
|
|
:- pred read_int_and_char_or_zero(int::out, char::in,
|
|
list(char)::in, list(char)::out) is det.
|
|
|
|
read_int_and_char_or_zero(Int, Char, !Chars) :-
|
|
( if
|
|
read_int(Int0, !.Chars, Chars1),
|
|
Chars1 = [Char | Rest]
|
|
then
|
|
!:Chars = Rest,
|
|
Int = Int0
|
|
else
|
|
Int = 0
|
|
).
|
|
|
|
:- pred read_int(int::out, list(char)::in, list(char)::out) is det.
|
|
|
|
read_int(Val, !Chars) :-
|
|
read_int_2(0, Val, !Chars).
|
|
|
|
:- pred read_int_2(int::in, int::out, list(char)::in, list(char)::out) is det.
|
|
|
|
read_int_2(!Val, !Chars) :-
|
|
( if
|
|
!.Chars = [Char | Rest],
|
|
decimal_digit_to_int(Char, Digit)
|
|
then
|
|
!:Val = !.Val * 10 + Digit,
|
|
read_int_2(!Val, Rest, !:Chars)
|
|
else
|
|
true
|
|
).
|
|
|
|
%---------------------------------------------------------------------------%
|
|
:- end_module calendar.
|
|
%---------------------------------------------------------------------------%
|