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 — gh-161746: Use the Neri–Schneider algorithm for ordinal-to-date conversion in datetime by mazalovmx · Pull Request #161747 · python/cpython

Conversation

@mazalovmx

@mazalovmx mazalovmx commented Nov 10, 2026 •
edited
Loading

Uh oh!

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

Copy link
Copy Markdown

gh-161746

Verification

  • Same results as main for all 3,652,059 ordinals (0001-01-01 … 9999-12-31):
    SHA-256 over every date.fromordinal() result, plus fromisocalendar() and
    date + timedelta, for both the C and the pure-Python implementations.
  • C: same results for every positive int ordinal (1 … 2**31-1), including
    the error messages past 9999-12-31; checked with gcc 13 and clang.
  • Pure Python: same results for 2.3 million other integers, including negative
    and huge ones.
  • test_datetime, test_calendar, test_strptime, test_time, test_zoneinfo,
    test_plistlib and test_xmlrpc pass. The PR adds a Hypothesis property test
    (fromordinal(d.toordinal()) == d, with explicit examples at the ends of
    the 4-, 100- and 400-year cycles) and tests for the year reported in the
    error past 9999-12-31.
  • Lib/test/support/hypothesis_helper.py now silences the typing.AnyStr
    DeprecationWarning that Hypothesis emits on import; otherwise
    test___all__ fails when it imports test.datetimetester.

Benchmarks

Measured on the first version of the patch. pyperf, x86-64 VM, gcc 13.3, -O3, no PGO; µs per 1000 operations:

Benchmark main patched Speedup
date.fromordinal, random dates 35.6 25.3 1.40x
date + timedelta(days=40) 42.9 35.7 1.20x
date.fromisocalendar 83.5 71.0 1.18x
datetime + timedelta(days=40) 37.9 34.1 1.11x
date.fromordinal, consecutive dates 26.3 23.8 1.10x
date + timedelta(days=1), 1000 steps 32.8 31.8 1.03x

After the review changes (simple form, 32-bit) I measured again: C date.fromordinal 1.16x, pure Python 1.31x (random dates).

The C function itself takes 3-4 ns per call instead of 15-16 ns on random
dates (gcc 13 and clang, -O3). The pure-Python date.fromordinal() is about
1.3x faster.

With a PGO build (--enable-optimizations, gcc 13.3) of main (e3262d9) and of
the patch, 1,000,000 operations, best of 7, builds alternated:

Operation main patched Speedup
date.fromordinal, random dates 62.6 ms 45.9 ms 1.36x
date + timedelta(days=90) 69.0 ms 60.8 ms 1.13x
date.fromisoformat('2024-W07-1') 88.6 ms 77.8 ms 1.14x
date + timedelta(days=10) 53.0 ms 52.5 ms 1.01x

pyperformance 1.14.0 (--fast, same two PGO builds, 119 benchmarks in common):
geometric mean 1.00x. No pyperformance benchmark exercises this code path, so
no change is expected there; the differences per benchmark are noise.

🤖 Generated with Claude Code

Sorry, something went wrong.

Uh oh!

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

gpshead and Object905 reacted with heart emoji
All reactions
  • ❤️ 2 reactions
@mazalovmx @claude
pythongh-161746: Use the Neri-Schneider algorithm for ordinal to date…
e2feb05
… conversion

Replace the 400/100/4/1-year cycle decomposition and the month
estimate-and-correct step in ord_to_ymd() (Modules/_datetimemodule.c)
and _ord2ymd() (Lib/_pydatetime.py) with Figure 12 of Neri and
Schneider, "Euclidean affine functions and their application to
calendar algorithms" (Softw Pract Exper. 2023;53(4):937-970), which
GCC's libstdc++, the Linux kernel, .NET and Go already use.

The C version computes the year and the month steps with one
multiplication each, and does the century step in 64 bits so that every
positive int ordinal gives the same result as before.  The pure-Python
version uses plain floor division.

Both versions return the same results as the old code for every
ordinal from 1 to 3,652,059; the C version also for every positive int,
and the pure-Python one for 2.3 million other integers, including
negative and huge ones.  In microbenchmarks on x86-64 (gcc 13, -O3),
date.fromordinal() on random dates is about 1.4x faster, date +
timedelta across a month boundary and date.fromisocalendar() about 1.2x
faster, and the pure-Python date.fromordinal() about 1.3x faster.

Add a test for the first and last days of every month in years at the
start and the end of the 4-, 100- and 400-year cycles.

Co-Authored-By: Claude Fable 5.2 <[email protected]>
@mazalovmx
mazalovmx requested review from StanFromIreland and pganssle as code owners November 10, 2026 13:29
@python-cla-bot

python-cla-bot Bot commented Nov 10, 2026 •
edited
Loading

Uh oh!

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

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

All reactions

Sorry, something went wrong.

Uh oh!

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

@bedevere-app bedevere-app Bot mentioned this pull request Nov 10, 2026
Use the Neri–Schneider algorithm for ordinal-to-date conversion in datetime #161746
Open
@bedevere-app bedevere-app Bot added the awaiting review label Nov 10, 2026
StanFromIreland
StanFromIreland reviewed Nov 10, 2026
View reviewed changes
Comment thread Modules/_datetimemodule.c Outdated
* ceil(2**32 / DI4Y), so the high 32 bits are the quotient and the low
* 32 bits encode the remainder. */
const uint32_t year_scaled = 4 * day_of_century + 3;
const uint64_t year_product = (uint64_t)2939745 * year_scaled;

@StanFromIreland StanFromIreland Nov 10, 2026 •
edited
Loading

Uh oh!

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

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.

I don't like these magic numbers, can you please benchmark the simpler approach (compilers are pretty smart, and should be able to deal with these), like you implemented in Python? If we accept this (I still have to think about this), they should be as close as possible for maintenance purposes.

Sorry, something went wrong.

Uh oh!

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

All reactions

@mazalovmx mazalovmx Nov 10, 2026

Copy link
Copy Markdown
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 rewrote it to follow _ord2ymd() step by step, with plain / and % by constants. gcc 14 and clang 19 turn them into multiplications anyway (no div instructions at -O3).

Results, C implementation, µs per 1000 operations (pyperf, gcc 14.2, -O3, no PGO):

Benchmark main magic numbers simple
date.fromordinal, random dates 101 60.8 (1.65x) 62.2 (1.62x)
date + timedelta(days=40) 104 82.0 (1.27x) 85.2 (1.22x)
date.fromisocalendar 184 146 (1.26x) 156 (1.18x)
datetime + timedelta(days=40) 102 78.7 (1.29x) 83.2 (1.22x)
date.fromordinal, consecutive dates 65.9 61.9 (1.06x) 59.8 (1.10x)
Geometric mean (6 benchmarks) 1.25x 1.21x

Simple vs. magic numbers: 4 of 6 benchmarks not significant, geometric mean 1.03x slower. On its own, the function takes about 6.5 ns instead of 5.5 ns (the old code: about 17 ns). That isn't worth the magic numbers, so I've pushed the simple version.

It still matches the old code for every positive int ordinal (gcc and clang) and for all dates in the supported range.

Sorry, something went wrong.

Uh oh!

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

All reactions
Comment thread Misc/NEWS.d/next/Library/2026-11-10-13-28-10.gh-issue-161746.HxgY9R.rst Outdated
Speed up conversion of ordinals to dates in :mod:`datetime`, used by
:meth:`~datetime.date.fromordinal`, :meth:`~datetime.date.fromisocalendar`
and :class:`~datetime.timedelta` arithmetic, by switching to the
Neri–Schneider algorithm.

@StanFromIreland StanFromIreland Nov 10, 2026

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.

Add a link, and a rough % speedup.

Sorry, something went wrong.

Uh oh!

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

All reactions

@mazalovmx mazalovmx Nov 10, 2026

Copy link
Copy Markdown
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.

Done.

Sorry, something went wrong.

Uh oh!

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

All reactions
@mazalovmx @claude
Simplify ord_to_ymd() to mirror _ord2ymd(); expand NEWS
7144141
Use plain division and modulo by constants instead of hand-derived
multiply-shift constants; compilers emit multiplications anyway.
Add a link to the paper and rough speedups to the NEWS entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@bedevere-app bedevere-app Bot added the type-feature A feature request or enhancement label Nov 10, 2026
@StanFromIreland

StanFromIreland commented Nov 10, 2026

Copy link
Copy Markdown
Member

CC @cassioneri this may be of interest.

All reactions

Sorry, something went wrong.

Uh oh!

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

pganssle
pganssle reviewed Nov 10, 2026
View reviewed changes
Comment thread Lib/test/datetimetester.py Outdated
self.assertEqual(d, self.theclass.fromordinal(n))
n += 1

# Check the first and last days of every month in years at the

@pganssle pganssle Nov 10, 2026

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.

This might actually be a good opportunity to use a property test for the property that theclass.fromordinal(d.toordinal()) == d (handwavy)

Sorry, something went wrong.

Uh oh!

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

All reactions

@mazalovmx mazalovmx Nov 11, 2026

Copy link
Copy Markdown
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.

Done. I replaced my loop with a property test: fromordinal(d.toordinal()) == d for strategies.dates(). The ends of the 4-, 100- and 400-year cycles, and min/max date, are added as @example, so they run also without Hypothesis. The date is created again with self.theclass, so the test checks the class under test.

Sorry, something went wrong.

Uh oh!

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

All reactions
Comment thread Lib/_pydatetime.py
day_of_year = rest // 4 # day 0 is March 1
# Month lengths repeat every 5 months (153 days) from March on;
# month 3 is March, ..., month 14 is February.
month, rest = divmod(5 * day_of_year + 461, 153)

@pganssle pganssle Nov 10, 2026

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.

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

Sorry, something went wrong.

Uh oh!

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

All reactions

@mazalovmx mazalovmx Nov 11, 2026

Copy link
Copy Markdown
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.

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.

Sorry, something went wrong.

Uh oh!

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

All reactions
Comment thread Modules/_datetimemodule.c Outdated

*day = n + 1;
/* Days since 0000-03-01, the start of the computational calendar. */
const uint64_t days = (uint64_t)ordinal + 305;

@pganssle pganssle Nov 10, 2026

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.

I think we can drop the 64-bit arithmetic here entirely. As far as I can tell, the only reason for it is so that date.fromordinal() with an ordinal past MAXORDINAL still gets to the same "year must be in 1..9999, not N" error that it raises on main, because fromordinal only checks ordinal < 1 before calling ord_to_ymd. The other two callers never pass anything larger than MAXORDINAL + 2: normalize_date already range-checks the ordinal, and iso_to_ymd can reach at most 10000-01-02, the last day of ISO year 9999, which its caller then rejects.

Since the calendar repeats exactly every 400 years, fromordinal can handle the error case by shifting the ordinal into the first 400-year cycle and adding the years back afterwards, which keeps the error message identical without needing a 64-bit path in the hot function:

    if (ordinal < 1) {
        PyErr_SetString(PyExc_ValueError, "ordinal must be >= 1");
        return NULL;
    }
    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);

With that change, every intermediate value in ord_to_ymd fits in a uint32_t — 4 * days + 3 tops out around 14.6 million — so the function can assert ordinal <= MAXORDINAL + 2 and the comment explaining why the century step is done in 64 bits can go away. I checked this version against the ord_to_ymd on main for every positive int, and the year, month and day all match.

I did not see a measurable speed difference on x86-64 with either gcc or clang, so this is mostly an argument for simplicity. It may help somewhat on 32-bit platforms, where the 64-bit division can turn into a library call, though I have not measured that. The pure Python version does not need an equivalent change, since it uses arbitrary-precision integers and floor division.

Sorry, something went wrong.

Uh oh!

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

All reactions

@mazalovmx mazalovmx Nov 11, 2026

Copy link
Copy Markdown
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.

Done, I used your version. ord_to_ymd() is now uint32_t only, with assert(ordinal <= MAXORDINAL + 2), and fromordinal() handles bigger ordinals with the 400-year shift. I checked it against main for all positive int, and the error message is the same. I also added a test that the error still has the correct year for some ordinals after MAXORDINAL.

I checked the changes twice.

Sorry, something went wrong.

Uh oh!

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

All reactions
mazalovmx and others added 2 commits November 10, 2026 22:01
@mazalovmx @claude
Address review: 32-bit ord_to_ymd(), property test, comments
b3f74a0
* ord_to_ymd() uses only uint32_t; fromordinal() shifts larger
  ordinals by 400-year cycles, so the error message is unchanged.
* Replace the hand-picked loop with a Hypothesis round-trip test
  and add tests for the error year after MAXORDINAL.
* Explain each step in the comments of both implementations; cite
  the paper by DOI.
* Update the NEWS entry with new measurements.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@mazalovmx @claude
Silence the typing.AnyStr DeprecationWarning when importing Hypothesis
3688a10
test.test___all__ imports test.datetimetester, which now imports
Hypothesis.  Hypothesis imports the deprecated typing.AnyStr, and
test___all__ fails on the unexpected DeprecationWarning.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@pganssle pganssle pganssle left review comments
@StanFromIreland StanFromIreland StanFromIreland left review comments

Uh oh!

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

Assignees

No one assigned

Labels

awaiting review type-feature A feature request or enhancement

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

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

3 participants

@mazalovmx @StanFromIreland @pganssle
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 01402503 — 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.