File 9122-Write-tests.patch of Package erlang
From 7d8d38cbdcc044f5c46e5bd86b00b327a4c44d2e Mon Sep 17 00:00:00 2001
From: Daniel Kukula <daniel.kuku@gmail.com>
Date: Mon, 8 Dec 2025 17:20:38 +0100
Subject: [PATCH 2/4] Write tests
---
lib/stdlib/src/calendar.erl | 4 +-
lib/stdlib/test/calendar_SUITE.erl | 95 +++++++++++++++++--
lib/stdlib/test/calendar_prop_SUITE.erl | 34 ++++++-
.../test/property_test/calendar_prop.erl | 73 ++++++++++++++
4 files changed, 191 insertions(+), 15 deletions(-)
diff --git a/lib/stdlib/src/calendar.erl b/lib/stdlib/src/calendar.erl
index e2521dbcf2..db6c5bed1e 100644
--- a/lib/stdlib/src/calendar.erl
+++ b/lib/stdlib/src/calendar.erl
@@ -237,7 +237,7 @@ date_to_gregorian_days(Year, Month, Day) when is_integer(Day), Day > 0 ->
Era = if Y >= 0 -> Y div ?YEARS_PER_ERA; true -> (Y - 399) div ?YEARS_PER_ERA end,
YearOfEra = Y - Era * ?YEARS_PER_ERA,
MonthPrime = if Month > 2 -> Month - 3; true -> Month + 9 end,
- DayOfYear = ?DAYS_PER_5_MONTHS * MonthPrime div ?MONTHS_PER_CYCLE + Day - 1,
+ DayOfYear = (?DAYS_PER_5_MONTHS * MonthPrime + 2) div ?MONTHS_PER_CYCLE + Day - 1,
DayOfEra = ?DAYS_PER_YEAR * YearOfEra + YearOfEra div 4 - YearOfEra div 100 + DayOfYear,
Era * ?DAYS_PER_ERA + DayOfEra + ?MARCH_1_YEAR_0.
@@ -307,7 +307,7 @@ gregorian_days_to_date(Days) ->
- DayOfEra div (?DAYS_PER_ERA - 1)) div ?DAYS_PER_YEAR,
DayOfYear = DayOfEra - (?DAYS_PER_YEAR * YearOfEra + YearOfEra div 4 - YearOfEra div 100),
MonthPrime = (?MONTHS_PER_CYCLE * DayOfYear + 2) div ?DAYS_PER_5_MONTHS,
- Day = DayOfYear - ?DAYS_PER_5_MONTHS * MonthPrime div ?MONTHS_PER_CYCLE + 1,
+ Day = DayOfYear - (?DAYS_PER_5_MONTHS * MonthPrime + 2) div ?MONTHS_PER_CYCLE + 1,
Month = if MonthPrime < 10 -> MonthPrime + 3; true -> MonthPrime - 9 end,
Y = YearOfEra + Era * ?YEARS_PER_ERA,
Year = if Month =< 2 -> Y + 1; true -> Y end,
diff --git a/lib/stdlib/test/calendar_SUITE.erl b/lib/stdlib/test/calendar_SUITE.erl
index e73a9e3c17..82f3590283 100644
--- a/lib/stdlib/test/calendar_SUITE.erl
+++ b/lib/stdlib/test/calendar_SUITE.erl
@@ -23,10 +23,11 @@
-include_lib("common_test/include/ct.hrl").
--export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
- init_per_group/2,end_per_group/2,
+-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
+ init_per_group/2,end_per_group/2,
gregorian_days/1,
big_gregorian_days/1,
+ gregorian_days_edge_cases/1,
gregorian_seconds/1,
day_of_the_week/1,
day_of_the_week_calibrate/1,
@@ -36,21 +37,22 @@
iso_week_number/1,
system_time/1, rfc3339/1]).
--define(START_YEAR, 1947).
--define(END_YEAR, 2012).
+-define(START_YEAR, 1947).
+-define(END_YEAR, 2032).
-define(BIG_START_YEAR, 20000000).
-define(BIG_END_YEAR, 20000020).
suite() -> [{ct_hooks,[ts_install_cth]}].
-all() ->
+all() ->
[gregorian_days, gregorian_seconds, day_of_the_week,
day_of_the_week_calibrate, leap_years,
last_day_of_the_month, local_time_to_universal_time_dst,
- iso_week_number, system_time, rfc3339, big_gregorian_days].
+ iso_week_number, system_time, rfc3339, big_gregorian_days,
+ gregorian_days_edge_cases].
-groups() ->
+groups() ->
[].
init_per_suite(Config) ->
@@ -81,6 +83,79 @@ big_gregorian_days(Config) when is_list(Config) ->
MaxDays = calendar:date_to_gregorian_days({?BIG_END_YEAR, 1, 1}),
check_gregorian_days(Days, MaxDays).
+%% Tests edge cases for the Neri-Schneider algorithm.
+%% This includes epoch boundaries, leap years, century boundaries,
+%% and 400-year era boundaries.
+gregorian_days_edge_cases(Config) when is_list(Config) ->
+ %% Test epoch (day 0 = Jan 1, year 0)
+ 0 = calendar:date_to_gregorian_days(0, 1, 1),
+ {0, 1, 1} = calendar:gregorian_days_to_date(0),
+
+ %% Test year 0 boundaries (year 0 is a leap year)
+ 0 = calendar:date_to_gregorian_days({0, 1, 1}),
+ 30 = calendar:date_to_gregorian_days({0, 1, 31}),
+ 31 = calendar:date_to_gregorian_days({0, 2, 1}),
+ 58 = calendar:date_to_gregorian_days({0, 2, 28}),
+ 59 = calendar:date_to_gregorian_days({0, 2, 29}), % Leap day
+ 60 = calendar:date_to_gregorian_days({0, 3, 1}),
+ 365 = calendar:date_to_gregorian_days({0, 12, 31}),
+
+ %% Test year 1 (not a leap year)
+ 366 = calendar:date_to_gregorian_days({1, 1, 1}),
+ {1, 1, 1} = calendar:gregorian_days_to_date(366),
+ 730 = calendar:date_to_gregorian_days({1, 12, 31}),
+
+ %% Test century boundaries (1900 is not a leap year, 2000 is)
+ 693961 = calendar:date_to_gregorian_days({1900, 1, 1}),
+ {1900, 1, 1} = calendar:gregorian_days_to_date(693961),
+ 694325 = calendar:date_to_gregorian_days({1900, 12, 31}), % 365 days (not leap)
+
+ 730485 = calendar:date_to_gregorian_days({2000, 1, 1}),
+ {2000, 1, 1} = calendar:gregorian_days_to_date(730485),
+ 730850 = calendar:date_to_gregorian_days({2000, 12, 31}), % 366 days (leap)
+
+ %% Verify 1900 is not a leap year (Feb has 28 days, Mar 1 is next day)
+ 694019 = calendar:date_to_gregorian_days({1900, 2, 28}),
+ 694020 = calendar:date_to_gregorian_days({1900, 3, 1}),
+
+ %% Verify 2000 is a leap year (Feb has 29 days)
+ 730544 = calendar:date_to_gregorian_days({2000, 2, 29}),
+ 730545 = calendar:date_to_gregorian_days({2000, 3, 1}),
+
+ %% Test 400-year era boundaries
+ 146097 = calendar:date_to_gregorian_days({400, 1, 1}),
+ {400, 1, 1} = calendar:gregorian_days_to_date(146097),
+ 292194 = calendar:date_to_gregorian_days({800, 1, 1}),
+ {800, 1, 1} = calendar:gregorian_days_to_date(292194),
+
+ %% Test specific known dates
+ %% July 4, 1776 (US Independence Day)
+ 648856 = calendar:date_to_gregorian_days({1776, 7, 4}),
+ {1776, 7, 4} = calendar:gregorian_days_to_date(648856),
+
+ %% December 7, 2025 (a Sunday)
+ 739957 = calendar:date_to_gregorian_days({2025, 12, 7}),
+ {2025, 12, 7} = calendar:gregorian_days_to_date(739957),
+ 7 = calendar:day_of_the_week({2025, 12, 7}), % Sunday
+
+ %% Test far future date
+ 3652424 = calendar:date_to_gregorian_days({9999, 12, 31}),
+ {9999, 12, 31} = calendar:gregorian_days_to_date(3652424),
+
+ %% Test roundtrip for sampled days across entire valid range
+ check_roundtrip_samples(),
+
+ ok.
+
+%% Helper: check roundtrip for sampled days
+check_roundtrip_samples() ->
+ %% Sample every 10000 days from 0 to 4000000 (covers year 0 to ~10950)
+ lists:foreach(
+ fun(Days) ->
+ Date = calendar:gregorian_days_to_date(Days),
+ Days = calendar:date_to_gregorian_days(Date)
+ end, lists:seq(0, 4000000, 10000)).
+
%% Tests that datetime_to_gregorian_seconds and
%% gregorian_seconds_to_date are each others inverses for a sampled
%% number of seconds from ?START_YEAR-01-01 up to ?END_YEAR-01-01: We check
@@ -164,7 +239,7 @@ local_time_to_universal_time_dst_x(Config) when is_list(Config) ->
{{1969,12,31},{23,59,59}} ->
%% It seems that Apple has no intention of fixing this bug in
%% Mac OS 10.3.9, and we have no intention of implementing a
- %% workaround.
+ %% workaround.
{comment,"Bug in mktime() in this OS"}
end.
@@ -383,7 +458,7 @@ do_format(Time, Options) ->
calendar:system_time_to_rfc3339(Time, Options).
%% check_gregorian_days
-%%
+%%
check_gregorian_days(Days, MaxDays) when Days < MaxDays ->
Date = calendar:gregorian_days_to_date(Days),
true = calendar:valid_date(Date),
@@ -393,7 +468,7 @@ check_gregorian_days(_Days, _MaxDays) ->
ok.
%% check_gregorian_seconds
-%%
+%%
%% We increment with something prime (172801 = 2 days + 1 second).
%%
check_gregorian_seconds(Secs, MaxSecs) when Secs < MaxSecs ->
diff --git a/lib/stdlib/test/calendar_prop_SUITE.erl b/lib/stdlib/test/calendar_prop_SUITE.erl
index 7c92263df4..0d91134623 100644
--- a/lib/stdlib/test/calendar_prop_SUITE.erl
+++ b/lib/stdlib/test/calendar_prop_SUITE.erl
@@ -21,13 +21,24 @@
-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
init_per_group/2, end_per_group/2,
- rfc3339_lists_binaries/1]).
+ rfc3339_lists_binaries/1,
+ local_time_system_time_symmetry/1,
+ gregorian_days_roundtrip/1,
+ gregorian_days_monotonic/1,
+ day_of_week_cycle/1,
+ year_length/1]).
suite() ->
[{ct_hooks,[ts_install_cth]}].
all() ->
- [rfc3339_lists_binaries].
+ [rfc3339_lists_binaries,
+ universal_time_system_time_symmetry,
+ local_time_system_time_symmetry,
+ gregorian_days_roundtrip,
+ gregorian_days_monotonic,
+ day_of_week_cycle,
+ year_length].
groups() ->
[].
@@ -48,3 +59,23 @@ rfc3339_lists_binaries(Config) when is_list(Config) ->
ct_property_test:quickcheck(
calendar_prop:rfc3339_lists_binaries(),
Config).
+
+gregorian_days_roundtrip(Config) when is_list(Config) ->
+ ct_property_test:quickcheck(
+ calendar_prop:gregorian_days_roundtrip(),
+ Config).
+
+gregorian_days_monotonic(Config) when is_list(Config) ->
+ ct_property_test:quickcheck(
+ calendar_prop:gregorian_days_monotonic(),
+ Config).
+
+day_of_week_cycle(Config) when is_list(Config) ->
+ ct_property_test:quickcheck(
+ calendar_prop:day_of_week_cycle(),
+ Config).
+
+year_length(Config) when is_list(Config) ->
+ ct_property_test:quickcheck(
+ calendar_prop:year_length(),
+ Config).
diff --git a/lib/stdlib/test/property_test/calendar_prop.erl b/lib/stdlib/test/property_test/calendar_prop.erl
index 0c329d31a7..e968bea7b5 100644
--- a/lib/stdlib/test/property_test/calendar_prop.erl
+++ b/lib/stdlib/test/property_test/calendar_prop.erl
@@ -44,3 +44,76 @@ rfc3339_lists_binaries() ->
DateTimeBin =:= ListToBinary andalso FromStr =:= FromBin
end
).
+
+%% Property: date_to_gregorian_days and gregorian_days_to_date are inverses
+gregorian_days_roundtrip() ->
+ ?FORALL(
+ Days,
+ integer(0, 4_000_000), % Covers year 0 to ~10950
+ begin
+ Date = calendar:gregorian_days_to_date(Days),
+ Days =:= calendar:date_to_gregorian_days(Date)
+ end
+ ).
+
+%% Property: date_to_gregorian_days produces strictly increasing values
+gregorian_days_monotonic() ->
+ ?FORALL(
+ {Year, Month, Day},
+ valid_date(),
+ begin
+ Days1 = calendar:date_to_gregorian_days(Year, Month, Day),
+ %% Next day should be Days1 + 1
+ {Y2, M2, D2} = next_day(Year, Month, Day),
+ Days2 = calendar:date_to_gregorian_days(Y2, M2, D2),
+ Days2 =:= Days1 + 1
+ end
+ ).
+
+%% Property: day_of_the_week cycles correctly (1-7, Monday-Sunday)
+day_of_week_cycle() ->
+ ?FORALL(
+ Days,
+ integer(0, 1_000_000),
+ begin
+ DOW1 = calendar:day_of_the_week(calendar:gregorian_days_to_date(Days)),
+ DOW2 = calendar:day_of_the_week(calendar:gregorian_days_to_date(Days + 7)),
+ DOW1 =:= DOW2 andalso DOW1 >= 1 andalso DOW1 =< 7
+ end
+ ).
+
+%% Property: leap years have 366 days, non-leap years have 365 days
+year_length() ->
+ ?FORALL(
+ Year,
+ integer(0, 10000),
+ begin
+ Jan1 = calendar:date_to_gregorian_days(Year, 1, 1),
+ Dec31 = calendar:date_to_gregorian_days(Year, 12, 31),
+ YearLength = Dec31 - Jan1 + 1,
+ ExpectedLength = case calendar:is_leap_year(Year) of
+ true -> 366;
+ false -> 365
+ end,
+ YearLength =:= ExpectedLength
+ end
+ ).
+
+%% Generator for valid dates
+valid_date() ->
+ ?LET(Year, integer(0, 9999),
+ ?LET(Month, integer(1, 12),
+ ?LET(Day, integer(1, calendar:last_day_of_the_month(Year, Month)),
+ {Year, Month, Day}))).
+
+%% Helper: compute next day
+next_day(Year, Month, Day) ->
+ LastDay = calendar:last_day_of_the_month(Year, Month),
+ if
+ Day < LastDay ->
+ {Year, Month, Day + 1};
+ Month < 12 ->
+ {Year, Month + 1, 1};
+ true ->
+ {Year + 1, 1, 1}
+ end.
--
2.51.0