Skip to content

bpo-24416: Return IsoCalendarDate from date.isocalendar() #20113

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

Merged
merged 3 commits into from
May 16, 2020
Merged
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
20 changes: 12 additions & 8 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,8 @@ Instance methods:

.. method:: date.isocalendar()

Return a 3-tuple, (ISO year, ISO week number, ISO weekday).
Return a :term:`named tuple` object with three components: ``year``,
``week`` and ``weekday``.

The ISO calendar is a widely used variant of the Gregorian calendar. [#]_

Expand All @@ -682,11 +683,14 @@ Instance methods:
For example, 2004 begins on a Thursday, so the first week of ISO year 2004
begins on Monday, 29 Dec 2003 and ends on Sunday, 4 Jan 2004::

>>> from datetime import date
>>> date(2003, 12, 29).isocalendar()
(2004, 1, 1)
>>> date(2004, 1, 4).isocalendar()
(2004, 1, 7)
>>> from datetime import date
>>> date(2003, 12, 29).isocalendar()
datetime.IsoCalendarDate(year=2004, week=1, weekday=1)
>>> date(2004, 1, 4).isocalendar()
datetime.IsoCalendarDate(year=2004, week=1, weekday=7)

.. versionchanged:: 3.9
Result changed from a tuple to a :term:`named tuple`.

.. method:: date.isoformat()

Expand Down Expand Up @@ -1397,8 +1401,8 @@ Instance methods:

.. method:: datetime.isocalendar()

Return a 3-tuple, (ISO year, ISO week number, ISO weekday). The same as
``self.date().isocalendar()``.
Return a :term:`named tuple` with three components: ``year``, ``week``
and ``weekday``. The same as ``self.date().isocalendar()``.


.. method:: datetime.isoformat(sep='T', timespec='auto')
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,13 @@ Add :func:`curses.get_escdelay`, :func:`curses.set_escdelay`,
:func:`curses.get_tabsize`, and :func:`curses.set_tabsize` functions.
(Contributed by Anthony Sottile in :issue:`38312`.)

datetime
--------
The :meth:`~datetime.date.isocalendar()` of :class:`datetime.date`
and :meth:`~datetime.datetime.isocalendar()` of :class:`datetime.datetime`
methods now returns a :func:`~collections.namedtuple` instead of a :class:`tuple`.
(Contributed by Dong-hee Na in :issue:`24416`.)

fcntl
-----

Expand Down
37 changes: 34 additions & 3 deletions Lib/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1095,7 +1095,7 @@ def isoweekday(self):
return self.toordinal() % 7 or 7

def isocalendar(self):
"""Return a 3-tuple containing ISO year, week number, and weekday.
"""Return a named tuple containing ISO year, week number, and weekday.

The first ISO week of the year is the (Mon-Sun) week
containing the year's first Thursday; everything else derives
Expand All @@ -1120,7 +1120,7 @@ def isocalendar(self):
if today >= _isoweek1monday(year+1):
year += 1
week = 0
return year, week+1, day+1
return _IsoCalendarDate(year, week+1, day+1)

# Pickle support.

Expand Down Expand Up @@ -1210,6 +1210,36 @@ def __reduce__(self):
else:
return (self.__class__, args, state)


class IsoCalendarDate(tuple):

def __new__(cls, year, week, weekday, /):
return super().__new__(cls, (year, week, weekday))

@property
def year(self):
return self[0]

@property
def week(self):
return self[1]

@property
def weekday(self):
return self[2]

def __reduce__(self):
# This code is intended to pickle the object without making the
# class public. See https://bugs.python.org/msg352381
return (tuple, (tuple(self),))

def __repr__(self):
return (f'{self.__class__.__name__}'
f'(year={self[0]}, week={self[1]}, weekday={self[2]})')


_IsoCalendarDate = IsoCalendarDate
del IsoCalendarDate
_tzinfo_class = tzinfo

class time:
Expand Down Expand Up @@ -1559,6 +1589,7 @@ def __reduce__(self):
time.max = time(23, 59, 59, 999999)
time.resolution = timedelta(microseconds=1)


class datetime(date):
"""datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]])

Expand Down Expand Up @@ -2514,7 +2545,7 @@ def _name_from_offset(delta):
_format_time, _format_offset, _is_leap, _isoweek1monday, _math,
_ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord,
_divide_and_round, _parse_isoformat_date, _parse_isoformat_time,
_parse_hh_mm_ss_ff)
_parse_hh_mm_ss_ff, _IsoCalendarDate)
# XXX Since import * above excludes names that start with _,
# docstring does not get overwritten. In the future, it may be
# appropriate to maintain a single module level docstring and
Expand Down
51 changes: 38 additions & 13 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

See http://www.zope.org/Members/fdrake/DateTimeUncyclo/TestCases
"""
import io
import itertools
import bisect
import copy
Expand Down Expand Up @@ -1355,19 +1356,43 @@ def test_weekday(self):
def test_isocalendar(self):
# Check examples from
# http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm
for i in range(7):
d = self.theclass(2003, 12, 22+i)
self.assertEqual(d.isocalendar(), (2003, 52, i+1))
d = self.theclass(2003, 12, 29) + timedelta(i)
self.assertEqual(d.isocalendar(), (2004, 1, i+1))
d = self.theclass(2004, 1, 5+i)
self.assertEqual(d.isocalendar(), (2004, 2, i+1))
d = self.theclass(2009, 12, 21+i)
self.assertEqual(d.isocalendar(), (2009, 52, i+1))
d = self.theclass(2009, 12, 28) + timedelta(i)
self.assertEqual(d.isocalendar(), (2009, 53, i+1))
d = self.theclass(2010, 1, 4+i)
self.assertEqual(d.isocalendar(), (2010, 1, i+1))
week_mondays = [
((2003, 12, 22), (2003, 52, 1)),
((2003, 12, 29), (2004, 1, 1)),
((2004, 1, 5), (2004, 2, 1)),
((2009, 12, 21), (2009, 52, 1)),
((2009, 12, 28), (2009, 53, 1)),
((2010, 1, 4), (2010, 1, 1)),
]

test_cases = []
for cal_date, iso_date in week_mondays:
base_date = self.theclass(*cal_date)
# Adds one test case for every day of the specified weeks
for i in range(7):
new_date = base_date + timedelta(i)
new_iso = iso_date[0:2] + (iso_date[2] + i,)
test_cases.append((new_date, new_iso))

for d, exp_iso in test_cases:
with self.subTest(d=d, comparison="tuple"):
self.assertEqual(d.isocalendar(), exp_iso)

# Check that the tuple contents are accessible by field name
with self.subTest(d=d, comparison="fields"):
t = d.isocalendar()
self.assertEqual((t.year, t.week, t.weekday), exp_iso)

def test_isocalendar_pickling(self):
"""Test that the result of datetime.isocalendar() can be pickled.

The result of a round trip should be a plain tuple.
"""
d = self.theclass(2019, 1, 1)
p = pickle.dumps(d.isocalendar())
res = pickle.loads(p)
self.assertEqual(type(res), tuple)
self.assertEqual(res, (2019, 1, 2))

def test_iso_long_years(self):
# Calculate long ISO years and compare to table from
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The ``isocalendar()`` methods of :class:`datetime.date` and
:class:`datetime.datetime` now return a :term:`named tuple`
instead of a :class:`tuple`.
Loading