Skip to content

Commit 3e6c698

Browse files
paulanthonywilsonJosé Valim
authored andcommitted
Fix for converting from negative iso days on New Year in a leap year (#8145)
1 parent f167716 commit 3e6c698

File tree

2 files changed

+64
-14
lines changed

2 files changed

+64
-14
lines changed

lib/elixir/lib/calendar/iso.ex

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -208,23 +208,13 @@ defmodule Calendar.ISO do
208208

209209
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
210210
@doc false
211-
@doc since: "1.5.0"
212-
def date_from_iso_days(days) when days in 0..3_652_424 do
211+
def date_from_iso_days(days) when days in -3_652_059..3_652_424 do
213212
{year, day_of_year} = days_to_year(days)
214213
extra_day = if leap_year?(year), do: 1, else: 0
215214
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
216215
{year, month, day_in_month + 1}
217216
end
218217

219-
def date_from_iso_days(days) when days in -3_652_059..-1 do
220-
{year, day_of_year} = days_to_year(-days)
221-
previous_extra_day = if leap_year?(year), do: 1, else: 0
222-
extra_day = if leap_year?(year + 1), do: 1, else: 0
223-
day_of_year = @days_per_nonleap_year + extra_day - day_of_year
224-
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
225-
{-year - 1, month, day_in_month + previous_extra_day}
226-
end
227-
228218
defp div_mod(int1, int2) do
229219
div = div(int1, int2)
230220
rem = int1 - div * int2
@@ -761,20 +751,44 @@ defmodule Calendar.ISO do
761751
if leap_year?(year), do: 1, else: 0
762752
end
763753

754+
defp days_to_year(days) when days < 0 do
755+
year_estimate = -div(-days, @days_per_nonleap_year) - 1
756+
757+
{year, days_before_year} =
758+
days_to_year(year_estimate, days, days_to_end_of_epoch(year_estimate))
759+
760+
leap_year_pad = if leap_year?(year), do: 1, else: 0
761+
{year, leap_year_pad + @days_per_nonleap_year + days - days_before_year}
762+
end
763+
764764
defp days_to_year(days) do
765-
year = Integer.floor_div(days, @days_per_nonleap_year)
766-
{year, days_before_year} = days_to_year(year, days, days_in_previous_years(year))
765+
year_estimate = div(days, @days_per_nonleap_year)
766+
767+
{year, days_before_year} =
768+
days_to_year(year_estimate, days, days_in_previous_years(year_estimate))
769+
767770
{year, days - days_before_year}
768771
end
769772

770-
defp days_to_year(year, days1, days2) when days1 < days2 do
773+
defp days_to_year(year, days1, days2) when year < 0 and days1 >= days2 do
774+
days_to_year(year + 1, days1, days_to_end_of_epoch(year + 1))
775+
end
776+
777+
defp days_to_year(year, days1, days2) when year >= 0 and days1 < days2 do
771778
days_to_year(year - 1, days1, days_in_previous_years(year - 1))
772779
end
773780

774781
defp days_to_year(year, _days1, days2) do
775782
{year, days2}
776783
end
777784

785+
defp days_to_end_of_epoch(year) when year < 0 do
786+
previous_year = year + 1
787+
788+
div(previous_year, 4) - div(previous_year, 100) + div(previous_year, 400) +
789+
previous_year * @days_per_nonleap_year
790+
end
791+
778792
defp days_in_previous_years(0), do: 0
779793

780794
defp days_in_previous_years(year) do

lib/elixir/test/elixir/calendar/iso_test.exs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,40 @@ Code.require_file("../test_helper.exs", __DIR__)
33
defmodule Calendar.ISOTest do
44
use ExUnit.Case, async: true
55
doctest Calendar.ISO
6+
7+
describe "date_from_iso_days" do
8+
test "with positive dates" do
9+
assert {0, 1, 1} == iso_day_roundtrip(0, 1, 1)
10+
assert {0, 12, 31} == iso_day_roundtrip(0, 12, 31)
11+
assert {1, 12, 31} == iso_day_roundtrip(1, 12, 31)
12+
assert {4, 1, 1} == iso_day_roundtrip(4, 1, 1)
13+
assert {4, 12, 31} == iso_day_roundtrip(4, 12, 31)
14+
assert {9999, 12, 31} == iso_day_roundtrip(9999, 12, 31)
15+
assert {9999, 1, 1} == iso_day_roundtrip(9999, 1, 1)
16+
assert {9996, 12, 31} == iso_day_roundtrip(9996, 12, 31)
17+
assert {9996, 1, 1} == iso_day_roundtrip(9996, 1, 1)
18+
end
19+
20+
test "with negative dates" do
21+
assert {-1, 1, 1} == iso_day_roundtrip(-1, 1, 1)
22+
assert {-1, 12, 31} == iso_day_roundtrip(-1, 12, 31)
23+
assert {-1, 12, 31} == iso_day_roundtrip(-1, 12, 31)
24+
assert {-2, 1, 1} == iso_day_roundtrip(-2, 1, 1)
25+
assert {-5, 12, 31} == iso_day_roundtrip(-5, 12, 31)
26+
27+
assert {-4, 1, 1} == iso_day_roundtrip(-4, 1, 1)
28+
assert {-4, 12, 31} == iso_day_roundtrip(-4, 12, 31)
29+
30+
assert {-9999, 12, 31} == iso_day_roundtrip(-9999, 12, 31)
31+
assert {-9996, 12, 31} == iso_day_roundtrip(-9996, 12, 31)
32+
33+
assert {-9996, 12, 31} == iso_day_roundtrip(-9996, 12, 31)
34+
assert {-9996, 1, 1} == iso_day_roundtrip(-9996, 1, 1)
35+
end
36+
end
37+
38+
defp iso_day_roundtrip(year, month, day) do
39+
iso_days = Calendar.ISO.date_to_iso_days(year, month, day)
40+
Calendar.ISO.date_from_iso_days(iso_days)
41+
end
642
end

0 commit comments

Comments
 (0)