Skip to content

WIP: Implement big year parsing #13720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/elixir/lib/calendar/date.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1159,13 +1159,13 @@ defmodule Date do

defimpl Inspect do
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when year in -9999..9999 do
when calendar == Calendar.ISO do
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end

def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when calendar == Calendar.ISO do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})"
when year in -9999..9999 do
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end

def inspect(%{calendar: calendar, year: year, month: month, day: day}, _) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove this clause and the guards. Just always invoke calendar.date_to_string. :)

Copy link
Contributor Author

@voughtdq voughtdq Jul 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that, but wanted to avoid a situation where a particular calendar implementation is inadvertently relying on year being in -9999..9999. I know there is a Jalaali calendar implementation I can test, not sure if there are others,

Expand Down
103 changes: 103 additions & 0 deletions lib/elixir/lib/calendar/iso.ex
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,22 @@ defmodule Calendar.ISO do
]
end

[match_big_year, guard_big_year, match_big_year_rest, guard_big_year_rest, read_big_year_rest] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about this: every time we see a + or - sign, we use Integer.parse to extract the year and then we parse the rest (month/day) using the patterns below? Would that simplify things a bit? But the general direction is good to me.

quote do
[
<<y1, y2, y3, y4, y5>>,
y1 >= ?0 and y1 <= ?9 and (y2 >= ?0 and y2 <= ?9) and
(y3 >= ?0 and y3 <= ?9) and (y4 >= ?0 and y4 <= ?9) and (y5 >= ?0 and y5 <= ?9),
<<@ext_date_sep, m1, m2, @ext_date_sep, d1, d2>>,
m1 >= ?0 and m1 <= ?9 and (m2 >= ?0 and m1 <= ?9) and (d1 >= ?0 and d1 <= ?9) and
(d2 >= ?0 and d2 <= ?9),
{
(m1 - ?0) * 10 + (m2 - ?0),
(d1 - ?0) * 10 + (d2 - ?0)
}
]
end

[match_basic_time, match_ext_time, guard_time, read_time] =
quote do
[
Expand Down Expand Up @@ -421,6 +437,23 @@ defmodule Calendar.ISO do
parse_formatted_date(year, month, day, multiplier)
end

defp do_parse_date(unquote(match_big_year) <> rest, multiplier, :extended)
when unquote(guard_big_year) do
<<y1, y2, y3, y4, y5>> = unquote(match_big_year)
{year, rest} = parse_big_year(rest, [y5, y4, y3, y2, y1])

case parse_big_year_rest(rest) do
:error ->
{:error, :invalid_format}

{{month, day}, ""} ->
parse_formatted_date(year, month, day, multiplier)

{{_month, _day}, _rest} ->
{:error, :invalid_format}
end
end

defp do_parse_date(_, _, _) do
{:error, :invalid_format}
end
Expand All @@ -435,6 +468,38 @@ defmodule Calendar.ISO do
end
end

defp parse_big_year(<<y, rest::binary>>, digits) when y >= ?0 and y <= ?9 do
parse_big_year(rest, [y | digits])
end

defp parse_big_year("", _digits) do
{:error, :invalid_format}
end

defp parse_big_year(rest, digits) do
{year, _} =
Enum.reduce(digits, {0, 1}, fn digit, {year, n} ->
{(digit - ?0) * n + year, 10 * n}
end)

{year, rest}
end

defp parse_big_year_rest(unquote(match_big_year_rest)) when unquote(guard_big_year_rest) do
{month, day} = unquote(read_big_year_rest)
{{month, day}, ""}
end

defp parse_big_year_rest(unquote(match_big_year_rest) <> rest)
when unquote(guard_big_year_rest) do
{month, day} = unquote(read_big_year_rest)
{{month, day}, rest}
end

defp parse_big_year_rest(_rest) do
:error
end

@doc """
Parses a naive datetime `string` in the `:extended` format.

Expand Down Expand Up @@ -518,6 +583,25 @@ defmodule Calendar.ISO do
parse_formatted_naive_datetime(year, month, day, hour, minute, second, rest, multiplier)
end

defp do_parse_naive_datetime(unquote(match_big_year) <> rest, multiplier, :extended)
when unquote(guard_big_year) do
<<y1, y2, y3, y4, y5>> = unquote(match_big_year)
{year, rest} = parse_big_year(rest, [y5, y4, y3, y2, y1])

case parse_big_year_rest(rest) do
:error ->
{:error, :invalid_format}

{{month, day}, <<datetime_sep, unquote(match_ext_time), rest::binary>>}
when datetime_sep in @datetime_seps and unquote(guard_time) ->
{hour, minute, second} = unquote(read_time)
parse_formatted_naive_datetime(year, month, day, hour, minute, second, rest, multiplier)

{{_month, _day}, _rest} ->
{:error, :invalid_format}
end
end

defp do_parse_naive_datetime(_, _, _) do
{:error, :invalid_format}
end
Expand Down Expand Up @@ -622,6 +706,25 @@ defmodule Calendar.ISO do
parse_formatted_utc_datetime(year, month, day, hour, minute, second, rest, multiplier)
end

defp do_parse_utc_datetime(unquote(match_big_year) <> rest, multiplier, :extended)
when unquote(guard_big_year) do
<<y1, y2, y3, y4, y5>> = unquote(match_big_year)
{year, rest} = parse_big_year(rest, [y5, y4, y3, y2, y1])

case parse_big_year_rest(rest) do
:error ->
{:error, :invalid_format}

{{month, day}, <<datetime_sep, unquote(match_ext_time), rest::binary>>}
when datetime_sep in @datetime_seps and unquote(guard_time) ->
{hour, minute, second} = unquote(read_time)
parse_formatted_utc_datetime(year, month, day, hour, minute, second, rest, multiplier)

{{_month, _day}, _rest} ->
{:error, :invalid_format}
end
end

defp do_parse_utc_datetime(_, _, _) do
{:error, :invalid_format}
end
Expand Down
23 changes: 21 additions & 2 deletions lib/elixir/test/elixir/calendar/date_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ defmodule DateTest do
assert ~D[2000-01-01] ==
%Date{calendar: Calendar.ISO, year: 2000, month: 1, day: 1}

assert ~D[99999-01-01] == %Date{calendar: Calendar.ISO, year: 99_999, month: 1, day: 1}
assert ~D[+99999-01-01] == %Date{calendar: Calendar.ISO, year: 99_999, month: 1, day: 1}
assert ~D[-99999-01-01] == %Date{calendar: Calendar.ISO, year: -99_999, month: 1, day: 1}

assert ~D[20001-01-01 Calendar.Holocene] ==
%Date{calendar: Calendar.Holocene, year: 20001, month: 1, day: 1}

Expand All @@ -29,6 +33,10 @@ defmodule DateTest do
~s/cannot parse "20001-50-50" as Date for Calendar.Holocene, reason: :invalid_date/,
fn -> Code.eval_string("~D[20001-50-50 Calendar.Holocene]") end

assert_raise ArgumentError,
~s/cannot parse "555555-55-55" as Date for Calendar.ISO, reason: :invalid_date/,
fn -> Code.eval_string("~D[555555-55-55]") end

assert_raise UndefinedFunctionError, fn ->
Code.eval_string("~D[2000-01-01 UnknownCalendar]")
end
Expand All @@ -50,6 +58,14 @@ defmodule DateTest do

assert to_string(%{date2 | calendar: FakeCalendar}) == "31/12/5874897"
assert Date.to_string(%{date2 | calendar: FakeCalendar}) == "31/12/5874897"

date3 = Date.new!(-5_874_897, 12, 31)
assert to_string(date3) == "-5874897-12-31"
assert Date.to_string(date3) == "-5874897-12-31"
assert Date.to_string(Map.from_struct(date3))

assert to_string(%{date3 | calendar: FakeCalendar}) == "31/12/-5874897"
assert Date.to_string(%{date3 | calendar: FakeCalendar}) == "31/12/-5874897"
end

test "inspect/1" do
Expand All @@ -59,8 +75,11 @@ defmodule DateTest do
date = %{~D[2000-01-01] | calendar: FakeCalendar}
assert inspect(date) == "~D[1/1/2000 FakeCalendar]"

assert inspect(Date.new!(5_874_897, 12, 31)) == "Date.new!(5874897, 12, 31)"
assert inspect(Date.new!(-5_874_897, 1, 1)) == "Date.new!(-5874897, 1, 1)"
refute inspect(Date.new!(5_874_897, 12, 31)) == "Date.new!(5874897, 12, 31)"
refute inspect(Date.new!(-5_874_897, 1, 1)) == "Date.new!(-5874897, 1, 1)"

assert inspect(Date.new!(5_874_897, 12, 31)) == "~D[5874897-12-31]"
assert inspect(Date.new!(-5_874_897, 1, 1)) == "~D[-5874897-01-01]"

date2 = %{Date.new!(5_874_897, 12, 31) | calendar: FakeCalendar}

Expand Down
48 changes: 48 additions & 0 deletions lib/elixir/test/elixir/calendar/datetime_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,54 @@ defmodule DateTimeTest do
zone_abbr: "UTC"
}

assert ~U[10000-07-08 18:35:22.193042Z] ==
%DateTime{
calendar: Calendar.ISO,
year: 10_000,
month: 7,
day: 8,
hour: 18,
minute: 35,
second: 22,
microsecond: {193_042, 6},
std_offset: 0,
utc_offset: 0,
time_zone: "Etc/UTC",
zone_abbr: "UTC"
}

assert ~U[-10000-07-08 18:35:22.193042Z] ==
%DateTime{
calendar: Calendar.ISO,
year: -10_000,
month: 7,
day: 8,
hour: 18,
minute: 35,
second: 22,
microsecond: {193_042, 6},
std_offset: 0,
utc_offset: 0,
time_zone: "Etc/UTC",
zone_abbr: "UTC"
}

assert ~U[-12345-07-08T18:35:22.2288Z] ==
%DateTime{
calendar: Calendar.ISO,
year: -12_345,
month: 7,
day: 8,
hour: 18,
minute: 35,
second: 22,
microsecond: {228_800, 4},
std_offset: 0,
utc_offset: 0,
time_zone: "Etc/UTC",
zone_abbr: "UTC"
}

assert ~U[2000-01-01 12:34:56Z Calendar.Holocene] ==
%DateTime{
calendar: Calendar.Holocene,
Expand Down
49 changes: 49 additions & 0 deletions lib/elixir/test/elixir/calendar/iso_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Calendar.ISOTest do
assert {9999, 1, 1} == iso_day_roundtrip(9999, 1, 1)
assert {9996, 12, 31} == iso_day_roundtrip(9996, 12, 31)
assert {9996, 1, 1} == iso_day_roundtrip(9996, 1, 1)
assert {55_555, 5, 5} == iso_day_roundtrip(55_555, 5, 5)
end

test "with negative dates" do
Expand All @@ -32,6 +33,8 @@ defmodule Calendar.ISOTest do

assert {-9996, 12, 31} == iso_day_roundtrip(-9996, 12, 31)
assert {-9996, 1, 1} == iso_day_roundtrip(-9996, 1, 1)

assert {-55_555, 5, 5} == iso_day_roundtrip(-55_555, 5, 5)
end
end

Expand All @@ -48,6 +51,11 @@ defmodule Calendar.ISOTest do
assert Calendar.ISO.date_to_string(10000, 1, 1, :basic) == "100000101"
assert Calendar.ISO.date_to_string(10000, 1, 1, :extended) == "10000-01-01"
end

test "handles years < -9999" do
assert Calendar.ISO.date_to_string(-99_999, 1, 1, :basic) == "-999990101"
assert Calendar.ISO.date_to_string(-99_999, 1, 1, :extended) == "-99999-01-01"
end
end

describe "naive_datetime_to_iso_days/7" do
Expand All @@ -72,6 +80,10 @@ defmodule Calendar.ISOTest do
Calendar.ISO.day_of_week(2017, 11, 0, :default)
end
end

test "returns the correct day of week for big years" do
assert Calendar.ISO.day_of_week(27579, 7, 13, :default) == {5, 1, 7}
end
end

describe "day_of_era/3" do
Expand Down Expand Up @@ -126,11 +138,24 @@ defmodule Calendar.ISOTest do
assert Calendar.ISO.parse_date("20150123") == {:error, :invalid_format}
assert Calendar.ISO.parse_date("2015-01-23") == {:ok, {2015, 1, 23}}
end

test "parses big years" do
assert Calendar.ISO.parse_date("+201500-01-23") == {:ok, {201_500, 1, 23}}
assert Calendar.ISO.parse_date("201500-01-23") == {:ok, {201_500, 1, 23}}
assert Calendar.ISO.parse_date("-201500-01-23") == {:ok, {-201_500, 1, 23}}
end

test "fails with bad data" do
assert Calendar.ISO.parse_date("+201500-01-23 ") == {:error, :invalid_format}
assert Calendar.ISO.parse_date("2015y00-01-23") == {:error, :invalid_format}
assert Calendar.ISO.parse_date("201500T01-23") == {:error, :invalid_format}
end
end

describe "parse_date/2" do
test "allows enforcing basic formats" do
assert Calendar.ISO.parse_date("20150123", :basic) == {:ok, {2015, 1, 23}}
assert Calendar.ISO.parse_date("20000150123", :basic) == {:error, :invalid_format}
assert Calendar.ISO.parse_date("2015-01-23", :basic) == {:error, :invalid_format}
end

Expand Down Expand Up @@ -238,6 +263,9 @@ defmodule Calendar.ISOTest do
assert Calendar.ISO.parse_naive_datetime("2015:01:23 23-50-07") == {:error, :invalid_format}
assert Calendar.ISO.parse_naive_datetime("2015-01-23P23:50:07") == {:error, :invalid_format}

assert Calendar.ISO.parse_naive_datetime("303030-01-12X23:11:40") ==
{:error, :invalid_format}

assert Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07A") ==
{:error, :invalid_format}
end
Expand All @@ -257,6 +285,9 @@ defmodule Calendar.ISOTest do
assert Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-02:30") ==
{:ok, {2015, 1, 23, 23, 50, 7, {123_000, 3}}}

assert Calendar.ISO.parse_naive_datetime("20152015-01-23T23:50:07.123+02:30") ==
{:ok, {20_152_015, 1, 23, 23, 50, 7, {123_000, 3}}}

assert Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-00:00") ==
{:error, :invalid_format}

Expand Down Expand Up @@ -290,6 +321,17 @@ defmodule Calendar.ISOTest do
assert Calendar.ISO.parse_naive_datetime("2015-01-23 235007.123") ==
{:error, :invalid_format}
end

test "supports big years" do
assert Calendar.ISO.parse_naive_datetime("123456-12-01 13:27:34") ==
{:ok, {123_456, 12, 1, 13, 27, 34, {0, 0}}}

assert Calendar.ISO.parse_naive_datetime("+123456-12-01 13:27:34") ==
{:ok, {123_456, 12, 1, 13, 27, 34, {0, 0}}}

assert Calendar.ISO.parse_naive_datetime("-123456-12-01 13:27:34") ==
{:ok, {-123_456, 12, 1, 13, 27, 34, {0, 0}}}
end
end

describe "parse_naive_datetime/2" do
Expand All @@ -299,6 +341,9 @@ defmodule Calendar.ISOTest do

assert Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07.123", :basic) ==
{:error, :invalid_format}

assert Calendar.ISO.parse_naive_datetime("55555-01-23 23:50:07.123", :basic) ==
{:error, :invalid_format}
end

test "allows enforcing extended formats" do
Expand Down Expand Up @@ -344,6 +389,9 @@ defmodule Calendar.ISOTest do
assert Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07.123-02:30") ==
{:ok, {2015, 1, 24, 2, 20, 7, {123_000, 3}}, -9000}

assert Calendar.ISO.parse_utc_datetime("200015-01-23T23:50:07.123-02:30") ==
{:ok, {200_015, 1, 24, 2, 20, 7, {123_000, 3}}, -9000}

assert Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07.123-00:00") ==
{:error, :invalid_format}

Expand Down Expand Up @@ -452,6 +500,7 @@ defmodule Calendar.ISOTest do
assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 7)) == {2024, 8, 31}
assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 8)) == {2024, 9, 30}
assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 9)) == {2024, 10, 31}
assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(year: 200_000)) == {202_024, 1, 31}
end

test "shift_naive_datetime/2" do
Expand Down