Skip to content

Conversation

@gesslerpd
Copy link
Contributor

@gesslerpd gesslerpd commented Jan 6, 2026

Previously, negative timestamps (representing dates before 1970-01-01) were not supported on Windows due to platform limitations. The changes introduce a fallback implementation using the Windows FILETIME API, allowing negative timestamps to be correctly handled in both UTC and local time conversions. Additionally, related test code is updated to remove Windows-specific skips and error handling, ensuring consistent behavior across platforms.

This may have some overlap and/or impact on the proposed changes in #134461

Other related issues:

@gesslerpd gesslerpd changed the title gh-80620: Support negative timestamps on windows for datetime.datetime gh-80620: Support negative timestamps on windows in datetime module Jan 6, 2026
@gesslerpd gesslerpd force-pushed the windows-datetime-pre-epoch branch from 0b9b5e8 to 64c70c3 Compare January 6, 2026 04:24
Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

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

I would prefer to change datetime in a separated PR, and restrict this change to time.gmtime() and time.localtime() changes.

You should document time.gmtime() and time.localtime() changes in a NEWS entry.

Python/pytime.c Outdated
Comment on lines 328 to 344
/* Calculate day of year using Windows FILETIME difference */
// SYSTEMTIME st_jan1 = {st_result.wYear, 1, 0, 1, 0, 0, 0, 0};
// FILETIME ft_jan1, ft_date;
// if (!SystemTimeToFileTime(&st_jan1, &ft_jan1) ||
// !SystemTimeToFileTime(&st_result, &ft_date)) {
// PyErr_SetFromWindowsErr(0);
// return -1;
// }
// ULARGE_INTEGER jan1, date;
// jan1.LowPart = ft_jan1.dwLowDateTime;
// jan1.HighPart = ft_jan1.dwHighDateTime;
// date.LowPart = ft_date.dwLowDateTime;
// date.HighPart = ft_date.dwHighDateTime;
// /* Convert 100-nanosecond intervals to days */
// LONGLONG days_diff = (date.QuadPart - jan1.QuadPart) / (24LL * 60 * 60 * HUNDRED_NS_PER_SEC);

// tm->tm_yday = (int)days_diff;
Copy link
Member

Choose a reason for hiding this comment

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

Please remove commented/dead code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you think that yday is important to have? Doesn't impact datetime AFAIK but it does show up in struct_time objects returned from gmtime/localtime. For completeness with respect to time module it could be put back in.

Copy link
Member

Choose a reason for hiding this comment

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

If possible, it would be better to fill tm_yday as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Restored tm_yday calculation, I think the windows API version was a bit heavy so simplified it by adding custom util function.

@vstinner
Copy link
Member

vstinner commented Jan 8, 2026

test_time.test_mktime() can be updated: except (OverflowError, OSError) is no longer needed if I'm right.

@gesslerpd
Copy link
Contributor Author

test_time.test_mktime() can be updated: except (OverflowError, OSError) is no longer needed if I'm right.

time.mktime still doesn't support negative timestamps with this change, gmtime and localtime do as side effect, was mostly making these changes for datetime.

I was going to tweak callsites so it doesn't impact time module at all but thought I'd leave that choice to the review.

@vstinner
Copy link
Member

vstinner commented Jan 8, 2026

I was going to tweak callsites so it doesn't impact time module at all but thought I'd leave that choice to the review.

Oh, it's a good thing to implement the new feature in the time module.


Existing test_time test:

    def test_mktime(self):
        # Issue #1726687
        for t in (-2, -1, 0, 1):
            try:
                tt = time.localtime(t)
            except (OverflowError, OSError):
                pass
            else:
                self.assertEqual(time.mktime(tt), t)

If localtime() is modified to support negative timestamp, but mktime() can still fail, the test should be modified to something like:

        for t in (-2, -1, 0, 1):
            tt = time.localtime(t)
            try:
                t2 = time.mktime(tt)
            except (OverflowError, OSError):
                pass
            else:
                self.assertEqual(t2, t)

@gesslerpd gesslerpd changed the title gh-80620: Support negative timestamps on windows in datetime module gh-80620: Support negative timestamps on windows in time.gmtime, time.localtime, and datetime module Jan 8, 2026
else:
self.assertEqual(time.mktime(tt), t)
self.assertEqual(t1, t)

Copy link
Member

Choose a reason for hiding this comment

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

Can you add this test after def test_epoch():?

    def test_gmtime(self):
        # expected format:
        # (tm_year, tm_mon, tm_mday,
        #  tm_hour, tm_min, tm_sec,
        #  tm_wday, tm_yday)
        for t, expected in (
            (-13262400, (1969, 7, 31, 12, 0, 0, 3, 212)),
            (-6177600, (1969, 10, 21, 12, 0, 0, 1, 294)),
        ):
            with self.subTest(t=t, expected=expected):
                res = time.gmtime(t)
                self.assertEqual(tuple(res)[:8], expected, res)

test_gmtime() tests indirectly _PyTime_calc_yday().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for review, added, also added some pre-epoch cases that "leap" across the leap day gap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants