[m-rev.] for review: allow for only a single positive leap second

Julien Fischer jfischer at opturion.com
Tue Apr 14 21:58:28 AEST 2026


For review by anyone.

-----------------------------------------------

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.

Julien.

diff --git a/NEWS.md b/NEWS.md
index d70465ef1..3e78b3c8c 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -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.
+
 * 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.
+
 ### Changes to the `tree_bitset` module

 * The following predicates and functions have been added:
diff --git a/library/calendar.m b/library/calendar.m
index 496f8677a..3b6a5d240 100644
--- a/library/calendar.m
+++ b/library/calendar.m
@@ -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)
diff --git a/library/time.m b/library/time.m
index 58dc7605f..1962762fe 100644
--- a/library/time.m
+++ b/library/time.m
@@ -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,
diff --git a/tests/hard_coded/calendar_date_time_conv.exp
b/tests/hard_coded/calendar_date_time_conv.exp
index 3757903ba..3b8e3edb1 100644
--- a/tests/hard_coded/calendar_date_time_conv.exp
+++ b/tests/hard_coded/calendar_date_time_conv.exp
@@ -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)
diff --git a/tests/hard_coded/calendar_date_time_conv.m
b/tests/hard_coded/calendar_date_time_conv.m
index b680be6a8..21e7211c1 100644
--- a/tests/hard_coded/calendar_date_time_conv.m
+++ b/tests/hard_coded/calendar_date_time_conv.m
@@ -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"),


More information about the reviews mailing list