gh-80620: Support negative timestamps on windows in time.gmtime, time.localtime, and datetime module (#143463)

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.

Co-authored-by: Victor Stinner <vstinner@python.org>
This commit is contained in:
Peter Gessler 2026-01-15 03:51:11 -06:00 committed by GitHub
parent 565685f6e8
commit f5685a266b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 172 additions and 87 deletions

View File

@ -2706,24 +2706,20 @@ class TestDateTime(TestDate):
self.assertEqual(zero.second, 0)
self.assertEqual(zero.microsecond, 0)
one = fts(1e-6)
try:
minus_one = fts(-1e-6)
except OSError:
# localtime(-1) and gmtime(-1) is not supported on Windows
pass
else:
self.assertEqual(minus_one.second, 59)
self.assertEqual(minus_one.microsecond, 999999)
minus_one = fts(-1e-6)
t = fts(-1e-8)
self.assertEqual(t, zero)
t = fts(-9e-7)
self.assertEqual(t, minus_one)
t = fts(-1e-7)
self.assertEqual(t, zero)
t = fts(-1/2**7)
self.assertEqual(t.second, 59)
self.assertEqual(t.microsecond, 992188)
self.assertEqual(minus_one.second, 59)
self.assertEqual(minus_one.microsecond, 999999)
t = fts(-1e-8)
self.assertEqual(t, zero)
t = fts(-9e-7)
self.assertEqual(t, minus_one)
t = fts(-1e-7)
self.assertEqual(t, zero)
t = fts(-1/2**7)
self.assertEqual(t.second, 59)
self.assertEqual(t.microsecond, 992188)
t = fts(1e-7)
self.assertEqual(t, zero)
@ -2752,22 +2748,18 @@ class TestDateTime(TestDate):
self.assertEqual(zero.second, 0)
self.assertEqual(zero.microsecond, 0)
one = fts(D('0.000_001'))
try:
minus_one = fts(D('-0.000_001'))
except OSError:
# localtime(-1) and gmtime(-1) is not supported on Windows
pass
else:
self.assertEqual(minus_one.second, 59)
self.assertEqual(minus_one.microsecond, 999_999)
minus_one = fts(D('-0.000_001'))
t = fts(D('-0.000_000_1'))
self.assertEqual(t, zero)
t = fts(D('-0.000_000_9'))
self.assertEqual(t, minus_one)
t = fts(D(-1)/2**7)
self.assertEqual(t.second, 59)
self.assertEqual(t.microsecond, 992188)
self.assertEqual(minus_one.second, 59)
self.assertEqual(minus_one.microsecond, 999_999)
t = fts(D('-0.000_000_1'))
self.assertEqual(t, zero)
t = fts(D('-0.000_000_9'))
self.assertEqual(t, minus_one)
t = fts(D(-1)/2**7)
self.assertEqual(t.second, 59)
self.assertEqual(t.microsecond, 992188)
t = fts(D('0.000_000_1'))
self.assertEqual(t, zero)
@ -2803,22 +2795,18 @@ class TestDateTime(TestDate):
self.assertEqual(zero.second, 0)
self.assertEqual(zero.microsecond, 0)
one = fts(F(1, 1_000_000))
try:
minus_one = fts(F(-1, 1_000_000))
except OSError:
# localtime(-1) and gmtime(-1) is not supported on Windows
pass
else:
self.assertEqual(minus_one.second, 59)
self.assertEqual(minus_one.microsecond, 999_999)
minus_one = fts(F(-1, 1_000_000))
t = fts(F(-1, 10_000_000))
self.assertEqual(t, zero)
t = fts(F(-9, 10_000_000))
self.assertEqual(t, minus_one)
t = fts(F(-1, 2**7))
self.assertEqual(t.second, 59)
self.assertEqual(t.microsecond, 992188)
self.assertEqual(minus_one.second, 59)
self.assertEqual(minus_one.microsecond, 999_999)
t = fts(F(-1, 10_000_000))
self.assertEqual(t, zero)
t = fts(F(-9, 10_000_000))
self.assertEqual(t, minus_one)
t = fts(F(-1, 2**7))
self.assertEqual(t.second, 59)
self.assertEqual(t.microsecond, 992188)
t = fts(F(1, 10_000_000))
self.assertEqual(t, zero)
@ -2860,6 +2848,7 @@ class TestDateTime(TestDate):
# If that assumption changes, this value can change as well
self.assertEqual(max_ts, 253402300799.0)
@unittest.skipIf(sys.platform == "win32", "Windows doesn't support min timestamp")
def test_fromtimestamp_limits(self):
try:
self.theclass.fromtimestamp(-2**32 - 1)
@ -2899,6 +2888,7 @@ class TestDateTime(TestDate):
# OverflowError, especially on 32-bit platforms.
self.theclass.fromtimestamp(ts)
@unittest.skipIf(sys.platform == "win32", "Windows doesn't support min timestamp")
def test_utcfromtimestamp_limits(self):
with self.assertWarns(DeprecationWarning):
try:
@ -2960,13 +2950,11 @@ class TestDateTime(TestDate):
self.assertRaises(OverflowError, self.theclass.utcfromtimestamp,
insane)
@unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps")
def test_negative_float_fromtimestamp(self):
# The result is tz-dependent; at least test that this doesn't
# fail (like it did before bug 1646728 was fixed).
self.theclass.fromtimestamp(-1.05)
@unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps")
def test_negative_float_utcfromtimestamp(self):
with self.assertWarns(DeprecationWarning):
d = self.theclass.utcfromtimestamp(-1.05)

View File

@ -187,6 +187,27 @@ class TimeTestCase(unittest.TestCase):
# Only test the date and time, ignore other gmtime() members
self.assertEqual(tuple(epoch)[:6], (1970, 1, 1, 0, 0, 0), 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)),
# non-leap years (pre epoch)
(-2203891200, (1900, 3, 1, 0, 0, 0, 3, 60)),
(-2203977600, (1900, 2, 28, 0, 0, 0, 2, 59)),
(-5359564800, (1800, 3, 1, 0, 0, 0, 5, 60)),
(-5359651200, (1800, 2, 28, 0, 0, 0, 4, 59)),
# leap years (pre epoch)
(-2077660800, (1904, 3, 1, 0, 0, 0, 1, 61)),
(-2077833600, (1904, 2, 28, 0, 0, 0, 6, 59)),
):
with self.subTest(t=t, expected=expected):
res = time.gmtime(t)
self.assertEqual(tuple(res)[:8], expected, res)
def test_strftime(self):
tt = time.gmtime(self.t)
for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'H', 'I',
@ -501,12 +522,13 @@ class TimeTestCase(unittest.TestCase):
def test_mktime(self):
# Issue #1726687
for t in (-2, -1, 0, 1):
t_struct = time.localtime(t)
try:
tt = time.localtime(t)
t1 = time.mktime(t_struct)
except (OverflowError, OSError):
pass
else:
self.assertEqual(time.mktime(tt), t)
self.assertEqual(t1, t)
# Issue #13309: passing extreme values to mktime() or localtime()
# borks the glibc's internal timezone data.

View File

@ -0,0 +1 @@
Support negative timestamps in :func:`time.gmtime`, :func:`time.localtime`, and various :mod:`datetime` functions.

View File

@ -5584,22 +5584,7 @@ datetime_from_timet_and_us(PyTypeObject *cls, TM_FUNC f, time_t timet, int us,
second = Py_MIN(59, tm.tm_sec);
/* local timezone requires to compute fold */
if (tzinfo == Py_None && f == _PyTime_localtime
/* On Windows, passing a negative value to local results
* in an OSError because localtime_s on Windows does
* not support negative timestamps. Unfortunately this
* means that fold detection for time values between
* 0 and max_fold_seconds will result in an identical
* error since we subtract max_fold_seconds to detect a
* fold. However, since we know there haven't been any
* folds in the interval [0, max_fold_seconds) in any
* timezone, we can hackily just forego fold detection
* for this time range.
*/
#ifdef MS_WINDOWS
&& (timet - max_fold_seconds > 0)
#endif
) {
if (tzinfo == Py_None && f == _PyTime_localtime) {
long long probe_seconds, result_seconds, transition;
result_seconds = utc_to_seconds(year, month, day,

View File

@ -273,6 +273,89 @@ _PyTime_AsCLong(PyTime_t t, long *t2)
*t2 = (long)t;
return 0;
}
// Seconds between 1601-01-01 and 1970-01-01:
// 369 years + 89 leap days.
#define SECS_BETWEEN_EPOCHS 11644473600LL
#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.
// 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 +965,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 +1323,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 +1366,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