[m-rev.] for review: add is_leap_year/1 and days_in_month/2

Julien Fischer jfischer at opturion.com
Fri Mar 20 19:32:45 AEDT 2026


For review by anyone.

Add is_leap_year/1 and days_in_month/2.

Add a predicate to the calendar module for testing if a year is a leap year.
Use a more efficient method for determining this than the existing code
in the implementation of this module used (and replace that code with
a call to the new predicate).

Add the function days_to_month/2, which is a strongly typed wrapper
for the implementation function max_day_in_month_for/2.

Add a new test case covering basic operations in the calendar module,
together with the newly added operations.

library/calendar.m:
    As above.

NEWS.md:
    Announce the new additions.

tests/hard_coded/Mmakefile:
tests/hard_coded/calendar_basics.{m,exp}:
    Add the new test case.

Julien.

diff --git a/NEWS.md b/NEWS.md
index 5d49d2628..2e5f084be 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -224,6 +224,13 @@ Changes to the Mercury standard library
     - func `promise_only_solution/1`
     - pred `promise_only_solution_io/4`

+### Changes to the `calendar` module
+
+* The following function and predicate have been added:
+
+    - func `days_in_month/2`
+    - pred `is_leap_year/1`
+
 ### Changes to the `char` module

 * The following type has had its typeclass memberships changed:
diff --git a/library/calendar.m b/library/calendar.m
index 78f7f70e7..2affb0911 100644
--- a/library/calendar.m
+++ b/library/calendar.m
@@ -124,6 +124,24 @@
     %
 :- 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.
+    %
+    % A year is a leap year if it is divisible by 4, except that years
+    % divisible by 100 are not leap years, unless they are also divisible
+    % by 400.
+    %
+:- pred is_leap_year(year::in) is semidet.
+
 %---------------------%

     % init_date(Year, Month, Day, Hour, Minute, Second, MicroSecond, Date):
@@ -670,11 +688,37 @@ month_to_int(Month) = Int :-
 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) :-
+    ( if Year /\ 3 = 0 then
+        % Year is divisible by 4.
+        ( if Year `unchecked_rem` 25 \= 0 then
+            % Year is not divisible by 25. Since it is divisible by 4
+            % but not by 25, it is not divisible by lcm(4, 25) = 100,
+            % so it is not a century year. All non-century years that are
+            % multiples of 4 are leap years.
+            true
+        else
+            % Year is divisible by both 4 and 25, therefore it is
+            % divisible by lcm(4, 25) = 100: it is a century year.
+            % A century year is a leap year only if it is divisible
+            % by 400. Since Year is already divisible by 100,
+            % it is divisible by 400 iff it is also divisible by
+            % lcm(100, 16) = 400, i.e. iff it is divisible by 16.
+            Year /\ 15 = 0
+        )
+    else
+        % Year is not divisible by 4, so it is not a leap year.
+        fail
+    ).
+
 %---------------------------------------------------------------------------%

 init_date(Year, Month, Day, Hour, Minute, Second, MicroSecond, Date) :-
     Day >= 1,
-    Day =< max_day_in_month_for(Year, month_to_int(Month)),
+    Day =< days_in_month(Year, Month),
     Hour >= 0,
     Hour < 24,
     Minute >= 0,
@@ -1089,7 +1133,7 @@ max_day_in_month_for(YearValue, MonthValue) = Max :-
             Max0 = 30
         ;
             M = 2,
-            ( if ( Y mod 400 = 0 ; ( Y mod 100 \= 0, Y mod 4 = 0 ) ) then
+            ( if is_leap_year(Y) then
                 Max0 = 29
             else
                 Max0 = 28
diff --git a/tests/hard_coded/Mmakefile b/tests/hard_coded/Mmakefile
index 68cb61346..dc4e1b1ad 100644
--- a/tests/hard_coded/Mmakefile
+++ b/tests/hard_coded/Mmakefile
@@ -791,6 +791,7 @@ ifeq "$(findstring profdeep,$(GRADE))" ""
  bitwise_uint32 \
  bitwise_uint64 \
  bitwise_uint8 \
+ calendar_basics \
  calendar_init_date \
  char_to_string \
  clamp_int \
diff --git a/tests/hard_coded/calendar_basics.exp
b/tests/hard_coded/calendar_basics.exp
new file mode 100644
index 000000000..3beef968b
--- /dev/null
+++ b/tests/hard_coded/calendar_basics.exp
@@ -0,0 +1,109 @@
+=== Test det_int_to_month/2 ===
+
+det_int_to_month(-1) ==> EXCEPTION
+det_int_to_month(0) ==> EXCEPTION
+det_int_to_month(1) ==> january
+det_int_to_month(2) ==> february
+det_int_to_month(11) ==> november
+det_int_to_month(12) ==> december
+det_int_to_month(13) ==> EXCEPTION
+
+=== Test det_int0_to_month/2 ===
+
+det_int0_to_month(-1) ==> EXCEPTION
+det_int0_to_month(0) ==> january
+det_int0_to_month(1) ==> february
+det_int0_to_month(2) ==> march
+det_int0_to_month(11) ==> december
+det_int0_to_month(12) ==> EXCEPTION
+det_int0_to_month(13) ==> EXCEPTION
+
+=== Test int_to_month/2 ===
+
+int_to_month(-1) ==> FAILED
+int_to_month(0) ==> FAILED
+int_to_month(1) ==> january
+int_to_month(2) ==> february
+int_to_month(11) ==> november
+int_to_month(12) ==> december
+int_to_month(13) ==> FAILED
+
+=== Test int0_to_month/2 ===
+
+int0_to_month(-1) ==> FAILED
+int0_to_month(0) ==> january
+int0_to_month(1) ==> february
+int0_to_month(2) ==> march
+int0_to_month(11) ==> december
+int0_to_month(12) ==> FAILED
+int0_to_month(13) ==> FAILED
+
+=== Test month_to_int/1 ===
+
+month_to_int(january) = 1
+month_to_int(february) = 2
+month_to_int(march) = 3
+month_to_int(april) = 4
+month_to_int(may) = 5
+month_to_int(june) = 6
+month_to_int(july) = 7
+month_to_int(august) = 8
+month_to_int(september) = 9
+month_to_int(october) = 10
+month_to_int(november) = 11
+month_to_int(december) = 12
+
+=== Test month_to_int0/1 ===
+
+month_to_int0(january) = 0
+month_to_int0(february) = 1
+month_to_int0(march) = 2
+month_to_int0(april) = 3
+month_to_int0(may) = 4
+month_to_int0(june) = 5
+month_to_int0(july) = 6
+month_to_int0(august) = 7
+month_to_int0(september) = 8
+month_to_int0(october) = 9
+month_to_int0(november) = 10
+month_to_int0(december) = 11
+
+=== Test days_in_month/2 ===
+
+days_in_month(1977, january) = 31
+days_in_month(1977, february) = 28
+days_in_month(1977, march) = 31
+days_in_month(1977, april) = 30
+days_in_month(1977, may) = 31
+days_in_month(1977, june) = 30
+days_in_month(1977, july) = 31
+days_in_month(1977, august) = 31
+days_in_month(1977, september) = 30
+days_in_month(1977, october) = 31
+days_in_month(1977, november) = 30
+days_in_month(1977, december) = 31
+
+days_in_month(2000, january) = 31
+days_in_month(2000, february) = 29
+days_in_month(2000, march) = 31
+days_in_month(2000, april) = 30
+days_in_month(2000, may) = 31
+days_in_month(2000, june) = 30
+days_in_month(2000, july) = 31
+days_in_month(2000, august) = 31
+days_in_month(2000, september) = 30
+days_in_month(2000, october) = 31
+days_in_month(2000, november) = 30
+days_in_month(2000, december) = 31
+
+=== Test is_leap_year/1 ===
+
+Year 2000 is a leap year.
+Year 1900 is a common year.
+Year 2024 is a leap year.
+Year 2023 is a common year.
+Year 0 is a leap year.
+Year -1 is a common year.
+Year -4 is a leap year.
+Year -100 is a common year.
+
diff --git a/tests/hard_coded/calendar_basics.m
b/tests/hard_coded/calendar_basics.m
new file mode 100644
index 000000000..0c0901f98
--- /dev/null
+++ b/tests/hard_coded/calendar_basics.m
@@ -0,0 +1,179 @@
+%---------------------------------------------------------------------------%
+% vim: ft=mercury ts=4 sw=4 et
+%---------------------------------------------------------------------------%
+
+:- module calendar_basics.
+:- interface.
+
+:- import_module io.
+
+:- pred main(io::di, io::uo) is cc_multi.
+
+%---------------------------------------------------------------------------%
+%---------------------------------------------------------------------------%
+
+:- implementation.
+
+:- import_module calendar.
+:- import_module list.
+:- import_module string.
+
+%---------------------------------------------------------------------------%
+
+main(!IO) :-
+    test_det_int_to_month("det_int_to_month", det_int_to_month, !IO),
+    test_det_int_to_month("det_int0_to_month", det_int0_to_month, !IO),
+    test_int_to_month("int_to_month",
+        (pred(I::in, M::out) is semidet :- int_to_month(I, M)), !IO),
+    test_int_to_month("int0_to_month",
+        (pred(I::in, M::out) is semidet :- int0_to_month(I, M)), !IO),
+    test_month_to_int("month_to_int", month_to_int, !IO),
+    test_month_to_int("month_to_int0", month_to_int0, !IO),
+    test_days_in_month(!IO),
+    test_is_leap_year(!IO).
+
+%---------------------------------------------------------------------------%
+
+:- pred test_det_int_to_month(string::in,
+    (func(int) = month)::in, io::di, io::uo) is cc_multi.
+
+test_det_int_to_month(Desc, Func, !IO) :-
+    io.format("=== Test %s/2 ===\n\n", [s(Desc)], !IO),
+    list.foldl(do_test_det_int_to_month(Desc, Func), ints, !IO),
+    io.nl(!IO).
+
+:- pred do_test_det_int_to_month(string::in,
+    (func(int) = month)::in, int::in, io::di, io::uo) is cc_multi.
+
+do_test_det_int_to_month(Desc, Func, Int, !IO) :-
+    io.format("%s(%d) ==> ", [s(Desc), i(Int)], !IO),
+    ( try []
+        Month = Func(Int)
+    then
+        io.format("%s\n", [s(string(Month))], !IO)
+    catch_any _ ->
+        io.write_string("EXCEPTION\n", !IO)
+    ).
+
+%---------------------------------------------------------------------------%
+
+:- pred test_int_to_month(string::in,
+    pred(int, month)::in(pred(in, out) is semidet), io::di, io::uo) is det.
+
+test_int_to_month(Desc, Pred, !IO) :-
+    io.format("=== Test %s/2 ===\n\n", [s(Desc)], !IO),
+    list.foldl(do_test_int_to_month(Desc, Pred), ints, !IO),
+    io.nl(!IO).
+
+:- pred do_test_int_to_month(string::in,
+    pred(int, month)::in(pred(in, out) is semidet), int::in,
+    io::di, io::uo) is det.
+
+do_test_int_to_month(Desc, Pred, Int, !IO) :-
+    io.format("%s(%d) ==> ", [s(Desc), i(Int)], !IO),
+    ( if Pred(Int, Month) then
+        io.format("%s\n", [s(string(Month))], !IO)
+    else
+        io.write_string("FAILED\n", !IO)
+    ).
+
+:- func ints = list(int).
+
+ints = [
+    -1,
+    0,
+    1,
+    2,
+    11,
+    12,
+    13
+].
+
+%---------------------------------------------------------------------------%
+
+:- pred test_month_to_int(string::in, (func(month) = int)::in,
+    io::di, io::uo) is det.
+
+test_month_to_int(Desc, Func, !IO) :-
+    io.format("=== Test %s/1 ===\n\n", [s(Desc)], !IO),
+    list.foldl(do_test_month_to_int(Desc, Func), months, !IO),
+    io.nl(!IO).
+
+:- pred do_test_month_to_int(string::in, (func(month) = int)::in, month::in,
+    io::di, io::uo) is det.
+
+do_test_month_to_int(Desc, Func, Month, !IO) :-
+    Int = Func(Month),
+    io.format("%s(%s) = %d\n", [s(Desc), s(string(Month)), i(Int)], !IO).
+
+%---------------------------------------------------------------------------%
+
+:- pred test_days_in_month(io::di, io::uo) is det.
+
+test_days_in_month(!IO) :-
+    io.write_string("=== Test days_in_month/2 ===\n\n", !IO),
+    list.foldl(do_test_days_in_month, [1977, 2000], !IO).
+
+:- pred do_test_days_in_month(year::in, io::di, io::uo) is det.
+
+do_test_days_in_month(Year, !IO) :-
+    list.foldl(do_test_days_in_month_2(Year), months, !IO),
+    io.nl(!IO).
+
+:- pred do_test_days_in_month_2(year::in, month::in, io::di, io::uo) is det.
+
+do_test_days_in_month_2(Year, Month, !IO) :-
+    DaysInMonth = days_in_month(Year, Month),
+    io.format("days_in_month(%d, %s) = %d\n",
+        [i(Year), s(string(Month)), i(DaysInMonth)], !IO).
+
+%---------------------------------------------------------------------------%
+
+:- pred test_is_leap_year(io::di, io::uo) is det.
+
+test_is_leap_year(!IO) :-
+    io.write_string("=== Test is_leap_year/1 ===\n\n", !IO),
+    list.foldl(do_test_is_leap_year, test_years, !IO),
+    io.nl(!IO).
+
+:- pred do_test_is_leap_year(year::in, io::di, io::uo) is det.
+
+do_test_is_leap_year(Year, !IO) :-
+    Desc = ( if is_leap_year(Year) then "leap" else "common" ),
+    io.format("Year %d is a %s year.\n", [i(Year), s(Desc)], !IO).
+
+:- func test_years = list(year).
+
+test_years = [
+    2000,   % Divisible by 400: leap year.
+    1900,   % Divisible by 100, but not by 100: common year.
+    2024,   % Divisible by 4, but not by 1000: leap year.
+    2023,   % Not divisible by 4: common year.
+       0,   % Divisible by 400: leap year.
+      -1,   % Not divisible by 4: common year.
+      -4,   % Divisible by 4: leap year.
+    -100    % Divisible by 100, but not 400: common year.
+].
+
+%---------------------------------------------------------------------------%
+
+:- func months = list(month).
+
+months = [
+    january,
+    february,
+    march,
+    april,
+    may,
+    june,
+    july,
+    august,
+    september,
+    october,
+    november,
+    december
+].
+
+%---------------------------------------------------------------------------%
+:- end_module calendar_basics.
+%---------------------------------------------------------------------------%


More information about the reviews mailing list