Skip to content
Open
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
97 changes: 39 additions & 58 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,67 +90,48 @@ def _ymd2ord(year, month, day):
# pasting together 25 4-year cycles.
assert _DI100Y == 25 * _DI4Y - 1

def _ord2ymd(n):
def _ord2ymd(ordinal):
"ordinal -> (year, month, day), considering 01-Jan-0001 as day 1."

# n is a 1-based index, starting at 1-Jan-1. The pattern of leap years
# repeats exactly every 400 years. The basic strategy is to find the
# closest 400-year boundary at or before n, then work with the offset
# from that boundary to n. Life is much clearer if we subtract 1 from
# n first -- then the values of n at 400-year boundaries are exactly
# those divisible by _DI400Y:
# This follows Neri and Schneider, "Euclidean affine functions and their
# application to calendar algorithms", Softw Pract Exper.
# 2023;53(4):937-970, https://doi.org/10.1002/spe.3172.
# ord_to_ymd() in Modules/_datetimemodule.c does the same steps.
#
# D M Y n n-1
# -- --- ---- ---------- ----------------
# 31 Dec -400 -_DI400Y -_DI400Y -1
# 1 Jan -399 -_DI400Y +1 -_DI400Y 400-year boundary
# ...
# 30 Dec 000 -1 -2
# 31 Dec 000 0 -1
# 1 Jan 001 1 0 400-year boundary
# 2 Jan 001 2 1
# 3 Jan 001 3 2
# ...
# 31 Dec 400 _DI400Y _DI400Y -1
# 1 Jan 401 _DI400Y +1 _DI400Y 400-year boundary
n -= 1
n400, n = divmod(n, _DI400Y)
year = n400 * 400 + 1 # ..., -399, 1, 401, ...

# Now n is the (non-negative) offset, in days, from January 1 of year, to
# the desired date. Now compute how many 100-year cycles precede n.
# Note that it's possible for n100 to equal 4! In that case 4 full
# 100-year cycles precede the desired day, which implies the desired
# day is December 31 at the end of a 400-year cycle.
n100, n = divmod(n, _DI100Y)

# Now compute how many 4-year cycles precede it.
n4, n = divmod(n, _DI4Y)

# And now how many single years. Again n1 can be 4, and again meaning
# that the desired day is December 31 at the end of the 4-year cycle.
n1, n = divmod(n, 365)

year += n100 * 100 + n4 * 4 + n1
if n1 == 4 or n100 == 4:
assert n == 0
return year-1, 12, 31

# Now the year is correct, and n is the offset from January 1. We find
# the month via an estimate that's either exact or one too large.
leapyear = n1 == 3 and (n4 != 24 or n100 == 3)
assert leapyear == _is_leap(year)
month = (n + 50) >> 5
preceding = _DAYS_BEFORE_MONTH[month] + (month > 2 and leapyear)
if preceding > n: # estimate is too large
month -= 1
preceding -= _DAYS_IN_MONTH[month] + (month == 2 and leapyear)
n -= preceding
assert 0 <= n < _days_in_month(year, month)

# Now the year and month are correct, and n is the offset from the
# start of that month: we're done!
return year, month, n+1
# February is the irregular month, so count in a calendar whose years
# start on March 1. Then the leap day, when there is one, is the last
# day of its year, and the long year of each 4-year cycle and the long
# century of each 400-year cycle come last as well. 0001-01-01 is 306
# days after 0000-03-01.
days = ordinal + 305

# A century is _DI400Y/4 = 36524.25 days on average, so century c starts
# on day floor(_DI400Y*c/4): the first three centuries of a 400-year
# cycle have 36524 days and the last one 36525. Inverting that, day n
# is in century (4*n + 3) // _DI400Y, and the remainder // 4 is its day
# within the century.
century, rest = divmod(4 * days + 3, _DI400Y)
day_of_century = rest // 4

# Within a century, years follow the same pattern with _DI4Y/4 = 365.25
# days (three common years, then a leap year; a 36524-day century just
# ends one day early).
year_of_century, rest = divmod(4 * day_of_century + 3, _DI4Y)
day_of_year = rest // 4 # March 1 is day 0

# From March on, month lengths follow 31, 30, 31, 30, 31: a 153-day
# block that repeats for March-July, August-December, and then
# January-February, cut short by the end of the year. So
# (5*day_of_year + 2) // 153 is the number of whole months before the
# day, and the remainder // 5 is the day within its month. Adding
# 3*153 = 459 makes March month 3; months 13 and 14 are January and
# February of the next calendar year.
month, rest = divmod(5 * day_of_year + 461, 153)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe you can do divmod(4 * day_of_year + 3, 153), right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think the factor must stay 5: one 153-day block has 5 months. With divmod(4 * day_of_year + 3, 153), March 1 (day_of_year == 0) gives month 0, and ordinal 1 gives (0, 8, 1).

461 is 3 * 153 + 2, so this is the same:

month, rest = divmod(5 * day_of_year + 2, 153)
month += 3
day = rest // 5 + 1

About the comments: I rewrote them in both versions. Now they explain each step: why the year starts on March 1, where 4*n + 3 comes from, and what 153 and 461 mean. I removed "Figure 12" (in the paper it is the multiply-shift version, not this one) and use the DOI.

day = rest // 5 + 1
year = 100 * century + year_of_century
if month > 12:
return year + 1, month - 12, day
return year, month, day

# Month and day names. For localized versions, see the calendar module.
_MONTHNAMES = [None, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
Expand Down
27 changes: 27 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from test import support
from test.support import is_resource_enabled, ALWAYS_EQ, LARGEST, SMALLEST
from test.support import os_helper, script_helper
from test.support.hypothesis_helper import hypothesis

import datetime as datetime_module
from datetime import MINYEAR, MAXYEAR
Expand Down Expand Up @@ -1434,6 +1435,25 @@ def test_ordinal_conversions(self):
self.assertEqual(d, self.theclass.fromordinal(n))
n += 1

# The ends of the 4-, 100- and 400-year cycles are the tricky cases.
@hypothesis.given(hypothesis.strategies.dates())
@hypothesis.example(date(1, 1, 1))
@hypothesis.example(date(4, 2, 29))
@hypothesis.example(date(4, 12, 31))
@hypothesis.example(date(100, 2, 28))
@hypothesis.example(date(100, 3, 1))
@hypothesis.example(date(100, 12, 31))
@hypothesis.example(date(400, 2, 29))
@hypothesis.example(date(400, 12, 31))
@hypothesis.example(date(401, 1, 1))
@hypothesis.example(date(2000, 3, 1))
@hypothesis.example(date(9999, 12, 31))
def test_ordinal_roundtrip(self, d):
# Rebuild d with the class under test: the strategy and the examples
# may come from the other implementation.
d = self.theclass(d.year, d.month, d.day)
self.assertEqual(self.theclass.fromordinal(d.toordinal()), d)

def test_extreme_ordinals(self):
a = self.theclass.min
a = self.theclass(a.year, a.month, a.day) # get rid of time parts
Expand All @@ -1455,6 +1475,13 @@ def test_extreme_ordinals(self):

self.assertRaises(ValueError, lambda: a.fromordinal(aord + 1))

# Past the maximum, the error still reports the year of the ordinal.
for n, year in ((aord + 1, 10000), (aord + 146097 + 1, 10400),
(2**31 - 1, 5879611)):
with self.subTest(n=n):
with self.assertRaisesRegex(ValueError, rf"\b{year}\b"):
self.theclass.fromordinal(n)

b = a - timedelta(days=1)
self.assertEqual(b.toordinal(), aord - 1)
self.assertEqual(b, self.theclass.fromordinal(aord - 1))
Expand Down
9 changes: 8 additions & 1 deletion Lib/test/support/hypothesis_helper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import os
import warnings

try:
import hypothesis
with warnings.catch_warnings():
# Hypothesis imports typing.AnyStr, which is deprecated. Silence the
# warning so that test.test___all__ can import test modules that use
# Hypothesis.
warnings.filterwarnings("ignore", "'typing.AnyStr' is deprecated",
DeprecationWarning)
import hypothesis
except ImportError:
from . import _hypothesis_stubs as hypothesis
else:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Speed up conversion of ordinals to dates in :mod:`datetime` by switching to
the `Neri–Schneider algorithm <https://doi.org/10.1002/spe.3172>`__.
:meth:`~datetime.date.fromordinal` is about 15% faster in the C
implementation and about 30% faster in the pure-Python implementation.
Other operations that convert ordinals to dates, such as
:meth:`~datetime.date.fromisocalendar` and :class:`~datetime.timedelta`
arithmetic, also benefit.
143 changes: 65 additions & 78 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -471,88 +471,63 @@ days_before_month(int year, int month)
#define DI100Y 36524 /* _PyTime_DaysBeforeYear(101); days in 100 years */
#define DI400Y 146097 /* _PyTime_DaysBeforeYear(401); days in 400 years */

/* ordinal -> year, month, day, considering 01-Jan-0001 as day 1. */
/* ordinal -> year, month, day, considering 01-Jan-0001 as day 1.
*
* This follows Neri and Schneider [1]; _ord2ymd() in Lib/_pydatetime.py
* does the same steps.
*
* February is the irregular month, so we count in a calendar whose years
* start on March 1. Then the leap day, when there is one, is the last day
* of its year, and the long year of each 4-year cycle and the long century
* of each 400-year cycle come last as well.
*
* A century is DI400Y/4 = 36524.25 days on average, so century c starts on
* day floor(DI400Y*c/4); the first three centuries of a 400-year cycle have
* 36524 days and the last one 36525. Inverting that, day n is in century
* (4*n + 3) / DI400Y, and (4*n + 3) % DI400Y / 4 is its day within the
* century. Within a century, years follow the same pattern with
* DI4Y/4 = 365.25 days (three common years, then a leap year; a 36524-day
* century just ends one day early), so the same step with DI4Y gives the
* year and the day within it.
*
* From March on, month lengths follow 31, 30, 31, 30, 31, a 153-day block
* that repeats: March-July, August-December, then January-February (cut
* short by the end of the year). So for day n of the year (March 1 is
* day 0), (5*n + 2) / 153 is the number of whole months before it and
* (5*n + 2) % 153 / 5 is the day within its month. We add 3*153 = 459 so
* that March is month 3; months 13 and 14 are January and February of the
* next calendar year.
*
* All intermediate values fit in 32 bits as long as ordinal is at most
* MAXORDINAL + 2 (iso_to_ymd() can reach 10000-01-02).
* date.fromordinal() handles larger ordinals itself.
*
* [1] Neri C, Schneider L. Euclidean affine functions and their application
* to calendar algorithms. Softw Pract Exper. 2023;53(4):937-970.
* https://doi.org/10.1002/spe.3172
*/
static void
ord_to_ymd(int ordinal, int *year, int *month, int *day)
{
int n, n1, n4, n100, n400, leapyear, preceding;

/* ordinal is a 1-based index, starting at 1-Jan-1. The pattern of
* leap years repeats exactly every 400 years. The basic strategy is
* to find the closest 400-year boundary at or before ordinal, then
* work with the offset from that boundary to ordinal. Life is much
* clearer if we subtract 1 from ordinal first -- then the values
* of ordinal at 400-year boundaries are exactly those divisible
* by DI400Y:
*
* D M Y n n-1
* -- --- ---- ---------- ----------------
* 31 Dec -400 -DI400Y -DI400Y -1
* 1 Jan -399 -DI400Y +1 -DI400Y 400-year boundary
* ...
* 30 Dec 000 -1 -2
* 31 Dec 000 0 -1
* 1 Jan 001 1 0 400-year boundary
* 2 Jan 001 2 1
* 3 Jan 001 3 2
* ...
* 31 Dec 400 DI400Y DI400Y -1
* 1 Jan 401 DI400Y +1 DI400Y 400-year boundary
*/
assert(ordinal >= 1);
--ordinal;
n400 = ordinal / DI400Y;
n = ordinal % DI400Y;
*year = n400 * 400 + 1;

/* Now n is the (non-negative) offset, in days, from January 1 of
* year, to the desired date. Now compute how many 100-year cycles
* precede n.
* Note that it's possible for n100 to equal 4! In that case 4 full
* 100-year cycles precede the desired day, which implies the
* desired day is December 31 at the end of a 400-year cycle.
*/
n100 = n / DI100Y;
n = n % DI100Y;

/* Now compute how many 4-year cycles precede it. */
n4 = n / DI4Y;
n = n % DI4Y;

/* And now how many single years. Again n1 can be 4, and again
* meaning that the desired day is December 31 at the end of the
* 4-year cycle.
*/
n1 = n / 365;
n = n % 365;

*year += n100 * 100 + n4 * 4 + n1;
if (n1 == 4 || n100 == 4) {
assert(n == 0);
*year -= 1;
*month = 12;
*day = 31;
return;
assert(1 <= ordinal && ordinal <= MAXORDINAL + 2);
/* Days since 0000-03-01; 0001-01-01 is 306 days later. */
const uint32_t days = (uint32_t)ordinal + 305;
const uint32_t century = (4 * days + 3) / DI400Y;
const uint32_t day_of_century = (4 * days + 3) % DI400Y / 4;
const uint32_t year_of_century = (4 * day_of_century + 3) / DI4Y;
const uint32_t day_of_year = (4 * day_of_century + 3) % DI4Y / 4;
const uint32_t m = (5 * day_of_year + 461) / 153;
const uint32_t d = (5 * day_of_year + 461) % 153 / 5 + 1;
const uint32_t y = 100 * century + year_of_century;
if (m > 12) { /* January or February */
*year = (int)(y + 1);
*month = (int)(m - 12);
}

/* Now the year is correct, and n is the offset from January 1. We
* find the month via an estimate that's either exact or one too
* large.
*/
leapyear = n1 == 3 && (n4 != 24 || n100 == 3);
assert(leapyear == is_leap(*year));
*month = (n + 50) >> 5;
preceding = (_days_before_month[*month] + (*month > 2 && leapyear));
if (preceding > n) {
/* estimate is too large */
*month -= 1;
preceding -= days_in_month(*year, *month);
else {
*year = (int)y;
*month = (int)m;
}
n -= preceding;
assert(0 <= n);
assert(n < days_in_month(*year, *month));

*day = n + 1;
*day = (int)d;
}

/* year, month, day -> ordinal, considering 01-Jan-0001 as day 1. */
Expand Down Expand Up @@ -3439,7 +3414,19 @@ datetime_date_fromordinal_impl(PyTypeObject *type, int ordinal)
PyErr_SetString(PyExc_ValueError, "ordinal must be >= 1");
return NULL;
}
ord_to_ymd(ordinal, &year, &month, &day);
if (ordinal <= MAXORDINAL) {
ord_to_ymd(ordinal, &year, &month, &day);
}
else {
/* Out of range: find the year anyway, so new_date_subclass_ex()
* raises the usual "year must be in 1..9999, not N" error. The
* calendar repeats every 400 years (DI400Y days), so convert the
* equivalent date in the first 400-year cycle and add the years
* back. */
int cycles = (ordinal - 1) / DI400Y;
ord_to_ymd(ordinal - cycles * DI400Y, &year, &month, &day);
year += 400 * cycles;
}
return new_date_subclass_ex(year, month, day, type);
}

Expand Down
Loading