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