Skip to content

Commit 9ab4d23

Browse files
author
Jiekang Tian
committed
gh-148658: Fix DST detection for negative timestamps on Windows
1 parent 1d0c65f commit 9ab4d23

2 files changed

Lines changed: 51 additions & 10 deletions

File tree

Lib/test/datetimetester.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5663,12 +5663,34 @@ def test_astimezone_default_near_fold(self):
56635663

56645664
@unittest.skipIf(sys.platform != "win32", "gh-148658 only affects Windows")
56655665
def test_astimezone_negative_timestamp_dst(self):
5666-
# gh-148658: astimezone() returned DST name for negative timestamps
5667-
# on Windows. Verify that the timezone name is consistent between
5668-
# negative and non-negative timestamps.
5669-
dt_neg1 = self.theclass.fromtimestamp(-1).astimezone()
5670-
dt_zero = self.theclass.fromtimestamp(0).astimezone()
5671-
self.assertEqual(dt_neg1.tzname(), dt_zero.tzname())
5666+
# gh-148658: astimezone() returned incorrect DST name for negative
5667+
# timestamps on Windows. Verify C layer returns correct tm_zone
5668+
# by checking astimezone() produces correct timezone names.
5669+
import time
5670+
winter_ts = -1 # 1969-12-31
5671+
summer_ts = -15897600 # 1969-07-01
5672+
5673+
# Get expected timezone names from time.localtime
5674+
winter_local = time.localtime(winter_ts)
5675+
summer_local = time.localtime(summer_ts)
5676+
5677+
dt_winter = self.theclass.fromtimestamp(winter_ts).astimezone()
5678+
dt_summer = self.theclass.fromtimestamp(summer_ts).astimezone()
5679+
5680+
# Verify astimezone() returns correct timezone names
5681+
self.assertEqual(dt_winter.tzname(), winter_local.tm_zone)
5682+
self.assertEqual(dt_summer.tzname(), summer_local.tm_zone)
5683+
5684+
# For DST timezones, winter and summer should have different names
5685+
# For non-DST timezones, names should be the same
5686+
if winter_local.tm_zone == summer_local.tm_zone:
5687+
# Non-DST timezone: both should be standard time
5688+
self.assertEqual(winter_local.tm_isdst, 0)
5689+
self.assertEqual(summer_local.tm_isdst, 0)
5690+
else:
5691+
# DST timezone: one should be standard, one DST
5692+
self.assertEqual(winter_local.tm_isdst, 0)
5693+
self.assertEqual(summer_local.tm_isdst, 1)
56725694

56735695
def test_aware_subtract(self):
56745696
cls = self.theclass

Python/pytime.c

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,18 +357,37 @@ _PyTime_windows_filetime(time_t timer, struct tm *tm, int is_local)
357357
tm->tm_yday = _PyTime_calc_yday(&st_result);
358358

359359
/* DST flag: -1 (unknown) for local time on historical dates, 0 for UTC.
360-
* For timezones that don't observe DST, set tm_isdst to 0 to ensure
361-
* strftime('%Z') returns the standard time name (gh-148658). */
360+
* Detect DST by comparing standard time with local time (gh-148658). */
362361
if (is_local) {
363362
TIME_ZONE_INFORMATION tzi;
364363
DWORD tz_result = GetTimeZoneInformation(&tzi);
365-
if (tz_result != TIME_ZONE_ID_INVALID &&
364+
if (tz_result == TIME_ZONE_ID_INVALID ||
366365
tzi.DaylightDate.wMonth == 0) {
367366
/* Timezone does not observe DST */
368367
tm->tm_isdst = 0;
369368
}
370369
else {
371-
tm->tm_isdst = -1;
370+
/* Calculate standard time from UTC and compare with local time. */
371+
ULONGLONG utc_ticks = ((ULONGLONG)timer + SECS_BETWEEN_EPOCHS) *
372+
HUNDRED_NS_PER_SEC;
373+
LONGLONG standard_offset_ticks = (LONGLONG)tzi.Bias * 60LL *
374+
HUNDRED_NS_PER_SEC;
375+
ULONGLONG standard_ticks = utc_ticks - standard_offset_ticks;
376+
FILETIME ft_standard;
377+
ft_standard.dwLowDateTime = (DWORD)(standard_ticks);
378+
ft_standard.dwHighDateTime = (DWORD)(standard_ticks >> 32);
379+
SYSTEMTIME st_standard;
380+
if (!FileTimeToSystemTime(&ft_standard, &st_standard)) {
381+
PyErr_SetFromWindowsErr(0);
382+
return -1;
383+
}
384+
if (st_result.wHour == st_standard.wHour &&
385+
st_result.wMinute == st_standard.wMinute) {
386+
tm->tm_isdst = 0;
387+
}
388+
else {
389+
tm->tm_isdst = 1;
390+
}
372391
}
373392
}
374393
else {

0 commit comments

Comments
 (0)