GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — bpo-36004: Add date.fromisocalendar by pganssle · Pull Request #11888 · python/cpython

bpo-36004: Add date.fromisocalendar #11888

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.

Sign up for GitHub

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
vstinner merged 14 commits into python:master from pganssle:add_fromisoformat
Apr 29, 2019

Uh oh!

There was an error while loading. Please reload this page.

Merged

bpo-36004: Add date.fromisocalendar #11888

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
6d323ad
Add date.fromisocalendar and tests
pganssle Feb 15, 2019
fbab728
Add early year check to fromisocalendar
pganssle Feb 17, 2019
925de2f
Covert OverflowError into ValueError
pganssle Feb 17, 2019
3ecf02e
Add bounds checking for ISO long vs. short weeks
pganssle Feb 17, 2019
e645cd0
Add more comments with test cases
pganssle Feb 17, 2019
86edee5
Switch to new method of detecting ISO long years
pganssle Feb 17, 2019
74fb523
Fix missing argument in PyErr_Format
pganssle Feb 17, 2019
46c6458
Add test for TypeError handling
pganssle Feb 17, 2019
28c7fb2
Add documentation for date(time).fromisocalendar
pganssle Feb 17, 2019
99e75d7
Add NEWS entry
pganssle Feb 17, 2019
c269a5c
Add What's New entry for fromisocalendar
pganssle Mar 17, 2019
c8444b7
Use MINYEAR and MAXYEAR in bounds checks
pganssle Apr 29, 2019
958ffff
Add tests for MINYEAR and MAXYEAR
pganssle Apr 29, 2019
3307865
Use generated test cases instead of manual list
pganssle Apr 29, 2019
File filter
Filter by extension

Clear filters
Conversations
Failed to load comments.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Jump to
Jump to file
Failed to load files.
Loading

Uh oh!

There was an error while loading. Please reload this page.

Diff view
Diff view

There are no files selected for viewing

17 changes: 17 additions & 0 deletions 17 Doc/library/datetime.rst
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,13 @@ Other constructors, all class methods:
.. 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:

Expand Down Expand Up @@ -854,6 +861,16 @@ Other constructors, all class methods:

.. 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)

Return a :class:`.datetime` corresponding to *date_string*, parsed according to
Expand Down
10 changes: 10 additions & 0 deletions 10 Doc/whatsnew/3.8.rst
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,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`.


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
-------

Expand Down
35 changes: 35 additions & 0 deletions 35 Lib/datetime.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,40 @@ def fromisoformat(cls, date_string):
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

Expand Down Expand Up @@ -2141,6 +2175,7 @@ def _isoweek1monday(year):
week1monday += 7
return week1monday


class timezone(tzinfo):
__slots__ = '_offset', '_name'

Expand Down
76 changes: 76 additions & 0 deletions 76 Lib/test/datetimetester.py
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -1795,6 +1795,82 @@ def test_fromisoformat_fails_typeerror(self):
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),
Comment thread Outdated

@vstinner vstinner Apr 28, 2019

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use MINYEAR/MAXYEAR here.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@pganssle pganssle Apr 29, 2019

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of converting these over, I have just added a MINYEAR and MAXYEAR test. Even though it's redundant, I like to have the explicit callout of the current boundaries, so that if MINYEAR or MAXYEAR get modified in a way that breaks backwards compatibility, it will raise an error.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
(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,
]
Comment thread Outdated

@vstinner vstinner Apr 28, 2019

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to use 3 loops to test all combinations, rather than generate these combinations manualy?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@pganssle pganssle Apr 29, 2019

Copy link
Copy Markdown
Member Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I've converted it over. I think it's a little harder to understand what's going on in the version where the test cases are generated (compared to manual), but using a loop also has its advantages.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

# 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

Expand Down
4 changes: 4 additions & 0 deletions 4 Misc/NEWS.d/next/Library/2019-02-17-12-55-51.bpo-36004.hCt_KK.rst
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
@@ -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.
67 changes: 67 additions & 0 deletions 67 Modules/_datetimemodule.c
Show comments
View file
Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -3003,6 +3003,67 @@ date_fromisoformat(PyObject *cls, PyObject *dtstr)
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.
*/
Expand Down Expand Up @@ -3296,6 +3357,12 @@ static PyMethodDef date_methods[] = {
METH_CLASS,
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,
PyDoc_STR("Current date or datetime: same as "
"self.__class__.fromtimestamp(time.time()).")},
Expand Down
Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.