Skip to content

Commit 44cf8e1

Browse files
committed
Fix Date.beginning/end_of_week
when first_day > last_day. Some calendars may start the week on a Sunday (for example, the Gregorian calendar as used in Israel) and therefore the return value from `Date.day_of_week/4` may be of the form `{1, 7, 6}`. In these cases, `Date.beginning_of_week/2` and `Date.end_of_week/2` return incorrect values. This commit adjust the calculation to return the correct values.
1 parent ef1450d commit 44cf8e1

File tree

3 files changed

+204
-2
lines changed

3 files changed

+204
-2
lines changed

lib/elixir/lib/calendar/date.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,7 @@ defmodule Date do
942942
%Date{calendar: calendar, year: year, month: month, day: day}
943943

944944
{day_of_week, first_day_of_week, _} ->
945-
add(date, -(day_of_week - first_day_of_week))
945+
add(date, -Integer.mod(day_of_week - first_day_of_week, 7))
946946
end
947947
end
948948

@@ -995,7 +995,7 @@ defmodule Date do
995995
%Date{calendar: calendar, year: year, month: month, day: day}
996996

997997
{day_of_week, _, last_day_of_week} ->
998-
add(date, last_day_of_week - day_of_week)
998+
add(date, Integer.mod(last_day_of_week - day_of_week, 7))
999999
end
10001000
end
10011001

lib/elixir/test/elixir/calendar/date_test.exs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Code.require_file("../test_helper.exs", __DIR__)
22
Code.require_file("holocene.exs", __DIR__)
3+
Code.require_file("week_starts_on_sunday.exs", __DIR__)
34
Code.require_file("fakes.exs", __DIR__)
45

56
defmodule DateTest do
@@ -138,6 +139,15 @@ defmodule DateTest do
138139
assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 04), :sunday) == 6
139140
assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 05), :sunday) == 7
140141
assert Date.day_of_week(Calendar.Holocene.date(2016, 11, 06), :sunday) == 1
142+
143+
assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 06)) == 1
144+
assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 07)) == 2
145+
assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 08)) == 3
146+
assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 09)) == 4
147+
assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 10)) == 5
148+
assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 11)) == 6
149+
assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 12)) == 7
150+
assert Date.day_of_week(Calendar.WeekStartsSunday.date(2025, 01, 13)) == 1
141151
end
142152

143153
test "beginning_of_week" do
@@ -152,6 +162,12 @@ defmodule DateTest do
152162

153163
assert Date.beginning_of_week(Calendar.Holocene.date(2020, 07, 11), :saturday) ==
154164
Calendar.Holocene.date(2020, 07, 11)
165+
166+
assert Date.beginning_of_week(Calendar.WeekStartsSunday.date(2025, 01, 08)) ==
167+
Calendar.WeekStartsSunday.date(2025, 01, 05)
168+
169+
assert Date.beginning_of_week(Calendar.WeekStartsSunday.date(2025, 01, 01)) ==
170+
Calendar.WeekStartsSunday.date(2024, 12, 29)
155171
end
156172

157173
test "end_of_week" do
@@ -166,6 +182,12 @@ defmodule DateTest do
166182

167183
assert Date.end_of_week(Calendar.Holocene.date(2020, 07, 05), :saturday) ==
168184
Calendar.Holocene.date(2020, 07, 10)
185+
186+
assert Date.end_of_week(Calendar.WeekStartsSunday.date(2025, 01, 08)) ==
187+
Calendar.WeekStartsSunday.date(2025, 01, 11)
188+
189+
assert Date.end_of_week(Calendar.WeekStartsSunday.date(2025, 01, 01)) ==
190+
Calendar.WeekStartsSunday.date(2025, 01, 04)
169191
end
170192

171193
test "convert/2" do
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
defmodule Calendar.WeekStartsSunday do
2+
# This calendar is used to test day_of_week calculations
3+
# when the first day of the week is greater than the last
4+
# day of the week. In this calendar, the week starts on
5+
# Sunday (7) and end on Saturday (6).
6+
7+
# The rest of this calendar is a copy/paste of Calendar.Holocene
8+
# except removing the 1000 year offset.
9+
10+
@behaviour Calendar
11+
12+
def date(year, month, day) do
13+
%Date{year: year, month: month, day: day, calendar: __MODULE__}
14+
end
15+
16+
def naive_datetime(year, month, day, hour, minute, second, microsecond \\ {0, 0}) do
17+
%NaiveDateTime{
18+
year: year,
19+
month: month,
20+
day: day,
21+
hour: hour,
22+
minute: minute,
23+
second: second,
24+
microsecond: microsecond,
25+
calendar: __MODULE__
26+
}
27+
end
28+
29+
@impl true
30+
def date_to_string(year, month, day) do
31+
"#{year}-#{zero_pad(month, 2)}-#{zero_pad(day, 2)}"
32+
end
33+
34+
@impl true
35+
def naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) do
36+
"#{year}-#{zero_pad(month, 2)}-#{zero_pad(day, 2)}" <>
37+
Calendar.ISO.time_to_string(hour, minute, second, microsecond)
38+
end
39+
40+
@impl true
41+
def datetime_to_string(
42+
year,
43+
month,
44+
day,
45+
hour,
46+
minute,
47+
second,
48+
microsecond,
49+
_time_zone,
50+
zone_abbr,
51+
_utc_offset,
52+
_std_offset
53+
) do
54+
"#{year}-#{zero_pad(month, 2)}-#{zero_pad(day, 2)}" <>
55+
Calendar.ISO.time_to_string(hour, minute, second, microsecond) <>
56+
" #{zone_abbr}"
57+
end
58+
59+
@impl true
60+
def day_of_week(year, month, day, _starting_on) do
61+
{day_of_week, 1, 7} = Calendar.ISO.day_of_week(year, month, day, :sunday)
62+
if day_of_week == 1, do: {7, 7, 6}, else: {day_of_week - 1, 7, 6}
63+
end
64+
65+
@impl true
66+
defdelegate time_to_string(hour, minute, second, microsecond), to: Calendar.ISO
67+
68+
@impl true
69+
def day_rollover_relative_to_midnight_utc(), do: {0, 1}
70+
71+
@impl true
72+
def naive_datetime_from_iso_days(entry) do
73+
{year, month, day, hour, minute, second, microsecond} =
74+
Calendar.ISO.naive_datetime_from_iso_days(entry)
75+
76+
{year, month, day, hour, minute, second, microsecond}
77+
end
78+
79+
@impl true
80+
def naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) do
81+
Calendar.ISO.naive_datetime_to_iso_days(
82+
year,
83+
month,
84+
day,
85+
hour,
86+
minute,
87+
second,
88+
microsecond
89+
)
90+
end
91+
92+
defp zero_pad(val, count) when val >= 0 do
93+
String.pad_leading("#{val}", count, ["0"])
94+
end
95+
96+
defp zero_pad(val, count) do
97+
"-" <> zero_pad(-val, count)
98+
end
99+
100+
@impl true
101+
def parse_date(string) do
102+
{year, month, day} =
103+
string
104+
|> String.split("-")
105+
|> Enum.map(&String.to_integer/1)
106+
|> List.to_tuple()
107+
108+
if valid_date?(year, month, day) do
109+
{:ok, {year, month, day}}
110+
else
111+
{:error, :invalid_date}
112+
end
113+
end
114+
115+
@impl true
116+
def valid_date?(year, month, day) do
117+
:calendar.valid_date(year, month, day)
118+
end
119+
120+
@impl true
121+
defdelegate parse_time(string), to: Calendar.ISO
122+
123+
@impl true
124+
defdelegate parse_naive_datetime(string), to: Calendar.ISO
125+
126+
@impl true
127+
defdelegate parse_utc_datetime(string), to: Calendar.ISO
128+
129+
@impl true
130+
defdelegate time_from_day_fraction(day_fraction), to: Calendar.ISO
131+
132+
@impl true
133+
defdelegate time_to_day_fraction(hour, minute, second, microsecond), to: Calendar.ISO
134+
135+
@impl true
136+
defdelegate leap_year?(year), to: Calendar.ISO
137+
138+
@impl true
139+
defdelegate days_in_month(year, month), to: Calendar.ISO
140+
141+
@impl true
142+
defdelegate months_in_year(year), to: Calendar.ISO
143+
144+
@impl true
145+
defdelegate day_of_year(year, month, day), to: Calendar.ISO
146+
147+
@impl true
148+
defdelegate quarter_of_year(year, month, day), to: Calendar.ISO
149+
150+
@impl true
151+
defdelegate year_of_era(year, month, day), to: Calendar.ISO
152+
153+
@impl true
154+
defdelegate day_of_era(year, month, day), to: Calendar.ISO
155+
156+
@impl true
157+
defdelegate valid_time?(hour, minute, second, microsecond), to: Calendar.ISO
158+
159+
@impl true
160+
defdelegate iso_days_to_beginning_of_day(iso_days), to: Calendar.ISO
161+
162+
@impl true
163+
defdelegate iso_days_to_end_of_day(iso_days), to: Calendar.ISO
164+
165+
# The Holocene calendar extends most year and day count guards implemented in the ISO calendars.
166+
@impl true
167+
def shift_date(_year, _month, _day, _duration) do
168+
raise "shift_date/4 not implemented"
169+
end
170+
171+
@impl true
172+
def shift_naive_datetime(_year, _month, _day, _hour, _minute, _second, _microsecond, _duration) do
173+
raise "shift_naive_datetime/8 not implemented"
174+
end
175+
176+
@impl true
177+
def shift_time(_hour, _minute, _second, _microsecond, _duration) do
178+
raise "shift_time/5 not implemented"
179+
end
180+
end

0 commit comments

Comments
 (0)