bpo-36004: Add date.fromisocalendar (GH-11888)
This commit implements the first version of date.fromisocalendar, the inverse function for date.isocalendar.
This commit is contained in:
parent
a86e06433a
commit
88c0937056
|
@ -458,6 +458,13 @@ Other constructors, all class methods:
|
||||||
.. versionadded:: 3.7
|
.. versionadded:: 3.7
|
||||||
|
|
||||||
|
|
||||||
|
.. classmethod:: date.fromisocalendar(year, week, day)
|
||||||
|
|
||||||
|
Return a :class:`date` corresponding to the ISO calendar date specified by
|
||||||
|
year, week and day. This is the inverse of the function :meth:`date.isocalendar`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.8
|
||||||
|
|
||||||
|
|
||||||
Class attributes:
|
Class attributes:
|
||||||
|
|
||||||
|
@ -854,6 +861,16 @@ Other constructors, all class methods:
|
||||||
|
|
||||||
.. versionadded:: 3.7
|
.. versionadded:: 3.7
|
||||||
|
|
||||||
|
|
||||||
|
.. classmethod:: datetime.fromisocalendar(year, week, day)
|
||||||
|
|
||||||
|
Return a :class:`datetime` corresponding to the ISO calendar date specified
|
||||||
|
by year, week and day. The non-date components of the datetime are populated
|
||||||
|
with their normal default values. This is the inverse of the function
|
||||||
|
:meth:`datetime.isocalendar`.
|
||||||
|
|
||||||
|
.. versionadded:: 3.8
|
||||||
|
|
||||||
.. classmethod:: datetime.strptime(date_string, format)
|
.. classmethod:: datetime.strptime(date_string, format)
|
||||||
|
|
||||||
Return a :class:`.datetime` corresponding to *date_string*, parsed according to
|
Return a :class:`.datetime` corresponding to *date_string*, parsed according to
|
||||||
|
|
|
@ -244,6 +244,16 @@ where the DLL is stored (if a full or partial path is used to load the initial
|
||||||
DLL) and paths added by :func:`~os.add_dll_directory`.
|
DLL) and paths added by :func:`~os.add_dll_directory`.
|
||||||
|
|
||||||
|
|
||||||
|
datetime
|
||||||
|
--------
|
||||||
|
|
||||||
|
Added new alternate constructors :meth:`datetime.date.fromisocalendar` and
|
||||||
|
:meth:`datetime.datetime.fromisocalendar`, which construct :class:`date` and
|
||||||
|
:class:`datetime` objects respectively from ISO year, week number and weekday;
|
||||||
|
these are the inverse of each class's ``isocalendar`` method.
|
||||||
|
(Contributed by Paul Ganssle in :issue:`36004`.)
|
||||||
|
|
||||||
|
|
||||||
gettext
|
gettext
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
@ -884,6 +884,40 @@ class date:
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ValueError(f'Invalid isoformat string: {date_string!r}')
|
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
|
# Conversions to string
|
||||||
|
|
||||||
|
@ -2141,6 +2175,7 @@ def _isoweek1monday(year):
|
||||||
week1monday += 7
|
week1monday += 7
|
||||||
return week1monday
|
return week1monday
|
||||||
|
|
||||||
|
|
||||||
class timezone(tzinfo):
|
class timezone(tzinfo):
|
||||||
__slots__ = '_offset', '_name'
|
__slots__ = '_offset', '_name'
|
||||||
|
|
||||||
|
|
|
@ -1795,6 +1795,82 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
self.theclass.fromisoformat(bad_type)
|
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
|
# datetime tests
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Added new alternate constructors :meth:`datetime.date.fromisocalendar` and
|
||||||
|
:meth:`datetime.datetime.fromisocalendar`, which construct date objects from
|
||||||
|
ISO year, week number and weekday; these are the inverse of each class's
|
||||||
|
``isocalendar`` method. Patch by Paul Ganssle.
|
|
@ -3003,6 +3003,67 @@ invalid_string_error:
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
date_fromisocalendar(PyObject *cls, PyObject *args, PyObject *kw)
|
||||||
|
{
|
||||||
|
static char *keywords[] = {
|
||||||
|
"year", "week", "day", NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
int year, week, day;
|
||||||
|
if (PyArg_ParseTupleAndKeywords(args, kw, "iii:fromisocalendar",
|
||||||
|
keywords,
|
||||||
|
&year, &week, &day) == 0) {
|
||||||
|
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"ISO calendar component out of range");
|
||||||
|
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Year is bounded to 0 < year < 10000 because 9999-12-31 is (9999, 52, 5)
|
||||||
|
if (year < MINYEAR || year > MAXYEAR) {
|
||||||
|
PyErr_Format(PyExc_ValueError, "Year is out of range: %d", year);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (week <= 0 || week >= 53) {
|
||||||
|
int out_of_range = 1;
|
||||||
|
if (week == 53) {
|
||||||
|
// ISO years have 53 weeks in it on years starting with a Thursday
|
||||||
|
// and on leap years starting on Wednesday
|
||||||
|
int first_weekday = weekday(year, 1, 1);
|
||||||
|
if (first_weekday == 3 || (first_weekday == 2 && is_leap(year))) {
|
||||||
|
out_of_range = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out_of_range) {
|
||||||
|
PyErr_Format(PyExc_ValueError, "Invalid week: %d", week);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (day <= 0 || day >= 8) {
|
||||||
|
PyErr_Format(PyExc_ValueError, "Invalid day: %d (range is [1, 7])",
|
||||||
|
day);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert (Y, W, D) to (Y, M, D) in-place
|
||||||
|
int day_1 = iso_week1_monday(year);
|
||||||
|
|
||||||
|
int month = week;
|
||||||
|
int day_offset = (month - 1)*7 + day - 1;
|
||||||
|
|
||||||
|
ord_to_ymd(day_1 + day_offset, &year, &month, &day);
|
||||||
|
|
||||||
|
return new_date_subclass_ex(year, month, day, cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Date arithmetic.
|
* Date arithmetic.
|
||||||
*/
|
*/
|
||||||
|
@ -3296,6 +3357,12 @@ static PyMethodDef date_methods[] = {
|
||||||
METH_CLASS,
|
METH_CLASS,
|
||||||
PyDoc_STR("str -> Construct a date from the output of date.isoformat()")},
|
PyDoc_STR("str -> Construct a date from the output of date.isoformat()")},
|
||||||
|
|
||||||
|
{"fromisocalendar", (PyCFunction)(void(*)(void))date_fromisocalendar,
|
||||||
|
METH_VARARGS | METH_KEYWORDS | METH_CLASS,
|
||||||
|
PyDoc_STR("int, int, int -> Construct a date from the ISO year, week "
|
||||||
|
"number and weekday.\n\n"
|
||||||
|
"This is the inverse of the date.isocalendar() function")},
|
||||||
|
|
||||||
{"today", (PyCFunction)date_today, METH_NOARGS | METH_CLASS,
|
{"today", (PyCFunction)date_today, METH_NOARGS | METH_CLASS,
|
||||||
PyDoc_STR("Current date or datetime: same as "
|
PyDoc_STR("Current date or datetime: same as "
|
||||||
"self.__class__.fromtimestamp(time.time()).")},
|
"self.__class__.fromtimestamp(time.time()).")},
|
||||||
|
|
Loading…
Reference in New Issue