Skip to content

Commit b3082b9

Browse files
committed
Add work in progress work to allow negative epoch on windows
1 parent c240c2d commit b3082b9

File tree

3 files changed

+113
-6
lines changed

3 files changed

+113
-6
lines changed

Lib/test/datetimetester.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2729,7 +2729,9 @@ def test_timestamp_limits(self):
27292729

27302730
def test_fromtimestamp_limits(self):
27312731
try:
2732-
self.theclass.fromtimestamp(-2**32 - 1)
2732+
# See if the platform can handle timestamps that are near the min.
2733+
# Windows, for example, can't do anything before its epoch.
2734+
self.theclass.fromtimestamp(self.theclass.min.timestamp())
27332735
except (OSError, OverflowError):
27342736
self.skipTest("Test not valid on this platform")
27352737

Lib/test/test_time.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,8 @@ def test_localtime_without_arg(self):
490490
t1 = time.mktime(lt1)
491491
self.assertAlmostEqual(t1, t0, delta=0.2)
492492

493+
@unittest.skipIf(sys.platform == "win32",
494+
"mktime with negative values not supported on windows")
493495
def test_mktime(self):
494496
# Issue #1726687
495497
for t in (-2, -1, 0, 1):

Python/pytime.c

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,19 +1263,122 @@ PyTime_PerfCounterRaw(PyTime_t *result)
12631263
return PyTime_MonotonicRaw(result);
12641264
}
12651265

1266+
#ifdef MS_WINDOWS
1267+
static int _cumulativeDaysInMonth[12] = {
1268+
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
1269+
};
1270+
static int _cumulativeDaysInMonthLeap[12] = {
1271+
0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335
1272+
};
1273+
1274+
time_t FILETIME_diff_seconds(FILETIME t1, FILETIME t2) {
1275+
ULARGE_INTEGER t1_int;
1276+
t1_int.LowPart = t1.dwLowDateTime;
1277+
t1_int.HighPart = t1.dwHighDateTime;
1278+
1279+
ULARGE_INTEGER t2_int;
1280+
t2_int.LowPart = t2.dwLowDateTime;
1281+
t2_int.HighPart = t2.dwHighDateTime;
1282+
1283+
ULARGE_INTEGER difference;
1284+
difference.QuadPart = t1_int.QuadPart - t2_int.QuadPart;
1285+
// Convert from hundreds of NS to seconds.
1286+
return (time_t) difference.QuadPart / (10 * SEC_TO_US);
1287+
}
1288+
#endif
12661289

12671290
int
12681291
_PyTime_localtime(time_t t, struct tm *tm)
12691292
{
12701293
#ifdef MS_WINDOWS
1271-
int error;
1294+
// While Windows does have unix-like localtime functions in the CRT, they
1295+
// more constrained than a usual libc on a unix system. In particular, they
1296+
// don't support negative numbers.
1297+
if (t >= 0) {
1298+
int error;
12721299

1273-
error = localtime_s(tm, &t);
1274-
if (error != 0) {
1275-
errno = error;
1276-
PyErr_SetFromErrno(PyExc_OSError);
1300+
error = localtime_s(tm, &t);
1301+
if (error != 0) {
1302+
errno = error;
1303+
PyErr_SetFromErrno(PyExc_OSError);
1304+
return -1;
1305+
}
1306+
return 0;
1307+
}
1308+
// For negative numbers, we use the equivalent win32 APIs, which involves
1309+
// converting our time_t to a win32 FILETIME.
1310+
// Windows doesn't provide an API to do this from C, only in the C++ WinRT
1311+
// * https://devblogs.microsoft.com/oldnewthing/20220602-00/?p=106706
1312+
// * https://learn.microsoft.com/en-us/windows/win32/sysinfo/converting-a-time-t-value-to-a-file-time
1313+
// We perform that conversion here. Firstly, Windows FILETIME uses hundredths
1314+
// of nanoseconds instead of seconds.
1315+
#define HUNDREDTH_NANOSECONDS_IN_SECONDS 10000000LL
1316+
// Secondly, the Windows epoch is 1601 instead of 1970, so this represents
1317+
// the number of hundredths of nanoseconds between those years:
1318+
// * https://www.wolframalpha.com/input?i=January+1%2C+1970+-+%28116444736000000000+*+100%29+nanoseconds
1319+
#define UNIX_EPOCH_TO_WINDOWS_HUNDREDTH_NS 116444736000000000LL;
1320+
ULARGE_INTEGER time_value;
1321+
time_value.QuadPart = (t * HUNDREDTH_NANOSECONDS_IN_SECONDS) + UNIX_EPOCH_TO_WINDOWS_HUNDREDTH_NS;
1322+
1323+
FILETIME tAsFiletime;
1324+
tAsFiletime.dwLowDateTime = time_value.LowPart;
1325+
tAsFiletime.dwHighDateTime = time_value.HighPart;
1326+
1327+
SYSTEMTIME utcSystemTime;
1328+
if (!FileTimeToSystemTime(&tAsFiletime, &utcSystemTime)) {
1329+
PyErr_SetFromWindowsErr(GetLastError());
12771330
return -1;
12781331
}
1332+
1333+
SYSTEMTIME localTime;
1334+
if (!SystemTimeToTzSpecificLocalTimeEx(NULL, &utcSystemTime, &localTime)) {
1335+
PyErr_SetFromWindowsErr(GetLastError());
1336+
return -1;
1337+
}
1338+
1339+
// SYSTEMTIME just has the year number, `struct tm` is years since 1900.
1340+
tm->tm_year = localTime.wYear - 1900;
1341+
// SYSTEMTIME uses 1-indexed months, `struct tm` is 0-indexed.
1342+
tm->tm_mon = localTime.wMonth - 1;
1343+
tm->tm_wday = localTime.wDayOfWeek;
1344+
tm->tm_mday = localTime.wDay;
1345+
tm->tm_hour = localTime.wHour;
1346+
tm->tm_min = localTime.wMinute;
1347+
tm->tm_sec = localTime.wSecond;
1348+
1349+
// We have two remaining fields in the `tm` struct to fill:
1350+
// `tm_yday` is the day in the current year, this is a function of whether
1351+
// we are in a leap year or not.
1352+
if (false /*is_leap_year*/) {
1353+
tm->tm_yday = _cumulativeDaysInMonthLeap[tm->tm_mon];
1354+
} else {
1355+
tm->tm_yday = _cumulativeDaysInMonth[tm->tm_mon];
1356+
}
1357+
tm->tm_yday += (localTime.wDay - 1);
1358+
1359+
// `tm_isdst` says whether or not DST was in effect locally at this time.
1360+
// This is a little trickier.
1361+
tm->tm_isdst = -1;
1362+
/*
1363+
FILETIME localTimeAsFiletime;
1364+
if (!SystemTimeToFileTime(&localTime, &localTimeAsFiletime)) {
1365+
PyErr_SetFromWindowsErr(GetLastError());
1366+
return -1;
1367+
}
1368+
1369+
time_t utc_diff = FILETIME_diff_seconds(tAsFiletime, localTimeAsFiletime);
1370+
1371+
TIME_ZONE_INFORMATION tzInfo;
1372+
if (!GetTimeZoneInformationForYear(localTime.wYear, NULL, &tzInfo)) {
1373+
PyErr_SetFromWindowsErr(GetLastError());
1374+
return -1;
1375+
}
1376+
1377+
tm->tm_isdst = 0;
1378+
if (tzInfo.Bias + tzInfo.StandardBias + tzInfo.DaylightBias == utc_diff / 60) {
1379+
tm->tm_isdst = 1;
1380+
}*/
1381+
12791382
return 0;
12801383
#else /* !MS_WINDOWS */
12811384

0 commit comments

Comments
 (0)