-
-
Notifications
You must be signed in to change notification settings - Fork 33.9k
gh-80620: Support negative timestamps on windows in time.gmtime, time.localtime, and datetime module
#143463
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.
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
Open
gesslerpd
wants to merge
10
commits into
python:main
Choose a base branch
from
gesslerpd:windows-datetime-pre-epoch
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+172
−88
Open
gh-80620: Support negative timestamps on windows in time.gmtime, time.localtime, and datetime module
#143463
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
c8eab29
init
gesslerpd 8ec0eca
remove hacks for previous windows negative timestamp behavior
gesslerpd 3d9f1a2
update datetime tests
gesslerpd 64c70c3
add blurb
gesslerpd 4f8c2ae
restore `tm_yday` calculation, improve `time` test
gesslerpd d58444c
update blurb
gesslerpd 97c1bfb
add gmtime pre-epoch test
gesslerpd 2224f68
test yday jumps
gesslerpd 09a3dac
fix tm_wday copy/paste mistake
gesslerpd d711f28
review fixes
gesslerpd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
Misc/NEWS.d/next/Windows/2026-01-05-21-36-58.gh-issue-80620.p1bD58.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Support negative timestamps in :func:`time.gmtime`, :func:`time.localtime`, and various :mod:`datetime` functions. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -273,6 +273,88 @@ _PyTime_AsCLong(PyTime_t t, long *t2) | |||||||||||
| *t2 = (long)t; | ||||||||||||
| return 0; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // 369 years + 89 leap days | ||||||||||||
| #define SECS_BETWEEN_EPOCHS 11644473600LL /* Seconds between 1601-01-01 and 1970-01-01 */ | ||||||||||||
|
Comment on lines
+277
to
+278
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| #define HUNDRED_NS_PER_SEC 10000000LL | ||||||||||||
|
|
||||||||||||
| // Calculate day of year (0-365) from SYSTEMTIME | ||||||||||||
| static int | ||||||||||||
| _PyTime_calc_yday(const SYSTEMTIME *st) | ||||||||||||
| { | ||||||||||||
| // Cumulative days before each month (non-leap year) | ||||||||||||
| static const int days_before_month[] = { | ||||||||||||
| 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 | ||||||||||||
| }; | ||||||||||||
| int yday = days_before_month[st->wMonth - 1] + st->wDay - 1; | ||||||||||||
| // Account for leap day if we're past February in a leap year. | ||||||||||||
| if (st->wMonth > 2) { | ||||||||||||
| // Leap year rules (Gregorian calendar): | ||||||||||||
| // - Years divisible by 4 are leap years | ||||||||||||
| // - EXCEPT years divisible by 100 are NOT leap years | ||||||||||||
| // - EXCEPT years divisible by 400 ARE leap years | ||||||||||||
| int year = st->wYear; | ||||||||||||
| int is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); | ||||||||||||
| yday += is_leap; | ||||||||||||
| } | ||||||||||||
| return yday; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Convert time_t to struct tm using Windows FILETIME API. | ||||||||||||
| // If is_local is true, convert to local time. */ | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| // Fallback for negative timestamps that localtime_s/gmtime_s cannot handle. | ||||||||||||
| // Return 0 on success. Return -1 on error. | ||||||||||||
| static int | ||||||||||||
| _PyTime_windows_filetime(time_t timer, struct tm *tm, int is_local) | ||||||||||||
| { | ||||||||||||
| /* Check for underflow - FILETIME epoch is 1601-01-01 */ | ||||||||||||
| if (timer < -SECS_BETWEEN_EPOCHS) { | ||||||||||||
| PyErr_SetString(PyExc_OverflowError, "timestamp out of range for Windows FILETIME"); | ||||||||||||
| return -1; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /* Convert time_t to FILETIME (100-nanosecond intervals since 1601-01-01) */ | ||||||||||||
| ULONGLONG ticks = ((ULONGLONG)timer + SECS_BETWEEN_EPOCHS) * HUNDRED_NS_PER_SEC; | ||||||||||||
| FILETIME ft; | ||||||||||||
| ft.dwLowDateTime = (DWORD)(ticks); // cast to DWORD truncates to low 32 bits | ||||||||||||
| ft.dwHighDateTime = (DWORD)(ticks >> 32); | ||||||||||||
|
|
||||||||||||
| /* Convert FILETIME to SYSTEMTIME */ | ||||||||||||
| SYSTEMTIME st_result; | ||||||||||||
| if (is_local) { | ||||||||||||
| /* Convert to local time */ | ||||||||||||
| FILETIME ft_local; | ||||||||||||
| if (!FileTimeToLocalFileTime(&ft, &ft_local) || | ||||||||||||
| !FileTimeToSystemTime(&ft_local, &st_result)) { | ||||||||||||
| PyErr_SetFromWindowsErr(0); | ||||||||||||
| return -1; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
| else { | ||||||||||||
| /* Convert to UTC */ | ||||||||||||
| if (!FileTimeToSystemTime(&ft, &st_result)) { | ||||||||||||
| PyErr_SetFromWindowsErr(0); | ||||||||||||
| return -1; | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /* Convert SYSTEMTIME to struct tm */ | ||||||||||||
| tm->tm_year = st_result.wYear - 1900; | ||||||||||||
| tm->tm_mon = st_result.wMonth - 1; /* SYSTEMTIME: 1-12, tm: 0-11 */ | ||||||||||||
| tm->tm_mday = st_result.wDay; | ||||||||||||
| tm->tm_hour = st_result.wHour; | ||||||||||||
| tm->tm_min = st_result.wMinute; | ||||||||||||
| tm->tm_sec = st_result.wSecond; | ||||||||||||
| tm->tm_wday = st_result.wDayOfWeek; /* 0=Sunday */ | ||||||||||||
|
|
||||||||||||
| // `time.gmtime` and `time.localtime` will return `struct_time` containing this | ||||||||||||
| tm->tm_yday = _PyTime_calc_yday(&st_result); | ||||||||||||
|
|
||||||||||||
| /* DST flag: -1 (unknown) for local time on historical dates, 0 for UTC */ | ||||||||||||
| tm->tm_isdst = is_local ? -1 : 0; | ||||||||||||
|
|
||||||||||||
| return 0; | ||||||||||||
| } | ||||||||||||
| #endif | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
|
|
@@ -882,10 +964,8 @@ py_get_system_clock(PyTime_t *tp, _Py_clock_info_t *info, int raise_exc) | |||||||||||
| GetSystemTimePreciseAsFileTime(&system_time); | ||||||||||||
| large.u.LowPart = system_time.dwLowDateTime; | ||||||||||||
| large.u.HighPart = system_time.dwHighDateTime; | ||||||||||||
| /* 11,644,473,600,000,000,000: number of nanoseconds between | ||||||||||||
| the 1st january 1601 and the 1st january 1970 (369 years + 89 leap | ||||||||||||
| days). */ | ||||||||||||
| PyTime_t ns = (large.QuadPart - 116444736000000000) * 100; | ||||||||||||
|
|
||||||||||||
| PyTime_t ns = (large.QuadPart - SECS_BETWEEN_EPOCHS * HUNDRED_NS_PER_SEC) * 100; | ||||||||||||
| *tp = ns; | ||||||||||||
| if (info) { | ||||||||||||
| // GetSystemTimePreciseAsFileTime() is implemented using | ||||||||||||
|
|
@@ -1242,15 +1322,19 @@ int | |||||||||||
| _PyTime_localtime(time_t t, struct tm *tm) | ||||||||||||
| { | ||||||||||||
| #ifdef MS_WINDOWS | ||||||||||||
| int error; | ||||||||||||
|
|
||||||||||||
| error = localtime_s(tm, &t); | ||||||||||||
| if (error != 0) { | ||||||||||||
| errno = error; | ||||||||||||
| PyErr_SetFromErrno(PyExc_OSError); | ||||||||||||
| return -1; | ||||||||||||
| if (t >= 0) { | ||||||||||||
| /* For non-negative timestamps, use localtime_s() */ | ||||||||||||
| int error = localtime_s(tm, &t); | ||||||||||||
| if (error != 0) { | ||||||||||||
| errno = error; | ||||||||||||
| PyErr_SetFromErrno(PyExc_OSError); | ||||||||||||
| return -1; | ||||||||||||
| } | ||||||||||||
| return 0; | ||||||||||||
| } | ||||||||||||
| return 0; | ||||||||||||
|
|
||||||||||||
| /* For negative timestamps, use FILETIME-based conversion */ | ||||||||||||
| return _PyTime_windows_filetime(t, tm, 1); | ||||||||||||
| #else /* !MS_WINDOWS */ | ||||||||||||
|
|
||||||||||||
| #if defined(_AIX) && (SIZEOF_TIME_T < 8) | ||||||||||||
|
|
@@ -1281,15 +1365,19 @@ int | |||||||||||
| _PyTime_gmtime(time_t t, struct tm *tm) | ||||||||||||
| { | ||||||||||||
| #ifdef MS_WINDOWS | ||||||||||||
| int error; | ||||||||||||
|
|
||||||||||||
| error = gmtime_s(tm, &t); | ||||||||||||
| if (error != 0) { | ||||||||||||
| errno = error; | ||||||||||||
| PyErr_SetFromErrno(PyExc_OSError); | ||||||||||||
| return -1; | ||||||||||||
| /* For non-negative timestamps, use gmtime_s() */ | ||||||||||||
| if (t >= 0) { | ||||||||||||
| int error = gmtime_s(tm, &t); | ||||||||||||
| if (error != 0) { | ||||||||||||
| errno = error; | ||||||||||||
| PyErr_SetFromErrno(PyExc_OSError); | ||||||||||||
| return -1; | ||||||||||||
| } | ||||||||||||
| return 0; | ||||||||||||
| } | ||||||||||||
| return 0; | ||||||||||||
|
|
||||||||||||
| /* For negative timestamps, use FILETIME-based conversion */ | ||||||||||||
| return _PyTime_windows_filetime(t, tm, 0); | ||||||||||||
| #else /* !MS_WINDOWS */ | ||||||||||||
| if (gmtime_r(&t, tm) == NULL) { | ||||||||||||
| #ifdef EINVAL | ||||||||||||
|
|
||||||||||||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.