Allow for only a single positive leap second.

Mercury currently allows for two positive leap seconds per minute, presumably
following earlier versions of the C standard (e.g. C90). This is based on
an erroneous understanding of how UTC is defined and was corrected in C99.
(UTC allows a maximum of one leap second, positive or negative, per minute.)

library/calendar.m:
library/time.m:
    Allow only a single positive leap second per minute.

NEWS.md:
    Announce the above change.

tests/hard_coded/calendar_date_time.conv.{m,exp}:
    Update this test.
This commit is contained in:
Julien Fischer
2026-04-15 00:24:41 +10:00
parent f3e5419499
commit d62391b040
5 changed files with 23 additions and 18 deletions

View File

@@ -246,6 +246,9 @@ Changes to the Mercury standard library
- func `det_date_from_string/1` (replacement: `det_date_time_from_string/1`)
- func `date_to_string/1` (replacement: `date_time_to_string/1`)
* `date_time/0` and `duration/0` values now only allow for a single positive
leap second for any given minute.
* The following functions and predicates have been added:
- pred `init_date_time/8`
@@ -1424,6 +1427,11 @@ Changes to the Mercury standard library
- pred `spawn_native_joinable/5`
- pred `join_thread/4`
### Changes to the `time` module
* The `tm_sec` field of the `tm/0` type now only allows for a single positive
leap second for any given minute.
### Changes to the `tree_bitset` module
* The following predicates and functions have been added:

View File

@@ -55,7 +55,7 @@
:- 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 second == int. % 0 .. 60 (60 is for a positive leap second)
:- type microsecond == int. % 0 .. 999,999
:- type month
@@ -165,8 +165,8 @@
%
% - 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)
% - 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
%
@@ -313,9 +313,9 @@
% - 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
% 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,401 or 86,402 seconds long.
% 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
@@ -774,7 +774,7 @@ init_date_time(Year, Month, Day, Hour, Minute, Second, MicroSecond,
Minute >= 0,
Minute < 60,
Second >= 0,
Second < 62,
Second < 61,
MicroSecond >= 0,
MicroSecond < 1000000,
DateTime = date_time(Year, month_to_int(Month), Day, Hour, Minute, Second,
@@ -843,7 +843,7 @@ date_time_from_string(Str, Date) :-
Minute =< 59,
read_char((:), !Chars),
read_int_and_num_chars(Second, 2, !Chars),
Second < 62,
Second < 61,
read_microseconds(MicroSecond, !Chars),
!.Chars = [],
Date = date_time(Year, Month, Day, Hour, Minute, Second, MicroSecond)

View File

@@ -71,8 +71,8 @@
tm_mday :: int, % MonthDay (1-31)
tm_hour :: int, % Hours (after midnight, 0-23)
tm_min :: int, % Minutes (0-59)
tm_sec :: int, % Seconds (0-61)
% (60 and 61 are for leap seconds)
tm_sec :: int, % Seconds (0-60)
% (60 allows for a positive leap second)
tm_yday :: int, % YearDay (number since Jan 1st, 0-365)
tm_wday :: int, % WeekDay (number since Sunday, 0-6)
tm_dst :: maybe(dst) % IsDST (is DST applicable, and if so,

View File

@@ -31,7 +31,6 @@ date_time_from_string("2024-12-31 00:00:00") ===> TEST PASSED (accepted: date_ti
date_time_from_string("2024-01-01 00:00:00") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 0))
date_time_from_string("2024-01-01 23:59:59") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 23, 59, 59, 0))
date_time_from_string("2024-01-01 00:00:60") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 60, 0))
date_time_from_string("2024-01-01 00:00:61") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 61, 0))
date_time_from_string("2024-01-01 00:00:00.1") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 100000))
date_time_from_string("2024-01-01 00:00:00.12") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 120000))
date_time_from_string("2024-01-01 00:00:00.123") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 123000))
@@ -40,7 +39,7 @@ date_time_from_string("2024-01-01 00:00:00.12345") ===> TEST PASSED (accepted: d
date_time_from_string("2024-01-01 00:00:00.123456") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 123456))
date_time_from_string("2024-01-01 00:00:00.000001") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 1))
date_time_from_string("2024-01-01 00:00:00.999999") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 999999))
date_time_from_string("2024-12-31 23:59:61.999999") ===> TEST PASSED (accepted: date_time(2024, 12, 31, 23, 59, 61, 999999))
date_time_from_string("2024-12-31 23:59:60.999999") ===> TEST PASSED (accepted: date_time(2024, 12, 31, 23, 59, 60, 999999))
date_time_from_string("1970-01-01 00:00:00") ===> TEST PASSED (accepted: date_time(1970, 1, 1, 0, 0, 0, 0))
date_time_from_string("1582-10-15 00:00:00") ===> TEST PASSED (accepted: date_time(1582, 10, 15, 0, 0, 0, 0))
date_time_from_string("2024-01-01 00:00:00.10") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 100000): to-string: "2024-01-01 00:00:00.1"))
@@ -86,7 +85,7 @@ date_time_from_string("1900-02-29 00:00:00") ===> TEST PASSED (rejected: Feb 29
date_time_from_string("2024-04-31 00:00:00") ===> TEST PASSED (rejected: day 31 in a 30-day month)
date_time_from_string("2024-01-01 24:00:00") ===> TEST PASSED (rejected: hour 24)
date_time_from_string("2024-01-01 00:60:00") ===> TEST PASSED (rejected: minute 60)
date_time_from_string("2024-01-01 00:00:62") ===> TEST PASSED (rejected: second 62)
date_time_from_string("2024-01-01 00:00:61") ===> TEST PASSED (rejected: second 61)
date_time_from_string("2024-01-01 00:00:00.") ===> TEST PASSED (rejected: trailing dot with no digits)
date_time_from_string("2024-01-01 00:00:00.1234567") ===> TEST PASSED (rejected: seven fractional digits)
date_time_from_string("-01-01 00:00:00") ===> TEST PASSED (rejected: negative sign but only two-digit year)
@@ -149,7 +148,6 @@ det_date_time_from_string("2024-12-31 00:00:00") ===> TEST PASSED (accepted: dat
det_date_time_from_string("2024-01-01 00:00:00") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 0))
det_date_time_from_string("2024-01-01 23:59:59") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 23, 59, 59, 0))
det_date_time_from_string("2024-01-01 00:00:60") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 60, 0))
det_date_time_from_string("2024-01-01 00:00:61") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 61, 0))
det_date_time_from_string("2024-01-01 00:00:00.1") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 100000))
det_date_time_from_string("2024-01-01 00:00:00.12") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 120000))
det_date_time_from_string("2024-01-01 00:00:00.123") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 123000))
@@ -158,7 +156,7 @@ det_date_time_from_string("2024-01-01 00:00:00.12345") ===> TEST PASSED (accepte
det_date_time_from_string("2024-01-01 00:00:00.123456") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 123456))
det_date_time_from_string("2024-01-01 00:00:00.000001") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 1))
det_date_time_from_string("2024-01-01 00:00:00.999999") ===> TEST PASSED (accepted: date_time(2024, 1, 1, 0, 0, 0, 999999))
det_date_time_from_string("2024-12-31 23:59:61.999999") ===> TEST PASSED (accepted: date_time(2024, 12, 31, 23, 59, 61, 999999))
det_date_time_from_string("2024-12-31 23:59:60.999999") ===> TEST PASSED (accepted: date_time(2024, 12, 31, 23, 59, 60, 999999))
det_date_time_from_string("1970-01-01 00:00:00") ===> TEST PASSED (accepted: date_time(1970, 1, 1, 0, 0, 0, 0))
det_date_time_from_string("1582-10-15 00:00:00") ===> TEST PASSED (accepted: date_time(1582, 10, 15, 0, 0, 0, 0))
@@ -192,7 +190,7 @@ det_date_time_from_string("1900-02-29 00:00:00") ===> TEST PASSED (exception: Fe
det_date_time_from_string("2024-04-31 00:00:00") ===> TEST PASSED (exception: day 31 in a 30-day month)
det_date_time_from_string("2024-01-01 24:00:00") ===> TEST PASSED (exception: hour 24)
det_date_time_from_string("2024-01-01 00:60:00") ===> TEST PASSED (exception: minute 60)
det_date_time_from_string("2024-01-01 00:00:62") ===> TEST PASSED (exception: second 62)
det_date_time_from_string("2024-01-01 00:00:61") ===> TEST PASSED (exception: second 61)
det_date_time_from_string("2024-01-01 00:00:00.") ===> TEST PASSED (exception: trailing dot with no digits)
det_date_time_from_string("2024-01-01 00:00:00.1234567") ===> TEST PASSED (exception: seven fractional digits)
det_date_time_from_string("-01-01 00:00:00") ===> TEST PASSED (exception: negative sign but only two-digit year)

View File

@@ -204,7 +204,6 @@ valid_date_times = [
dt_conv_test("midnight", "2024-01-01 00:00:00"),
dt_conv_test("last second of the day", "2024-01-01 23:59:59"),
dt_conv_test("leap second", "2024-01-01 00:00:60"),
dt_conv_test("double leap second", "2024-01-01 00:00:61"),
dt_conv_test("one fractional digit", "2024-01-01 00:00:00.1"),
dt_conv_test("two fractional digits", "2024-01-01 00:00:00.12"),
@@ -215,7 +214,7 @@ valid_date_times = [
dt_conv_test("smallest nonzero microsecond", "2024-01-01 00:00:00.000001"),
dt_conv_test("largest microsecond value", "2024-01-01 00:00:00.999999"),
dt_conv_test("all maximum", "2024-12-31 23:59:61.999999"),
dt_conv_test("all maximum", "2024-12-31 23:59:60.999999"),
dt_conv_test("Unix epoch", "1970-01-01 00:00:00"),
dt_conv_test("first day of the Gregorian calendar",
"1582-10-15 00:00:00")
@@ -287,7 +286,7 @@ invalid_date_times = [
dt_conv_test("day 31 in a 30-day month", "2024-04-31 00:00:00"),
dt_conv_test("hour 24", "2024-01-01 24:00:00"),
dt_conv_test("minute 60", "2024-01-01 00:60:00"),
dt_conv_test("second 62", "2024-01-01 00:00:62"),
dt_conv_test("second 61", "2024-01-01 00:00:61"),
dt_conv_test("trailing dot with no digits", "2024-01-01 00:00:00."),
dt_conv_test("seven fractional digits", "2024-01-01 00:00:00.1234567"),