Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 38 additions & 50 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -2706,24 +2706,20 @@ def utcfromtimestamp(*args, **kwargs):
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)

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)
minus_one = fts(-1e-6)

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)
Expand Down Expand Up @@ -2752,22 +2748,18 @@ def utcfromtimestamp(*args, **kwargs):
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'))

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)
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)
Expand Down Expand Up @@ -2803,22 +2795,18 @@ def utcfromtimestamp(*args, **kwargs):
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)
Expand Down Expand Up @@ -2860,6 +2848,7 @@ def test_timestamp_limits(self):
# 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)
Expand Down Expand Up @@ -2899,6 +2888,7 @@ def test_fromtimestamp_limits(self):
# 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:
Expand Down Expand Up @@ -2960,13 +2950,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)
Expand Down
3 changes: 2 additions & 1 deletion Lib/test/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,11 @@ def test_mktime(self):
for t in (-2, -1, 0, 1):
try:
tt = time.localtime(t)
ts = time.mktime(tt)
except (OverflowError, OSError):
pass
else:
self.assertEqual(time.mktime(tt), t)
self.assertEqual(ts, t)

Comment thread
vstinner marked this conversation as resolved.
# Issue #13309: passing extreme values to mktime() or localtime()
# borks the glibc's internal timezone data.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support negative timestamps in various :mod:`datetime` functions.
17 changes: 1 addition & 16 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
126 changes: 106 additions & 20 deletions Python/pytime.c
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,86 @@ _PyTime_AsCLong(PyTime_t t, long *t2)
*t2 = (long)t;
return 0;
}

// 369 years + 89 leap days
#define SECS_BETWEEN_EPOCHS 11644473600LL /* Seconds between 1601-01-01 and 1970-01-01 */
Comment thread
gesslerpd marked this conversation as resolved.
Outdated
#define HUNDRED_NS_PER_SEC 10000000LL

// Convert time_t to struct tm using Windows FILETIME API.
// If is_local is true, convert to local time. */
Comment thread
gesslerpd marked this conversation as resolved.
Outdated
// 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 */

/* Calculate day of year using Windows FILETIME difference */
// SYSTEMTIME st_jan1 = {st_result.wYear, 1, 0, 1, 0, 0, 0, 0};
// FILETIME ft_jan1, ft_date;
// if (!SystemTimeToFileTime(&st_jan1, &ft_jan1) ||
// !SystemTimeToFileTime(&st_result, &ft_date)) {
// PyErr_SetFromWindowsErr(0);
// return -1;
// }
// ULARGE_INTEGER jan1, date;
// jan1.LowPart = ft_jan1.dwLowDateTime;
// jan1.HighPart = ft_jan1.dwHighDateTime;
// date.LowPart = ft_date.dwLowDateTime;
// date.HighPart = ft_date.dwHighDateTime;
// /* Convert 100-nanosecond intervals to days */
// LONGLONG days_diff = (date.QuadPart - jan1.QuadPart) / (24LL * 60 * 60 * HUNDRED_NS_PER_SEC);

// tm->tm_yday = (int)days_diff;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove commented/dead code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think that yday is important to have? Doesn't impact datetime AFAIK but it does show up in struct_time objects returned from gmtime/localtime. For completeness with respect to time module it could be put back in.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, it would be better to fill tm_yday as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored tm_yday calculation, I think the windows API version was a bit heavy so simplified it by adding custom util function.


// datetime doesn't rely on tm_yday, so set invalid value and skip calculation
// time.gmtime / time.localtime will return struct_time with out of range tm_yday
// time.mktime doesn't support pre-epoch struct_time on windows anyway
tm->tm_yday = -1;

/* DST flag: -1 (unknown) for local time on historical dates, 0 for UTC */
tm->tm_isdst = is_local ? -1 : 0;

return 0;
}
#endif


Expand Down Expand Up @@ -882,10 +962,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
Expand Down Expand Up @@ -1242,15 +1320,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 standard conversion */
Comment thread
gesslerpd marked this conversation as resolved.
Outdated
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)
Expand Down Expand Up @@ -1281,15 +1363,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 standard conversion */
Comment thread
gesslerpd marked this conversation as resolved.
Outdated
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
Expand Down
Loading