mirror of
https://github.com/python/cpython.git
synced 2026-01-26 21:03:34 +00:00
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:
parent
565685f6e8
commit
f5685a266b
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -0,0 +1 @@
|
||||
Support negative timestamps in :func:`time.gmtime`, :func:`time.localtime`, and various :mod:`datetime` functions.
|
||||
@ -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,
|
||||
|
||||
129
Python/pytime.c
129
Python/pytime.c
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user