[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