Skip to content

Commit 904adda

Browse files
authored
Calculate date_from_iso_days using neri_schneider algorithm (#14999)
1 parent 8fc181f commit 904adda

File tree

1 file changed

+49
-132
lines changed

1 file changed

+49
-132
lines changed

lib/elixir/lib/calendar/iso.ex

Lines changed: 49 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,28 @@ defmodule Calendar.ISO do
196196
@ext_date_sep ?-
197197
@ext_time_sep ?:
198198

199-
@days_per_nonleap_year 365
200-
@days_per_leap_year 366
201-
202199
# The ISO epoch starts, in this implementation,
203200
# with ~D[0000-01-01]. Era "1" starts
204201
# on ~D[0001-01-01] which is 366 days later.
205202
@iso_epoch 366
206203

204+
# Constants for date calculations using 400-year era cycles.
205+
# The algorithm uses a March-based year where March 1 is day 0.
206+
# Reference: Neri C, Schneider L. "Euclidean Affine Functions and
207+
# their Application to Calendar Algorithms". Softw Pract Exper. 2022.
208+
@days_per_year 365
209+
@years_per_era 400
210+
@days_per_era @years_per_era * @days_per_year + 97
211+
@days_per_4_years 4 * @days_per_year
212+
@days_per_100_years 100 * @days_per_year + 24
213+
@march_1_offset 31 + 29
214+
@unix_epoch_days 719_528
215+
216+
# Month calculation constants: in a March-based year, each 5-month
217+
# cycle has exactly 153 days (31+30+31+30+31 or 31+30+31+30+31).
218+
@days_per_5_months 153
219+
@months_per_cycle 5
220+
207221
[match_basic_date, match_ext_date, guard_date, read_date] =
208222
quote do
209223
[
@@ -878,34 +892,49 @@ defmodule Calendar.ISO do
878892

879893
# Converts year, month, day to count of days since 0000-01-01.
880894
@doc false
881-
def date_to_iso_days(0, 1, 1) do
882-
0
883-
end
884-
885-
def date_to_iso_days(1970, 1, 1) do
886-
719_528
887-
end
895+
def date_to_iso_days(0, 1, 1), do: 0
896+
def date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
888897

889898
def date_to_iso_days(year, month, day) do
890899
ensure_day_in_month!(year, month, day)
891900

892-
days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
893-
1
901+
y = if month <= 2, do: year - 1, else: year
902+
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
903+
year_of_era = y - era * @years_per_era
904+
month_prime = if month > 2, do: month - 3, else: month + 9
905+
day_of_year = div(@days_per_5_months * month_prime + 2, @months_per_cycle) + day - 1
906+
907+
day_of_era =
908+
@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100) + day_of_year
909+
910+
era * @days_per_era + day_of_era + @march_1_offset
894911
end
895912

896913
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
897914
@doc false
898915
def date_from_iso_days(days) do
899-
{year, day_of_year} = days_to_year(days)
916+
z = days - @march_1_offset
917+
era = if z >= 0, do: div(z, @days_per_era), else: div(z - @days_per_era + 1, @days_per_era)
918+
day_of_era = z - era * @days_per_era
919+
920+
year_of_era =
921+
div(
922+
day_of_era - div(day_of_era, @days_per_4_years) + div(day_of_era, @days_per_100_years) -
923+
div(day_of_era, @days_per_era - 1),
924+
@days_per_year
925+
)
900926

901-
{month, day_in_month} =
902-
if leap_year?(year) do
903-
year_day_to_year_date_leap(day_of_year)
904-
else
905-
year_day_to_year_date(day_of_year)
906-
end
927+
day_of_year =
928+
day_of_era -
929+
(@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100))
907930

908-
{year, month, day_in_month + 1}
931+
month_prime = div(@months_per_cycle * day_of_year + 2, @days_per_5_months)
932+
day = day_of_year - div(@days_per_5_months * month_prime + 2, @months_per_cycle) + 1
933+
month = if month_prime < 10, do: month_prime + 3, else: month_prime - 9
934+
year = year_of_era + era * @years_per_era
935+
year = if month <= 2, do: year + 1, else: year
936+
937+
{year, month, day}
909938
end
910939

911940
defp div_rem(int1, int2) do
@@ -2108,91 +2137,6 @@ defmodule Calendar.ISO do
21082137
if leap_year?(year), do: 1, else: 0
21092138
end
21102139

2111-
defp days_to_year(days) when days < 0 do
2112-
y_min = floor_div_positive_divisor(days, @days_per_nonleap_year)
2113-
y_max = floor_div_positive_divisor(days, @days_per_leap_year)
2114-
2115-
{year, day_start} =
2116-
days_to_year_interpolated(
2117-
y_min,
2118-
y_max,
2119-
days,
2120-
days_in_previous_years(y_min),
2121-
days_in_previous_years(y_max)
2122-
)
2123-
2124-
{year, days - day_start}
2125-
end
2126-
2127-
defp days_to_year(days) do
2128-
y_min = floor_div_positive_divisor(days, @days_per_leap_year)
2129-
y_max = floor_div_positive_divisor(days, @days_per_nonleap_year)
2130-
2131-
{year, day_start} =
2132-
days_to_year_interpolated(
2133-
y_min,
2134-
y_max,
2135-
days,
2136-
days_in_previous_years(y_min),
2137-
days_in_previous_years(y_max)
2138-
)
2139-
2140-
{year, days - day_start}
2141-
end
2142-
2143-
defp days_to_year_interpolated(min, max, _days, d_min, _d_max) when min >= max do
2144-
{min, d_min}
2145-
end
2146-
2147-
defp days_to_year_interpolated(min, max, days, d_min, d_max) do
2148-
diff = max - min
2149-
d_diff = d_max - d_min
2150-
2151-
numerator = diff * (days - d_min)
2152-
offset = floor_div_positive_divisor(numerator, d_diff)
2153-
2154-
mid = min + max(0, min(offset, diff))
2155-
d_mid = days_in_previous_years(mid)
2156-
mid_length = if leap_year?(mid), do: @days_per_leap_year, else: @days_per_nonleap_year
2157-
2158-
cond do
2159-
days < d_mid ->
2160-
new_max = mid - 1
2161-
days_to_year_interpolated(min, new_max, days, d_min, days_in_previous_years(new_max))
2162-
2163-
days - d_mid >= mid_length ->
2164-
new_min = mid + 1
2165-
days_to_year_interpolated(new_min, max, days, days_in_previous_years(new_min), d_max)
2166-
2167-
true ->
2168-
{mid, d_mid}
2169-
end
2170-
end
2171-
2172-
defp days_in_previous_years(0), do: 0
2173-
2174-
# A concise version of the algorithm would use floor_div instead of div.
2175-
# However, floor_div would check the operands on every operation.
2176-
# We optimize this by providing a positive and negative version of each algorithm.
2177-
defp days_in_previous_years(year) when year > 0 do
2178-
previous_year = year - 1
2179-
2180-
div(previous_year, 4) - div(previous_year, 100) +
2181-
div(previous_year, 400) + previous_year * @days_per_nonleap_year +
2182-
@days_per_leap_year
2183-
end
2184-
2185-
defp days_in_previous_years(year) when year < 0 do
2186-
previous_year = year - 1
2187-
2188-
div(year, 4) - div(year, 100) +
2189-
div(year, 400) - 1 + previous_year * @days_per_nonleap_year +
2190-
@days_per_leap_year
2191-
end
2192-
2193-
# Note that this function does not add the extra leap day for a leap year.
2194-
# If you want to add that leap day when appropriate,
2195-
# add the result of leap_day_offset/2 to the result of days_before_month/1.
21962140
defp days_before_month(1), do: 0
21972141
defp days_before_month(2), do: 31
21982142
defp days_before_month(3), do: 59
@@ -2206,33 +2150,6 @@ defmodule Calendar.ISO do
22062150
defp days_before_month(11), do: 304
22072151
defp days_before_month(12), do: 334
22082152

2209-
# Note that 0 is the first day of the month.
2210-
defp year_day_to_year_date(day_of_year) when day_of_year < 31, do: {1, day_of_year}
2211-
defp year_day_to_year_date(day_of_year) when day_of_year < 59, do: {2, day_of_year - 31}
2212-
defp year_day_to_year_date(day_of_year) when day_of_year < 90, do: {3, day_of_year - 59}
2213-
defp year_day_to_year_date(day_of_year) when day_of_year < 120, do: {4, day_of_year - 90}
2214-
defp year_day_to_year_date(day_of_year) when day_of_year < 151, do: {5, day_of_year - 120}
2215-
defp year_day_to_year_date(day_of_year) when day_of_year < 181, do: {6, day_of_year - 151}
2216-
defp year_day_to_year_date(day_of_year) when day_of_year < 212, do: {7, day_of_year - 181}
2217-
defp year_day_to_year_date(day_of_year) when day_of_year < 243, do: {8, day_of_year - 212}
2218-
defp year_day_to_year_date(day_of_year) when day_of_year < 273, do: {9, day_of_year - 243}
2219-
defp year_day_to_year_date(day_of_year) when day_of_year < 304, do: {10, day_of_year - 273}
2220-
defp year_day_to_year_date(day_of_year) when day_of_year < 334, do: {11, day_of_year - 304}
2221-
defp year_day_to_year_date(day_of_year), do: {12, day_of_year - 334}
2222-
2223-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 31, do: {1, day_of_year}
2224-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 60, do: {2, day_of_year - 31}
2225-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 91, do: {3, day_of_year - 60}
2226-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 121, do: {4, day_of_year - 91}
2227-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 152, do: {5, day_of_year - 121}
2228-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 182, do: {6, day_of_year - 152}
2229-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 213, do: {7, day_of_year - 182}
2230-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 244, do: {8, day_of_year - 213}
2231-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 274, do: {9, day_of_year - 244}
2232-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 305, do: {10, day_of_year - 274}
2233-
defp year_day_to_year_date_leap(day_of_year) when day_of_year < 335, do: {11, day_of_year - 305}
2234-
defp year_day_to_year_date_leap(day_of_year), do: {12, day_of_year - 335}
2235-
22362153
defp iso_seconds_to_datetime(seconds) do
22372154
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
22382155

0 commit comments

Comments
 (0)