diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index f51ae3e56df..43ffefd4e51 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -942,7 +942,7 @@ defmodule Date do %Date{calendar: calendar, year: year, month: month, day: day} {day_of_week, first_day_of_week, _} -> - add(date, -(day_of_week - first_day_of_week)) + add(date, -Integer.mod(day_of_week - first_day_of_week, 7)) end end @@ -995,7 +995,7 @@ defmodule Date do %Date{calendar: calendar, year: year, month: month, day: day} {day_of_week, _, last_day_of_week} -> - add(date, last_day_of_week - day_of_week) + add(date, Integer.mod(last_day_of_week - day_of_week, 7)) end end diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index d2b34d67e3d..80a8454de33 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -1,5 +1,6 @@ Code.require_file("../test_helper.exs", __DIR__) Code.require_file("holocene.exs", __DIR__) +Code.require_file("week_starts_on_sunday.exs", __DIR__) Code.require_file("fakes.exs", __DIR__) defmodule DateTest do @@ -138,6 +139,15 @@ defmodule DateTest do assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 04), :sunday) == 6 assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 05), :sunday) == 7 assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 06), :sunday) == 1 + + assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 06)) == 1 + assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 07)) == 2 + assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 08)) == 3 + assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 09)) == 4 + assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 10)) == 5 + assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 11)) == 6 + assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 12)) == 7 + assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 13)) == 1 end test "beginning_of_week" do @@ -152,6 +162,12 @@ defmodule DateTest do assert Date.beginning_of_week(Calendar.Holocene.date(2020, 07, 11), :saturday) == Calendar.Holocene.date(2020, 07, 11) + + assert Date.beginning_of_week(Calendar.WeekStartsSunday.date(2025, 01, 08)) == + Calendar.WeekStartsSunday.date(2025, 01, 05) + + assert Date.beginning_of_week(Calendar.WeekStartsSunday.date(2025, 01, 01)) == + Calendar.WeekStartsSunday.date(2024, 12, 29) end test "end_of_week" do @@ -166,6 +182,12 @@ defmodule DateTest do assert Date.end_of_week(Calendar.Holocene.date(2020, 07, 05), :saturday) == Calendar.Holocene.date(2020, 07, 10) + + assert Date.end_of_week(Calendar.WeekStartsSunday.date(2025, 01, 08)) == + Calendar.WeekStartsSunday.date(2025, 01, 11) + + assert Date.end_of_week(Calendar.WeekStartsSunday.date(2025, 01, 01)) == + Calendar.WeekStartsSunday.date(2025, 01, 04) end test "convert/2" do diff --git a/lib/elixir/test/elixir/calendar/week_starts_on_sunday.exs b/lib/elixir/test/elixir/calendar/week_starts_on_sunday.exs new file mode 100644 index 00000000000..587d419c40e --- /dev/null +++ b/lib/elixir/test/elixir/calendar/week_starts_on_sunday.exs @@ -0,0 +1,179 @@ +defmodule Calendar.WeekStartsSunday do + # This calendar is used to test day_of_week calculations + # when the first day of the week is greater than the last + # day of the week. In this calendar, the week starts on + # Sunday (7) and end on Saturday (6). + + # The rest of this calendar is a copy/paste of Calendar.Holocene + # except removing the 1000 year offset. + + @behaviour Calendar + + def date(year, month, day) do + %Date{year: year, month: month, day: day, calendar: __MODULE__} + end + + def naive_datetime(year, month, day, hour, minute, second, microsecond \\ {0, 0}) do + %NaiveDateTime{ + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond, + calendar: __MODULE__ + } + end + + @impl true + def date_to_string(year, month, day) do + "#{year}-#{zero_pad(month, 2)}-#{zero_pad(day, 2)}" + end + + @impl true + def naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) do + "#{year}-#{zero_pad(month, 2)}-#{zero_pad(day, 2)}" <> + Calendar.ISO.time_to_string(hour, minute, second, microsecond) + end + + @impl true + def datetime_to_string( + year, + month, + day, + hour, + minute, + second, + microsecond, + _time_zone, + zone_abbr, + _utc_offset, + _std_offset + ) do + "#{year}-#{zero_pad(month, 2)}-#{zero_pad(day, 2)}" <> + Calendar.ISO.time_to_string(hour, minute, second, microsecond) <> + " #{zone_abbr}" + end + + @impl true + def day_of_week(year, month, day, _starting_on) do + {day_of_week, 1, 7} = Calendar.ISO.day_of_week(year, month, day, :sunday) + if day_of_week == 1, do: {7, 7, 6}, else: {day_of_week - 1, 7, 6} + end + + @impl true + defdelegate time_to_string(hour, minute, second, microsecond), to: Calendar.ISO + + @impl true + def day_rollover_relative_to_midnight_utc(), do: {0, 1} + + @impl true + def naive_datetime_from_iso_days(entry) do + {year, month, day, hour, minute, second, microsecond} = + Calendar.ISO.naive_datetime_from_iso_days(entry) + + {year, month, day, hour, minute, second, microsecond} + end + + @impl true + def naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) do + Calendar.ISO.naive_datetime_to_iso_days( + year, + month, + day, + hour, + minute, + second, + microsecond + ) + end + + defp zero_pad(val, count) when val >= 0 do + String.pad_leading("#{val}", count, ["0"]) + end + + defp zero_pad(val, count) do + "-" <> zero_pad(-val, count) + end + + @impl true + def parse_date(string) do + {year, month, day} = + string + |> String.split("-") + |> Enum.map(&String.to_integer/1) + |> List.to_tuple() + + if valid_date?(year, month, day) do + {:ok, {year, month, day}} + else + {:error, :invalid_date} + end + end + + @impl true + def valid_date?(year, month, day) do + :calendar.valid_date(year, month, day) + end + + @impl true + defdelegate parse_time(string), to: Calendar.ISO + + @impl true + defdelegate parse_naive_datetime(string), to: Calendar.ISO + + @impl true + defdelegate parse_utc_datetime(string), to: Calendar.ISO + + @impl true + defdelegate time_from_day_fraction(day_fraction), to: Calendar.ISO + + @impl true + defdelegate time_to_day_fraction(hour, minute, second, microsecond), to: Calendar.ISO + + @impl true + defdelegate leap_year?(year), to: Calendar.ISO + + @impl true + defdelegate days_in_month(year, month), to: Calendar.ISO + + @impl true + defdelegate months_in_year(year), to: Calendar.ISO + + @impl true + defdelegate day_of_year(year, month, day), to: Calendar.ISO + + @impl true + defdelegate quarter_of_year(year, month, day), to: Calendar.ISO + + @impl true + defdelegate year_of_era(year, month, day), to: Calendar.ISO + + @impl true + defdelegate day_of_era(year, month, day), to: Calendar.ISO + + @impl true + defdelegate valid_time?(hour, minute, second, microsecond), to: Calendar.ISO + + @impl true + defdelegate iso_days_to_beginning_of_day(iso_days), to: Calendar.ISO + + @impl true + defdelegate iso_days_to_end_of_day(iso_days), to: Calendar.ISO + + @impl true + def shift_date(_year, _month, _day, _duration) do + raise "shift_date/4 not implemented" + end + + @impl true + def shift_naive_datetime(_year, _month, _day, _hour, _minute, _second, _microsecond, _duration) do + raise "shift_naive_datetime/8 not implemented" + end + + @impl true + def shift_time(_hour, _minute, _second, _microsecond, _duration) do + raise "shift_time/5 not implemented" + end +end