Skip to content

Commit f6dde2e

Browse files
isentropicknueselinkydragonViralBShahadienes
authored
add ISO year / ISO week utilities (#48507)
Based on some discussion in https://discourse.julialang.org/t/determine-date-range-given-year-and-week-number/93878 and the relevant issue #48490 I created a couple of utilities to help handle 2023-W05-4 (YYYY-WWW-D) dateformats (https://en.wikipedia.org/wiki/ISO_week_date) Currently there is no functionality of ISO Week with weekday | 2023-W05-4 in Julia, but this could be good start and could help a lot of people who require such dateformat. Notably, with `firstmondayofyear` it is trivial to convert from week format 2023-W05-4 to usual date format 2023-02-02 as: ``` firstmondayofyear(Date(2023)) + Week(5 - 1) + Day(3) ``` This was otherwise impossible within std lib --------- Co-authored-by: Jeremie Knuesel <[email protected]> Co-authored-by: woclass <[email protected]> Co-authored-by: Viral B. Shah <[email protected]> Co-authored-by: Andy Dienes <[email protected]>
1 parent 857e017 commit f6dde2e

File tree

3 files changed

+140
-0
lines changed

3 files changed

+140
-0
lines changed

stdlib/Dates/src/Dates.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export Period, DatePeriod, TimePeriod,
6060
yearmonthday, yearmonth, monthday, year, month, week, day,
6161
hour, minute, second, millisecond, dayofmonth,
6262
microsecond, nanosecond,
63+
isoweekdate, isoyear, weeksinyear,
6364
# query.jl
6465
dayofweek, isleapyear, daysinmonth, daysinyear, dayofyear, dayname, dayabbr,
6566
dayofweekofmonth, daysofweekinmonth, monthname, monthabbr,

stdlib/Dates/src/accessors.jl

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ function day(days)
3232
y = fld(100b + h, 36525); c = b + z - 365y - fld(y, 4); m = div(5c + 456, 153)
3333
return c - div(153m - 457, 5)
3434
end
35+
36+
# ISO year utils
3537
# https://en.wikipedia.org/wiki/Talk:ISO_week_date#Algorithms
3638
const WEEK_INDEX = (15, 23, 3, 11)
3739
function week(days)
@@ -41,6 +43,80 @@ function week(days)
4143
return div(w, 28) + 1
4244
end
4345

46+
"""
47+
Return the number of ISO weeks in the given year (see https://en.wikipedia.org/wiki/ISO_week_date).
48+
49+
# Examples
50+
```jldoctest
51+
julia> weeksinyear(Year(2022))
52+
52
53+
54+
julia> weeksinyear(Year(2020))
55+
53
56+
```
57+
!!! compat "Julia 1.13"
58+
This function requires Julia 1.13 or later.
59+
"""
60+
function weeksinyear(y::Year)
61+
firstday = firstdayofyear(Date(y))
62+
lastday = lastdayofyear(Date(y))
63+
64+
if dayofweek(firstday) == 4 || dayofweek(lastday) == 4
65+
return 53
66+
end
67+
return 52
68+
end
69+
70+
"""
71+
Return the ISO year that contains `dt` (see https://en.wikipedia.org/wiki/ISO_week_date).
72+
73+
# Examples
74+
```jldoctest
75+
julia> isoyear(Date(2022, 1, 1))
76+
2021 years
77+
78+
julia> isoyear(Date(2021, 12, 31))
79+
2021 years
80+
```
81+
!!! compat "Julia 1.13"
82+
This function requires Julia 1.13 or later.
83+
"""
84+
function isoyear(dt::DateTime)
85+
thisyear = Year(dt)
86+
thismonth = Month(dt)
87+
weeknumber = week(dt)
88+
if weeknumber >= 52 && thismonth.value == 1
89+
# If it is january, then its the iso year from before
90+
return Year(thisyear.value - 1)
91+
elseif weeknumber == 1 && thismonth.value == 12
92+
# If it is december, then its the next year
93+
return Year(thisyear.value + 1)
94+
else
95+
return thisyear
96+
end
97+
end
98+
isoyear(dt::Date) = isoyear(DateTime(dt))
99+
100+
"""
101+
Return the ISO week date that corresponds to `dt` (see
102+
https://en.wikipedia.org/wiki/ISO_week_date).
103+
104+
The return type is a tuple of `Year`, `Week` and `Int64` (from 1 to 7).
105+
106+
# Examples
107+
```jldoctest
108+
julia> isoweekdate(Date(2023, 03, 06))
109+
(2023, 10, 1)
110+
111+
julia> isoweekdate(Date(2023, 01, 01))
112+
(2022, 52, 7)
113+
```
114+
!!! compat "Julia 1.13"
115+
This function requires Julia 1.13 or later.
116+
"""
117+
isoweekdate(dt::DateTime) = (isoyear(dt).value, week(dt), dayofweek(dt))
118+
isoweekdate(dt::Date) = isoweekdate(DateTime(dt))
119+
44120
function quarter(days)
45121
m = month(days)
46122
return m < 4 ? 1 : m < 7 ? 2 : m < 10 ? 3 : 4

stdlib/Dates/test/accessors.jl

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,69 @@ end
192192
dt1 = dt1 + Dates.Day(1)
193193
end
194194
end
195+
@testset "ISO year utils" begin
196+
# Tests from https://www.epochconverter.com/weeks
197+
@test Dates.weeksinyear(Dates.Year(2023)) == 52
198+
@test Dates.weeksinyear(Dates.Year(2022)) == 52
199+
@test Dates.weeksinyear(Dates.Year(2021)) == 52
200+
@test Dates.weeksinyear(Dates.Year(2020)) == 53
201+
@test Dates.weeksinyear(Dates.Year(2019)) == 52
202+
@test Dates.weeksinyear(Dates.Year(2018)) == 52
203+
@test Dates.weeksinyear(Dates.Year(2017)) == 52
204+
@test Dates.weeksinyear(Dates.Year(2016)) == 52
205+
@test Dates.weeksinyear(Dates.Year(2015)) == 53
206+
@test Dates.weeksinyear(Dates.Year(2014)) == 52
207+
@test Dates.weeksinyear(Dates.Year(2013)) == 52
208+
@test Dates.weeksinyear(Dates.Year(2012)) == 52
209+
@test Dates.weeksinyear(Dates.Year(2011)) == 52
210+
@test Dates.weeksinyear(Dates.Year(2010)) == 52
211+
@test Dates.weeksinyear(Dates.Year(2009)) == 53
212+
213+
# From python datetime isocalendar
214+
@test Dates.isoweekdate(Dates.Date(2023, 03, 06)) == (2023, 10, 1)
215+
@test Dates.isoweekdate(Dates.Date(2023, 03, 07)) == (2023, 10, 2)
216+
@test Dates.isoweekdate(Dates.Date(2023, 03, 08)) == (2023, 10, 3)
217+
@test Dates.isoweekdate(Dates.Date(2022, 12, 29)) == (2022, 52, 4)
218+
@test Dates.isoweekdate(Dates.Date(2022, 12, 30)) == (2022, 52, 5)
219+
@test Dates.isoweekdate(Dates.Date(2022, 12, 31)) == (2022, 52, 6)
220+
@test Dates.isoweekdate(Dates.Date(2023, 01, 01)) == (2022, 52, 7)
221+
@test Dates.isoweekdate(Dates.Date(2023, 01, 02)) == (2023, 1, 1)
222+
@test Dates.isoweekdate(Dates.Date(2023, 01, 03)) == (2023, 1, 2)
223+
@test Dates.isoweekdate(Dates.Date(2021, 12, 28)) == (2021, 52, 2)
224+
@test Dates.isoweekdate(Dates.Date(2021, 12, 29)) == (2021, 52, 3)
225+
@test Dates.isoweekdate(Dates.Date(2021, 12, 30)) == (2021, 52, 4)
226+
@test Dates.isoweekdate(Dates.Date(2021, 12, 31)) == (2021, 52, 5)
227+
@test Dates.isoweekdate(Dates.Date(2022, 01, 01)) == (2021, 52, 6)
228+
@test Dates.isoweekdate(Dates.Date(2022, 01, 02)) == (2021, 52, 7)
229+
@test Dates.isoweekdate(Dates.Date(2022, 01, 03)) == (2022, 1, 1)
230+
@test Dates.isoweekdate(Dates.Date(2022, 01, 04)) == (2022, 1, 2)
231+
@test Dates.isoweekdate(Dates.Date(2022, 01, 05)) == (2022, 1, 3)
232+
@test Dates.isoweekdate(Dates.Date(2022, 01, 06)) == (2022, 1, 4)
233+
@test Dates.isoweekdate(Dates.Date(2020, 12, 29)) == (2020, 53, 2)
234+
@test Dates.isoweekdate(Dates.Date(2020, 12, 30)) == (2020, 53, 3)
235+
@test Dates.isoweekdate(Dates.Date(2020, 12, 31)) == (2020, 53, 4)
236+
@test Dates.isoweekdate(Dates.Date(2021, 01, 01)) == (2020, 53, 5)
237+
@test Dates.isoweekdate(Dates.Date(2021, 01, 02)) == (2020, 53, 6)
238+
@test Dates.isoweekdate(Dates.Date(2021, 01, 03)) == (2020, 53, 7)
239+
@test Dates.isoweekdate(Dates.Date(2021, 01, 04)) == (2021, 1, 1)
240+
@test Dates.isoweekdate(Dates.Date(2021, 01, 05)) == (2021, 1, 2)
241+
@test Dates.isoweekdate(Dates.Date(2021, 12, 31)) == (2021, 52, 5)
242+
@test Dates.isoweekdate(Dates.Date(2022, 01, 01)) == (2021, 52, 6)
243+
@test Dates.isoweekdate(Dates.Date(2022, 01, 02)) == (2021, 52, 7)
244+
@test Dates.isoweekdate(Dates.Date(2020, 12, 31)) == (2020, 53, 4)
245+
@test Dates.isoweekdate(Dates.Date(2021, 01, 01)) == (2020, 53, 5)
246+
@test Dates.isoweekdate(Dates.Date(2021, 01, 02)) == (2020, 53, 6)
247+
@test Dates.isoweekdate(Dates.Date(2021, 12, 31)) == (2021, 52, 5)
248+
@test Dates.isoweekdate(Dates.Date(2022, 01, 01)) == (2021, 52, 6)
249+
@test Dates.isoweekdate(Dates.Date(2022, 01, 02)) == (2021, 52, 7)
250+
@test Dates.isoweekdate(Dates.Date(2022, 01, 03)) == (2022, 1, 1)
251+
@test Dates.isoweekdate(Dates.Date(2019, 12, 31)) == (2020, 1, 2)
252+
@test Dates.isoweekdate(Dates.Date(2020, 01, 01)) == (2020, 1, 3)
253+
@test Dates.isoweekdate(Dates.Date(2020, 01, 02)) == (2020, 1, 4)
254+
@test Dates.isoweekdate(Dates.Date(2018, 12, 31)) == (2019, 1, 1)
255+
@test Dates.isoweekdate(Dates.Date(2019, 01, 01)) == (2019, 1, 2)
256+
@test Dates.isoweekdate(Dates.Date(2019, 01, 02)) == (2019, 1, 3)
257+
end
195258
@testset "Vectorized accessors" begin
196259
a = Dates.Date(2014, 1, 1)
197260
dr = [a, a, a, a, a, a, a, a, a, a]

0 commit comments

Comments
 (0)