diff options
author | Paul Ganssle <pganssle@users.noreply.github.com> | 2019-04-29 13:22:03 (GMT) |
---|---|---|
committer | Victor Stinner <vstinner@redhat.com> | 2019-04-29 13:22:03 (GMT) |
commit | 88c093705615c50c47fdd9ab976803f73de7e308 (patch) | |
tree | 4a76496a2c930d1e27e020555f4b91e2b43f777b /Lib | |
parent | a86e06433a010f873dfd7957e0f87a39539876ee (diff) | |
download | cpython-88c093705615c50c47fdd9ab976803f73de7e308.zip cpython-88c093705615c50c47fdd9ab976803f73de7e308.tar.gz cpython-88c093705615c50c47fdd9ab976803f73de7e308.tar.bz2 |
bpo-36004: Add date.fromisocalendar (GH-11888)
This commit implements the first version of date.fromisocalendar, the
inverse function for date.isocalendar.
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/datetime.py | 35 | ||||
-rw-r--r-- | Lib/test/datetimetester.py | 76 |
2 files changed, 111 insertions, 0 deletions
diff --git a/Lib/datetime.py b/Lib/datetime.py index 85bfa48..0e64815 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -884,6 +884,40 @@ class date: except Exception: raise ValueError(f'Invalid isoformat string: {date_string!r}') + @classmethod + def fromisocalendar(cls, year, week, day): + """Construct a date from the ISO year, week number and weekday. + + This is the inverse of the date.isocalendar() function""" + # Year is bounded this way because 9999-12-31 is (9999, 52, 5) + if not MINYEAR <= year <= MAXYEAR: + raise ValueError(f"Year is out of range: {year}") + + if not 0 < week < 53: + out_of_range = True + + if week == 53: + # ISO years have 53 weeks in them on years starting with a + # Thursday and leap years starting on a Wednesday + first_weekday = _ymd2ord(year, 1, 1) % 7 + if (first_weekday == 4 or (first_weekday == 3 and + _is_leap(year))): + out_of_range = False + + if out_of_range: + raise ValueError(f"Invalid week: {week}") + + if not 0 < day < 8: + raise ValueError(f"Invalid weekday: {day} (range is [1, 7])") + + # Now compute the offset from (Y, 1, 1) in days: + day_offset = (week - 1) * 7 + (day - 1) + + # Calculate the ordinal day for monday, week 1 + day_1 = _isoweek1monday(year) + ord_day = day_1 + day_offset + + return cls(*_ord2ymd(ord_day)) # Conversions to string @@ -2141,6 +2175,7 @@ def _isoweek1monday(year): week1monday += 7 return week1monday + class timezone(tzinfo): __slots__ = '_offset', '_name' diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 617bf9a..9fe32eb 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1795,6 +1795,82 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase): with self.assertRaises(TypeError): self.theclass.fromisoformat(bad_type) + def test_fromisocalendar(self): + # For each test case, assert that fromisocalendar is the + # inverse of the isocalendar function + dates = [ + (2016, 4, 3), + (2005, 1, 2), # (2004, 53, 7) + (2008, 12, 30), # (2009, 1, 2) + (2010, 1, 2), # (2009, 53, 6) + (2009, 12, 31), # (2009, 53, 4) + (1900, 1, 1), # Unusual non-leap year (year % 100 == 0) + (1900, 12, 31), + (2000, 1, 1), # Unusual leap year (year % 400 == 0) + (2000, 12, 31), + (2004, 1, 1), # Leap year + (2004, 12, 31), + (1, 1, 1), + (9999, 12, 31), + (MINYEAR, 1, 1), + (MAXYEAR, 12, 31), + ] + + for datecomps in dates: + with self.subTest(datecomps=datecomps): + dobj = self.theclass(*datecomps) + isocal = dobj.isocalendar() + + d_roundtrip = self.theclass.fromisocalendar(*isocal) + + self.assertEqual(dobj, d_roundtrip) + + def test_fromisocalendar_value_errors(self): + isocals = [ + (2019, 0, 1), + (2019, -1, 1), + (2019, 54, 1), + (2019, 1, 0), + (2019, 1, -1), + (2019, 1, 8), + (2019, 53, 1), + (10000, 1, 1), + (0, 1, 1), + (9999999, 1, 1), + (2<<32, 1, 1), + (2019, 2<<32, 1), + (2019, 1, 2<<32), + ] + + for isocal in isocals: + with self.subTest(isocal=isocal): + with self.assertRaises(ValueError): + self.theclass.fromisocalendar(*isocal) + + def test_fromisocalendar_type_errors(self): + err_txformers = [ + str, + float, + lambda x: None, + ] + + # Take a valid base tuple and transform it to contain one argument + # with the wrong type. Repeat this for each argument, e.g. + # [("2019", 1, 1), (2019, "1", 1), (2019, 1, "1"), ...] + isocals = [] + base = (2019, 1, 1) + for i in range(3): + for txformer in err_txformers: + err_val = list(base) + err_val[i] = txformer(err_val[i]) + isocals.append(tuple(err_val)) + + for isocal in isocals: + with self.subTest(isocal=isocal): + with self.assertRaises(TypeError): + self.theclass.fromisocalendar(*isocal) + + ############################################################################# # datetime tests |