@@ -273,6 +273,86 @@ _PyTime_AsCLong(PyTime_t t, long *t2)
273273 * t2 = (long )t ;
274274 return 0 ;
275275}
276+
277+ // 369 years + 89 leap days
278+ #define SECS_BETWEEN_EPOCHS 11644473600LL /* Seconds between 1601-01-01 and 1970-01-01 */
279+ #define HUNDRED_NS_PER_SEC 10000000LL
280+
281+ // Convert time_t to struct tm using Windows FILETIME API.
282+ // If is_local is true, convert to local time. */
283+ // Fallback for negative timestamps that localtime_s/gmtime_s cannot handle.
284+ // Return 0 on success. Return -1 on error.
285+ static int
286+ _PyTime_windows_filetime (time_t timer , struct tm * tm , int is_local )
287+ {
288+ /* Check for underflow - FILETIME epoch is 1601-01-01 */
289+ if (timer < - SECS_BETWEEN_EPOCHS ) {
290+ PyErr_SetString (PyExc_OverflowError , "timestamp out of range for Windows FILETIME" );
291+ return -1 ;
292+ }
293+
294+ /* Convert time_t to FILETIME (100-nanosecond intervals since 1601-01-01) */
295+ ULONGLONG ticks = ((ULONGLONG )timer + SECS_BETWEEN_EPOCHS ) * HUNDRED_NS_PER_SEC ;
296+ FILETIME ft ;
297+ ft .dwLowDateTime = (DWORD )(ticks ); // cast to DWORD truncates to low 32 bits
298+ ft .dwHighDateTime = (DWORD )(ticks >> 32 );
299+
300+ /* Convert FILETIME to SYSTEMTIME */
301+ SYSTEMTIME st_result ;
302+ if (is_local ) {
303+ /* Convert to local time */
304+ FILETIME ft_local ;
305+ if (!FileTimeToLocalFileTime (& ft , & ft_local ) ||
306+ !FileTimeToSystemTime (& ft_local , & st_result )) {
307+ PyErr_SetFromWindowsErr (0 );
308+ return -1 ;
309+ }
310+ }
311+ else {
312+ /* Convert to UTC */
313+ if (!FileTimeToSystemTime (& ft , & st_result )) {
314+ PyErr_SetFromWindowsErr (0 );
315+ return -1 ;
316+ }
317+ }
318+
319+ /* Convert SYSTEMTIME to struct tm */
320+ tm -> tm_year = st_result .wYear - 1900 ;
321+ tm -> tm_mon = st_result .wMonth - 1 ; /* SYSTEMTIME: 1-12, tm: 0-11 */
322+ tm -> tm_mday = st_result .wDay ;
323+ tm -> tm_hour = st_result .wHour ;
324+ tm -> tm_min = st_result .wMinute ;
325+ tm -> tm_sec = st_result .wSecond ;
326+ tm -> tm_wday = st_result .wDayOfWeek ; /* 0=Sunday */
327+
328+ /* Calculate day of year using Windows FILETIME difference */
329+ // SYSTEMTIME st_jan1 = {st_result.wYear, 1, 0, 1, 0, 0, 0, 0};
330+ // FILETIME ft_jan1, ft_date;
331+ // if (!SystemTimeToFileTime(&st_jan1, &ft_jan1) ||
332+ // !SystemTimeToFileTime(&st_result, &ft_date)) {
333+ // PyErr_SetFromWindowsErr(0);
334+ // return -1;
335+ // }
336+ // ULARGE_INTEGER jan1, date;
337+ // jan1.LowPart = ft_jan1.dwLowDateTime;
338+ // jan1.HighPart = ft_jan1.dwHighDateTime;
339+ // date.LowPart = ft_date.dwLowDateTime;
340+ // date.HighPart = ft_date.dwHighDateTime;
341+ // /* Convert 100-nanosecond intervals to days */
342+ // LONGLONG days_diff = (date.QuadPart - jan1.QuadPart) / (24LL * 60 * 60 * HUNDRED_NS_PER_SEC);
343+
344+ // tm->tm_yday = (int)days_diff;
345+
346+ // datetime doesn't rely on tm_yday, so set invalid value and skip calculation
347+ // time.gmtime / time.localtime will return struct_time with out of range tm_yday
348+ // time.mktime doesn't support pre-epoch struct_time on windows anyway
349+ tm -> tm_yday = -1 ;
350+
351+ /* DST flag: -1 (unknown) for local time on historical dates, 0 for UTC */
352+ tm -> tm_isdst = is_local ? -1 : 0 ;
353+
354+ return 0 ;
355+ }
276356#endif
277357
278358
@@ -882,10 +962,8 @@ py_get_system_clock(PyTime_t *tp, _Py_clock_info_t *info, int raise_exc)
882962 GetSystemTimePreciseAsFileTime (& system_time );
883963 large .u .LowPart = system_time .dwLowDateTime ;
884964 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 ;
965+
966+ PyTime_t ns = (large .QuadPart - SECS_BETWEEN_EPOCHS * HUNDRED_NS_PER_SEC ) * 100 ;
889967 * tp = ns ;
890968 if (info ) {
891969 // GetSystemTimePreciseAsFileTime() is implemented using
@@ -1242,15 +1320,19 @@ int
12421320_PyTime_localtime (time_t t , struct tm * tm )
12431321{
12441322#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 ;
1323+ if (t >= 0 ) {
1324+ /* For non-negative timestamps, use standard conversion */
1325+ int error = localtime_s (tm , & t );
1326+ if (error != 0 ) {
1327+ errno = error ;
1328+ PyErr_SetFromErrno (PyExc_OSError );
1329+ return -1 ;
1330+ }
1331+ return 0 ;
12521332 }
1253- return 0 ;
1333+
1334+ /* For negative timestamps, use FILETIME-based conversion */
1335+ return _PyTime_windows_filetime (t , tm , 1 );
12541336#else /* !MS_WINDOWS */
12551337
12561338#if defined(_AIX ) && (SIZEOF_TIME_T < 8 )
@@ -1281,15 +1363,19 @@ int
12811363_PyTime_gmtime (time_t t , struct tm * tm )
12821364{
12831365#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 ;
1366+ /* For non-negative timestamps, use standard conversion */
1367+ if (t >= 0 ) {
1368+ int error = gmtime_s (tm , & t );
1369+ if (error != 0 ) {
1370+ errno = error ;
1371+ PyErr_SetFromErrno (PyExc_OSError );
1372+ return -1 ;
1373+ }
1374+ return 0 ;
12911375 }
1292- return 0 ;
1376+
1377+ /* For negative timestamps, use FILETIME-based conversion */
1378+ return _PyTime_windows_filetime (t , tm , 0 );
12931379#else /* !MS_WINDOWS */
12941380 if (gmtime_r (& t , tm ) == NULL ) {
12951381#ifdef EINVAL
0 commit comments