diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index fcf4416f331092..e679a57b3eba12 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -8,7 +8,6 @@ import time as _time import math as _math -import sys from operator import index as _index def _cmp(x, y): @@ -1849,13 +1848,6 @@ def _fromtimestamp(cls, t, utc, tz): # Let's probe 24 hours in the past to detect a transition: max_fold_seconds = 24 * 3600 - # On Windows localtime_s throws an OSError for negative values, - # thus we can't perform fold detection for values of time less - # than the max time fold. See comments in _datetimemodule's - # version of this method for more details. - if t < max_fold_seconds and sys.platform.startswith("win"): - return result - y, m, d, hh, mm, ss = converter(t - max_fold_seconds)[:6] probe1 = cls(y, m, d, hh, mm, ss, us, tz) trans = result - probe1 - timedelta(0, max_fold_seconds) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index ceeac9435dcb85..829b2be10ad0d0 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2727,9 +2727,16 @@ def test_timestamp_limits(self): # If that assumption changes, this value can change as well self.assertEqual(max_ts, 253402300799.0) + def test_fromtimestamp_roundtrip_near_epoch(self): + for ts in range(0, 1, 2): + roundtripped_ts = self.theclass.fromtimestamp(ts).timestamp() + self.assertEqual(roundtripped_ts, ts) + def test_fromtimestamp_limits(self): try: - self.theclass.fromtimestamp(-2**32 - 1) + # See if the platform can handle timestamps that are near the min. + # Windows, for example, can't do anything before its epoch. + self.theclass.fromtimestamp(self.theclass.min.timestamp()) except (OSError, OverflowError): self.skipTest("Test not valid on this platform") @@ -2827,13 +2834,11 @@ def test_insane_utcfromtimestamp(self): 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) diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 1147997d8d86bf..44eb4e8b9c3a88 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -490,6 +490,8 @@ def test_localtime_without_arg(self): t1 = time.mktime(lt1) self.assertAlmostEqual(t1, t0, delta=0.2) + @unittest.skipIf(sys.platform == "win32", + "mktime with negative values not supported on windows") def test_mktime(self): # Issue #1726687 for t in (-2, -1, 0, 1): diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 07d7089be09d20..7ce9d4b79a9b78 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -5460,22 +5460,7 @@ datetime_from_timet_and_us(PyObject *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, diff --git a/Python/pytime.c b/Python/pytime.c index b10d5cf927b7e1..29e3ac73978055 100644 --- a/Python/pytime.c +++ b/Python/pytime.c @@ -1263,19 +1263,122 @@ PyTime_PerfCounterRaw(PyTime_t *result) return PyTime_MonotonicRaw(result); } +#ifdef MS_WINDOWS +static int _cumulativeDaysInMonth[12] = { + 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 +}; +static int _cumulativeDaysInMonthLeap[12] = { + 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 +}; + +time_t FILETIME_diff_seconds(FILETIME t1, FILETIME t2) { + ULARGE_INTEGER t1_int; + t1_int.LowPart = t1.dwLowDateTime; + t1_int.HighPart = t1.dwHighDateTime; + + ULARGE_INTEGER t2_int; + t2_int.LowPart = t2.dwLowDateTime; + t2_int.HighPart = t2.dwHighDateTime; + + ULARGE_INTEGER difference; + difference.QuadPart = t1_int.QuadPart - t2_int.QuadPart; + // Convert from hundreds of NS to seconds. + return (time_t) difference.QuadPart / (10 * SEC_TO_US); +} +#endif int _PyTime_localtime(time_t t, struct tm *tm) { #ifdef MS_WINDOWS - int error; + // While Windows does have unix-like localtime functions in the CRT, they + // more constrained than a usual libc on a unix system. In particular, they + // don't support negative numbers. + if (t >= 0) { + int error; - error = localtime_s(tm, &t); - if (error != 0) { - errno = error; - PyErr_SetFromErrno(PyExc_OSError); + error = localtime_s(tm, &t); + if (error != 0) { + errno = error; + PyErr_SetFromErrno(PyExc_OSError); + return -1; + } + return 0; + } + // For negative numbers, we use the equivalent win32 APIs, which involves + // converting our time_t to a win32 FILETIME. + // Windows doesn't provide an API to do this from C, only in the C++ WinRT + // * https://devblogs.microsoft.com/oldnewthing/20220602-00/?p=106706 + // * https://learn.microsoft.com/en-us/windows/win32/sysinfo/converting-a-time-t-value-to-a-file-time + // We perform that conversion here. Firstly, Windows FILETIME uses hundredths + // of nanoseconds instead of seconds. + #define HUNDREDTH_NANOSECONDS_IN_SECONDS 10000000LL + // Secondly, the Windows epoch is 1601 instead of 1970, so this represents + // the number of hundredths of nanoseconds between those years: + // * https://www.wolframalpha.com/input?i=January+1%2C+1970+-+%28116444736000000000+*+100%29+nanoseconds + #define UNIX_EPOCH_TO_WINDOWS_HUNDREDTH_NS 116444736000000000LL; + ULARGE_INTEGER time_value; + time_value.QuadPart = (t * HUNDREDTH_NANOSECONDS_IN_SECONDS) + UNIX_EPOCH_TO_WINDOWS_HUNDREDTH_NS; + + FILETIME tAsFiletime; + tAsFiletime.dwLowDateTime = time_value.LowPart; + tAsFiletime.dwHighDateTime = time_value.HighPart; + + SYSTEMTIME utcSystemTime; + if (!FileTimeToSystemTime(&tAsFiletime, &utcSystemTime)) { + PyErr_SetFromWindowsErr(GetLastError()); return -1; } + + SYSTEMTIME localTime; + if (!SystemTimeToTzSpecificLocalTimeEx(NULL, &utcSystemTime, &localTime)) { + PyErr_SetFromWindowsErr(GetLastError()); + return -1; + } + + // SYSTEMTIME just has the year number, `struct tm` is years since 1900. + tm->tm_year = localTime.wYear - 1900; + // SYSTEMTIME uses 1-indexed months, `struct tm` is 0-indexed. + tm->tm_mon = localTime.wMonth - 1; + tm->tm_wday = localTime.wDayOfWeek; + tm->tm_mday = localTime.wDay; + tm->tm_hour = localTime.wHour; + tm->tm_min = localTime.wMinute; + tm->tm_sec = localTime.wSecond; + + // We have two remaining fields in the `tm` struct to fill: + // `tm_yday` is the day in the current year, this is a function of whether + // we are in a leap year or not. + if (false /*is_leap_year*/) { + tm->tm_yday = _cumulativeDaysInMonthLeap[tm->tm_mon]; + } else { + tm->tm_yday = _cumulativeDaysInMonth[tm->tm_mon]; + } + tm->tm_yday += (localTime.wDay - 1); + + // `tm_isdst` says whether or not DST was in effect locally at this time. + // This is a little trickier. + tm->tm_isdst = -1; + /* + FILETIME localTimeAsFiletime; + if (!SystemTimeToFileTime(&localTime, &localTimeAsFiletime)) { + PyErr_SetFromWindowsErr(GetLastError()); + return -1; + } + + time_t utc_diff = FILETIME_diff_seconds(tAsFiletime, localTimeAsFiletime); + + TIME_ZONE_INFORMATION tzInfo; + if (!GetTimeZoneInformationForYear(localTime.wYear, NULL, &tzInfo)) { + PyErr_SetFromWindowsErr(GetLastError()); + return -1; + } + + tm->tm_isdst = 0; + if (tzInfo.Bias + tzInfo.StandardBias + tzInfo.DaylightBias == utc_diff / 60) { + tm->tm_isdst = 1; + }*/ + return 0; #else /* !MS_WINDOWS */