Skip to content

Commit f5685a2

Browse files
gesslerpdvstinner
andauthored
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>
1 parent 565685f commit f5685a2

File tree

5 files changed

+173
-88
lines changed

5 files changed

+173
-88
lines changed

Lib/test/datetimetester.py

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2706,24 +2706,20 @@ def utcfromtimestamp(*args, **kwargs):
27062706
self.assertEqual(zero.second, 0)
27072707
self.assertEqual(zero.microsecond, 0)
27082708
one = fts(1e-6)
2709-
try:
2710-
minus_one = fts(-1e-6)
2711-
except OSError:
2712-
# localtime(-1) and gmtime(-1) is not supported on Windows
2713-
pass
2714-
else:
2715-
self.assertEqual(minus_one.second, 59)
2716-
self.assertEqual(minus_one.microsecond, 999999)
2717-
2718-
t = fts(-1e-8)
2719-
self.assertEqual(t, zero)
2720-
t = fts(-9e-7)
2721-
self.assertEqual(t, minus_one)
2722-
t = fts(-1e-7)
2723-
self.assertEqual(t, zero)
2724-
t = fts(-1/2**7)
2725-
self.assertEqual(t.second, 59)
2726-
self.assertEqual(t.microsecond, 992188)
2709+
minus_one = fts(-1e-6)
2710+
2711+
self.assertEqual(minus_one.second, 59)
2712+
self.assertEqual(minus_one.microsecond, 999999)
2713+
2714+
t = fts(-1e-8)
2715+
self.assertEqual(t, zero)
2716+
t = fts(-9e-7)
2717+
self.assertEqual(t, minus_one)
2718+
t = fts(-1e-7)
2719+
self.assertEqual(t, zero)
2720+
t = fts(-1/2**7)
2721+
self.assertEqual(t.second, 59)
2722+
self.assertEqual(t.microsecond, 992188)
27272723

27282724
t = fts(1e-7)
27292725
self.assertEqual(t, zero)
@@ -2752,22 +2748,18 @@ def utcfromtimestamp(*args, **kwargs):
27522748
self.assertEqual(zero.second, 0)
27532749
self.assertEqual(zero.microsecond, 0)
27542750
one = fts(D('0.000_001'))
2755-
try:
2756-
minus_one = fts(D('-0.000_001'))
2757-
except OSError:
2758-
# localtime(-1) and gmtime(-1) is not supported on Windows
2759-
pass
2760-
else:
2761-
self.assertEqual(minus_one.second, 59)
2762-
self.assertEqual(minus_one.microsecond, 999_999)
2751+
minus_one = fts(D('-0.000_001'))
2752+
2753+
self.assertEqual(minus_one.second, 59)
2754+
self.assertEqual(minus_one.microsecond, 999_999)
27632755

2764-
t = fts(D('-0.000_000_1'))
2765-
self.assertEqual(t, zero)
2766-
t = fts(D('-0.000_000_9'))
2767-
self.assertEqual(t, minus_one)
2768-
t = fts(D(-1)/2**7)
2769-
self.assertEqual(t.second, 59)
2770-
self.assertEqual(t.microsecond, 992188)
2756+
t = fts(D('-0.000_000_1'))
2757+
self.assertEqual(t, zero)
2758+
t = fts(D('-0.000_000_9'))
2759+
self.assertEqual(t, minus_one)
2760+
t = fts(D(-1)/2**7)
2761+
self.assertEqual(t.second, 59)
2762+
self.assertEqual(t.microsecond, 992188)
27712763

27722764
t = fts(D('0.000_000_1'))
27732765
self.assertEqual(t, zero)
@@ -2803,22 +2795,18 @@ def utcfromtimestamp(*args, **kwargs):
28032795
self.assertEqual(zero.second, 0)
28042796
self.assertEqual(zero.microsecond, 0)
28052797
one = fts(F(1, 1_000_000))
2806-
try:
2807-
minus_one = fts(F(-1, 1_000_000))
2808-
except OSError:
2809-
# localtime(-1) and gmtime(-1) is not supported on Windows
2810-
pass
2811-
else:
2812-
self.assertEqual(minus_one.second, 59)
2813-
self.assertEqual(minus_one.microsecond, 999_999)
2798+
minus_one = fts(F(-1, 1_000_000))
28142799

2815-
t = fts(F(-1, 10_000_000))
2816-
self.assertEqual(t, zero)
2817-
t = fts(F(-9, 10_000_000))
2818-
self.assertEqual(t, minus_one)
2819-
t = fts(F(-1, 2**7))
2820-
self.assertEqual(t.second, 59)
2821-
self.assertEqual(t.microsecond, 992188)
2800+
self.assertEqual(minus_one.second, 59)
2801+
self.assertEqual(minus_one.microsecond, 999_999)
2802+
2803+
t = fts(F(-1, 10_000_000))
2804+
self.assertEqual(t, zero)
2805+
t = fts(F(-9, 10_000_000))
2806+
self.assertEqual(t, minus_one)
2807+
t = fts(F(-1, 2**7))
2808+
self.assertEqual(t.second, 59)
2809+
self.assertEqual(t.microsecond, 992188)
28222810

28232811
t = fts(F(1, 10_000_000))
28242812
self.assertEqual(t, zero)
@@ -2860,6 +2848,7 @@ def test_timestamp_limits(self):
28602848
# If that assumption changes, this value can change as well
28612849
self.assertEqual(max_ts, 253402300799.0)
28622850

2851+
@unittest.skipIf(sys.platform == "win32", "Windows doesn't support min timestamp")
28632852
def test_fromtimestamp_limits(self):
28642853
try:
28652854
self.theclass.fromtimestamp(-2**32 - 1)
@@ -2899,6 +2888,7 @@ def test_fromtimestamp_limits(self):
28992888
# OverflowError, especially on 32-bit platforms.
29002889
self.theclass.fromtimestamp(ts)
29012890

2891+
@unittest.skipIf(sys.platform == "win32", "Windows doesn't support min timestamp")
29022892
def test_utcfromtimestamp_limits(self):
29032893
with self.assertWarns(DeprecationWarning):
29042894
try:
@@ -2960,13 +2950,11 @@ def test_insane_utcfromtimestamp(self):
29602950
self.assertRaises(OverflowError, self.theclass.utcfromtimestamp,
29612951
insane)
29622952

2963-
@unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps")
29642953
def test_negative_float_fromtimestamp(self):
29652954
# The result is tz-dependent; at least test that this doesn't
29662955
# fail (like it did before bug 1646728 was fixed).
29672956
self.theclass.fromtimestamp(-1.05)
29682957

2969-
@unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps")
29702958
def test_negative_float_utcfromtimestamp(self):
29712959
with self.assertWarns(DeprecationWarning):
29722960
d = self.theclass.utcfromtimestamp(-1.05)

Lib/test/test_time.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,27 @@ def test_epoch(self):
187187
# Only test the date and time, ignore other gmtime() members
188188
self.assertEqual(tuple(epoch)[:6], (1970, 1, 1, 0, 0, 0), epoch)
189189

190+
def test_gmtime(self):
191+
# expected format:
192+
# (tm_year, tm_mon, tm_mday,
193+
# tm_hour, tm_min, tm_sec,
194+
# tm_wday, tm_yday)
195+
for t, expected in (
196+
(-13262400, (1969, 7, 31, 12, 0, 0, 3, 212)),
197+
(-6177600, (1969, 10, 21, 12, 0, 0, 1, 294)),
198+
# non-leap years (pre epoch)
199+
(-2203891200, (1900, 3, 1, 0, 0, 0, 3, 60)),
200+
(-2203977600, (1900, 2, 28, 0, 0, 0, 2, 59)),
201+
(-5359564800, (1800, 3, 1, 0, 0, 0, 5, 60)),
202+
(-5359651200, (1800, 2, 28, 0, 0, 0, 4, 59)),
203+
# leap years (pre epoch)
204+
(-2077660800, (1904, 3, 1, 0, 0, 0, 1, 61)),
205+
(-2077833600, (1904, 2, 28, 0, 0, 0, 6, 59)),
206+
):
207+
with self.subTest(t=t, expected=expected):
208+
res = time.gmtime(t)
209+
self.assertEqual(tuple(res)[:8], expected, res)
210+
190211
def test_strftime(self):
191212
tt = time.gmtime(self.t)
192213
for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'H', 'I',
@@ -501,12 +522,13 @@ def test_localtime_without_arg(self):
501522
def test_mktime(self):
502523
# Issue #1726687
503524
for t in (-2, -1, 0, 1):
525+
t_struct = time.localtime(t)
504526
try:
505-
tt = time.localtime(t)
527+
t1 = time.mktime(t_struct)
506528
except (OverflowError, OSError):
507529
pass
508530
else:
509-
self.assertEqual(time.mktime(tt), t)
531+
self.assertEqual(t1, t)
510532

511533
# Issue #13309: passing extreme values to mktime() or localtime()
512534
# borks the glibc's internal timezone data.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support negative timestamps in :func:`time.gmtime`, :func:`time.localtime`, and various :mod:`datetime` functions.

Modules/_datetimemodule.c

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5584,22 +5584,7 @@ datetime_from_timet_and_us(PyTypeObject *cls, TM_FUNC f, time_t timet, int us,
55845584
second = Py_MIN(59, tm.tm_sec);
55855585

55865586
/* local timezone requires to compute fold */
5587-
if (tzinfo == Py_None && f == _PyTime_localtime
5588-
/* On Windows, passing a negative value to local results
5589-
* in an OSError because localtime_s on Windows does
5590-
* not support negative timestamps. Unfortunately this
5591-
* means that fold detection for time values between
5592-
* 0 and max_fold_seconds will result in an identical
5593-
* error since we subtract max_fold_seconds to detect a
5594-
* fold. However, since we know there haven't been any
5595-
* folds in the interval [0, max_fold_seconds) in any
5596-
* timezone, we can hackily just forego fold detection
5597-
* for this time range.
5598-
*/
5599-
#ifdef MS_WINDOWS
5600-
&& (timet - max_fold_seconds > 0)
5601-
#endif
5602-
) {
5587+
if (tzinfo == Py_None && f == _PyTime_localtime) {
56035588
long long probe_seconds, result_seconds, transition;
56045589

56055590
result_seconds = utc_to_seconds(year, month, day,

Python/pytime.c

Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,89 @@ _PyTime_AsCLong(PyTime_t t, long *t2)
273273
*t2 = (long)t;
274274
return 0;
275275
}
276+
277+
// Seconds between 1601-01-01 and 1970-01-01:
278+
// 369 years + 89 leap days.
279+
#define SECS_BETWEEN_EPOCHS 11644473600LL
280+
#define HUNDRED_NS_PER_SEC 10000000LL
281+
282+
// Calculate day of year (0-365) from SYSTEMTIME
283+
static int
284+
_PyTime_calc_yday(const SYSTEMTIME *st)
285+
{
286+
// Cumulative days before each month (non-leap year)
287+
static const int days_before_month[] = {
288+
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
289+
};
290+
int yday = days_before_month[st->wMonth - 1] + st->wDay - 1;
291+
// Account for leap day if we're past February in a leap year.
292+
if (st->wMonth > 2) {
293+
// Leap year rules (Gregorian calendar):
294+
// - Years divisible by 4 are leap years
295+
// - EXCEPT years divisible by 100 are NOT leap years
296+
// - EXCEPT years divisible by 400 ARE leap years
297+
int year = st->wYear;
298+
int is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
299+
yday += is_leap;
300+
}
301+
return yday;
302+
}
303+
304+
// Convert time_t to struct tm using Windows FILETIME API.
305+
// If is_local is true, convert to local time.
306+
// Fallback for negative timestamps that localtime_s/gmtime_s cannot handle.
307+
// Return 0 on success. Return -1 on error.
308+
static int
309+
_PyTime_windows_filetime(time_t timer, struct tm *tm, int is_local)
310+
{
311+
/* Check for underflow - FILETIME epoch is 1601-01-01 */
312+
if (timer < -SECS_BETWEEN_EPOCHS) {
313+
PyErr_SetString(PyExc_OverflowError, "timestamp out of range for Windows FILETIME");
314+
return -1;
315+
}
316+
317+
/* Convert time_t to FILETIME (100-nanosecond intervals since 1601-01-01) */
318+
ULONGLONG ticks = ((ULONGLONG)timer + SECS_BETWEEN_EPOCHS) * HUNDRED_NS_PER_SEC;
319+
FILETIME ft;
320+
ft.dwLowDateTime = (DWORD)(ticks); // cast to DWORD truncates to low 32 bits
321+
ft.dwHighDateTime = (DWORD)(ticks >> 32);
322+
323+
/* Convert FILETIME to SYSTEMTIME */
324+
SYSTEMTIME st_result;
325+
if (is_local) {
326+
/* Convert to local time */
327+
FILETIME ft_local;
328+
if (!FileTimeToLocalFileTime(&ft, &ft_local) ||
329+
!FileTimeToSystemTime(&ft_local, &st_result)) {
330+
PyErr_SetFromWindowsErr(0);
331+
return -1;
332+
}
333+
}
334+
else {
335+
/* Convert to UTC */
336+
if (!FileTimeToSystemTime(&ft, &st_result)) {
337+
PyErr_SetFromWindowsErr(0);
338+
return -1;
339+
}
340+
}
341+
342+
/* Convert SYSTEMTIME to struct tm */
343+
tm->tm_year = st_result.wYear - 1900;
344+
tm->tm_mon = st_result.wMonth - 1; /* SYSTEMTIME: 1-12, tm: 0-11 */
345+
tm->tm_mday = st_result.wDay;
346+
tm->tm_hour = st_result.wHour;
347+
tm->tm_min = st_result.wMinute;
348+
tm->tm_sec = st_result.wSecond;
349+
tm->tm_wday = st_result.wDayOfWeek; /* 0=Sunday */
350+
351+
// `time.gmtime` and `time.localtime` will return `struct_time` containing this
352+
tm->tm_yday = _PyTime_calc_yday(&st_result);
353+
354+
/* DST flag: -1 (unknown) for local time on historical dates, 0 for UTC */
355+
tm->tm_isdst = is_local ? -1 : 0;
356+
357+
return 0;
358+
}
276359
#endif
277360

278361

@@ -882,10 +965,8 @@ py_get_system_clock(PyTime_t *tp, _Py_clock_info_t *info, int raise_exc)
882965
GetSystemTimePreciseAsFileTime(&system_time);
883966
large.u.LowPart = system_time.dwLowDateTime;
884967
large.u.HighPart = system_time.dwHighDateTime;
885-
/* 11,644,473,600,000,000,000: number of nanoseconds between
886-
the 1st january 1601 and the 1st january 1970 (369 years + 89 leap
887-
days). */
888-
PyTime_t ns = (large.QuadPart - 116444736000000000) * 100;
968+
969+
PyTime_t ns = (large.QuadPart - SECS_BETWEEN_EPOCHS * HUNDRED_NS_PER_SEC) * 100;
889970
*tp = ns;
890971
if (info) {
891972
// GetSystemTimePreciseAsFileTime() is implemented using
@@ -1242,15 +1323,19 @@ int
12421323
_PyTime_localtime(time_t t, struct tm *tm)
12431324
{
12441325
#ifdef MS_WINDOWS
1245-
int error;
1246-
1247-
error = localtime_s(tm, &t);
1248-
if (error != 0) {
1249-
errno = error;
1250-
PyErr_SetFromErrno(PyExc_OSError);
1251-
return -1;
1326+
if (t >= 0) {
1327+
/* For non-negative timestamps, use localtime_s() */
1328+
int error = localtime_s(tm, &t);
1329+
if (error != 0) {
1330+
errno = error;
1331+
PyErr_SetFromErrno(PyExc_OSError);
1332+
return -1;
1333+
}
1334+
return 0;
12521335
}
1253-
return 0;
1336+
1337+
/* For negative timestamps, use FILETIME-based conversion */
1338+
return _PyTime_windows_filetime(t, tm, 1);
12541339
#else /* !MS_WINDOWS */
12551340

12561341
#if defined(_AIX) && (SIZEOF_TIME_T < 8)
@@ -1281,15 +1366,19 @@ int
12811366
_PyTime_gmtime(time_t t, struct tm *tm)
12821367
{
12831368
#ifdef MS_WINDOWS
1284-
int error;
1285-
1286-
error = gmtime_s(tm, &t);
1287-
if (error != 0) {
1288-
errno = error;
1289-
PyErr_SetFromErrno(PyExc_OSError);
1290-
return -1;
1369+
/* For non-negative timestamps, use gmtime_s() */
1370+
if (t >= 0) {
1371+
int error = gmtime_s(tm, &t);
1372+
if (error != 0) {
1373+
errno = error;
1374+
PyErr_SetFromErrno(PyExc_OSError);
1375+
return -1;
1376+
}
1377+
return 0;
12911378
}
1292-
return 0;
1379+
1380+
/* For negative timestamps, use FILETIME-based conversion */
1381+
return _PyTime_windows_filetime(t, tm, 0);
12931382
#else /* !MS_WINDOWS */
12941383
if (gmtime_r(&t, tm) == NULL) {
12951384
#ifdef EINVAL

0 commit comments

Comments
 (0)