Skip to content

Commit 18b7fef

Browse files
committed
ENH: Add year for week of year
This commit adds the year corresponding to the ISO 8601 week. For more information about ISO 8601 calculation of weeks and years, see e.g. https://en.wikipedia.org/wiki/ISO_week_date Address GH33206
1 parent f40fdf1 commit 18b7fef

File tree

9 files changed

+98
-0
lines changed

9 files changed

+98
-0
lines changed

doc/source/whatsnew/v1.1.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ Other enhancements
8787
- Positional slicing on a :class:`IntervalIndex` now supports slices with ``step > 1`` (:issue:`31658`)
8888
- :class:`Series.str` now has a `fullmatch` method that matches a regular expression against the entire string in each row of the series, similar to `re.fullmatch` (:issue:`32806`).
8989
- :meth:`DataFrame.sample` will now also allow array-like and BitGenerator objects to be passed to ``random_state`` as seeds (:issue:`32503`)
90+
- :class:`Series.dt` now has a `yearforweekofyear` accessor that returns the corresponding year for the iso week (:issue:`33206`).
91+
- :meth:`Timestamp.yearforweekofyear` returns the corresponding year for the iso week (:issue:`33206`).
9092
-
9193

9294
.. ---------------------------------------------------------------------------

pandas/_libs/tslibs/ccalendar.pxd

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ cdef int dayofweek(int y, int m, int d) nogil
77
cdef bint is_leapyear(int64_t year) nogil
88
cpdef int32_t get_days_in_month(int year, Py_ssize_t month) nogil
99
cpdef int32_t get_week_of_year(int year, int month, int day) nogil
10+
cpdef int32_t get_year_for_week_of_year(int year, int month, int day) nogil
1011
cpdef int32_t get_day_of_year(int year, int month, int day) nogil

pandas/_libs/tslibs/ccalendar.pyx

+37
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,43 @@ cpdef int32_t get_week_of_year(int year, int month, int day) nogil:
179179
return woy
180180

181181

182+
@cython.wraparound(False)
183+
@cython.boundscheck(False)
184+
cpdef int32_t get_year_for_week_of_year(int year, int month, int day) nogil:
185+
"""
186+
Return the year corresponding to the ISO 8601 calculation of week-of-year.
187+
188+
Parameters
189+
----------
190+
year : int
191+
month : int
192+
day : int
193+
194+
Returns
195+
-------
196+
year_for_week_of_year : int32_t
197+
198+
Notes
199+
-----
200+
Assumes the inputs describe a valid date.
201+
"""
202+
cdef:
203+
int32_t doy, woy
204+
int32_t year_woy
205+
206+
doy = get_day_of_year(year, month, day)
207+
woy = get_week_of_year(year, month, day)
208+
209+
year_woy = year
210+
if woy == 1 and doy > 10:
211+
year_woy += 1
212+
213+
elif woy >= 52 and doy < 10:
214+
year_woy -= 1
215+
216+
return year_woy
217+
218+
182219
@cython.wraparound(False)
183220
@cython.boundscheck(False)
184221
cpdef int32_t get_day_of_year(int year, int month, int day) nogil:

pandas/_libs/tslibs/fields.pyx

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ from pandas._libs.tslibs.ccalendar import (
1515
get_locale_names, MONTHS_FULL, DAYS_FULL, DAY_SECONDS)
1616
from pandas._libs.tslibs.ccalendar cimport (
1717
get_days_in_month, is_leapyear, dayofweek, get_week_of_year,
18+
get_year_for_week_of_year,
1819
get_day_of_year)
1920
from pandas._libs.tslibs.np_datetime cimport (
2021
npy_datetimestruct, pandas_timedeltastruct, dt64_to_dtstruct,
@@ -516,6 +517,17 @@ def get_date_field(const int64_t[:] dtindex, object field):
516517
out[i] = get_week_of_year(dts.year, dts.month, dts.day)
517518
return out
518519

520+
elif field == 'ywoy':
521+
with nogil:
522+
for i in range(count):
523+
if dtindex[i] == NPY_NAT:
524+
out[i] = -1
525+
continue
526+
527+
dt64_to_dtstruct(dtindex[i], &dts)
528+
out[i] = get_year_for_week_of_year(dts.year, dts.month, dts.day)
529+
return out
530+
519531
elif field == 'q':
520532
with nogil:
521533
for i in range(count):

pandas/_libs/tslibs/nattype.pyx

+1
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ class NaTType(_NaT):
374374
week = property(fget=lambda self: np.nan)
375375
dayofyear = property(fget=lambda self: np.nan)
376376
weekofyear = property(fget=lambda self: np.nan)
377+
yearforweekofyear = property(fget=lambda self: np.nan)
377378
days_in_month = property(fget=lambda self: np.nan)
378379
daysinmonth = property(fget=lambda self: np.nan)
379380
dayofweek = property(fget=lambda self: np.nan)

pandas/_libs/tslibs/timestamps.pyx

+7
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,13 @@ timedelta}, default 'raise'
731731

732732
weekofyear = week
733733

734+
@property
735+
def yearforweekofyear(self) -> int:
736+
"""
737+
Return the year for the ISO 8601 determination of week.
738+
"""
739+
return ccalendar.get_year_for_week_of_year(self.year, self.month, self.day)
740+
734741
@property
735742
def quarter(self) -> int:
736743
"""

pandas/core/arrays/datetimes.py

+8
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ class DatetimeArray(dtl.DatetimeLikeArrayMixin, dtl.TimelikeOps, dtl.DatelikeOps
173173
"second",
174174
"weekofyear",
175175
"week",
176+
"yearforweekofyear",
176177
"weekday",
177178
"dayofweek",
178179
"dayofyear",
@@ -1295,6 +1296,13 @@ def date(self):
12951296
""",
12961297
)
12971298
week = weekofyear
1299+
yearforweekofyear = _field_accessor(
1300+
"yearforweekofyear",
1301+
"ywoy",
1302+
"""
1303+
The ISO year corresponding to the ISO 8601 calculation of week
1304+
""",
1305+
)
12981306
_dayofweek_doc = """
12991307
The day of the week with Monday=0, Sunday=6.
13001308

pandas/tests/scalar/timestamp/test_timestamp.py

+6
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def check(value, equal):
8484
check(ts.quarter, 4)
8585
check(ts.dayofyear, 365)
8686
check(ts.week, 1)
87+
check(ts.yearforweekofyear, 2015)
8788
check(ts.daysinmonth, 31)
8889

8990
ts = Timestamp("2014-01-01 00:00:00+01:00")
@@ -155,26 +156,31 @@ def test_woy_boundary(self):
155156
result = Timestamp(d).week
156157
expected = 1 # ISO standard
157158
assert result == expected
159+
assert Timestamp(d).yearforweekofyear == 2014
158160

159161
d = datetime(2008, 12, 28)
160162
result = Timestamp(d).week
161163
expected = 52 # ISO standard
162164
assert result == expected
165+
assert Timestamp(d).yearforweekofyear == 2008
163166

164167
d = datetime(2009, 12, 31)
165168
result = Timestamp(d).week
166169
expected = 53 # ISO standard
167170
assert result == expected
171+
assert Timestamp(d).yearforweekofyear == 2009
168172

169173
d = datetime(2010, 1, 1)
170174
result = Timestamp(d).week
171175
expected = 53 # ISO standard
172176
assert result == expected
177+
assert Timestamp(d).yearforweekofyear == 2009
173178

174179
d = datetime(2010, 1, 3)
175180
result = Timestamp(d).week
176181
expected = 53 # ISO standard
177182
assert result == expected
183+
assert Timestamp(d).yearforweekofyear == 2009
178184

179185
result = np.array(
180186
[

pandas/tests/series/test_datetime_values.py

+24
Original file line numberDiff line numberDiff line change
@@ -665,3 +665,27 @@ def test_setitem_with_different_tz(self):
665665
dtype=object,
666666
)
667667
tm.assert_series_equal(ser, expected)
668+
669+
@pytest.mark.parametrize(
670+
"input_date, expected_week, expected_year",
671+
[
672+
["2020-01-01", 1, 2020],
673+
["2019-12-31", 1, 2020],
674+
["2019-12-30", 1, 2020],
675+
["2009-12-31", 53, 2009],
676+
["2010-01-01", 53, 2009],
677+
["2010-01-03", 53, 2009],
678+
["2010-01-04", 1, 2010],
679+
["2006-01-01", 52, 2005],
680+
["2005-12-31", 52, 2005],
681+
["2008-12-28", 52, 2008],
682+
["2008-12-29", 1, 2009],
683+
],
684+
)
685+
def test_dt_correct_iso_8601_week_and_year(
686+
self, input_date, expected_week, expected_year
687+
):
688+
689+
s = Series([pd.Timestamp(input_date)])
690+
tm.assert_series_equal(s.dt.week, Series([expected_week]))
691+
tm.assert_series_equal(s.dt.yearforweekofyear, Series([expected_year]))

0 commit comments

Comments
 (0)