Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
26 changes: 24 additions & 2 deletions Lib/test/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,27 @@ def test_epoch(self):
# Only test the date and time, ignore other gmtime() members
self.assertEqual(tuple(epoch)[:6], (1970, 1, 1, 0, 0, 0), epoch)

def test_gmtime(self):
# expected format:
# (tm_year, tm_mon, tm_mday,
# tm_hour, tm_min, tm_sec,
# tm_wday, tm_yday)
for t, expected in (
(-13262400, (1969, 7, 31, 12, 0, 0, 3, 212)),
(-6177600, (1969, 10, 21, 12, 0, 0, 1, 294)),
# non-leap years (pre epoch)
(-2203891200, (1900, 3, 1, 0, 0, 0, 3, 60)),
(-2203977600, (1900, 2, 28, 0, 0, 0, 2, 59)),
(-5359564800, (1800, 3, 1, 0, 0, 0, 5, 60)),
(-5359651200, (1800, 2, 28, 0, 0, 0, 4, 59)),
# leap years (pre epoch)
(-2077660800, (1904, 3, 1, 0, 0, 0, 1, 61)),
(-2077833600, (1904, 2, 28, 0, 0, 0, 6, 59)),
):
with self.subTest(t=t, expected=expected):
res = time.gmtime(t)
self.assertEqual(tuple(res)[:8], expected, res)

def test_strftime(self):
tt = time.gmtime(self.t)
for directive in ('a', 'A', 'b', 'B', 'c', 'd', 'H', 'I',
Expand Down Expand Up @@ -501,12 +522,13 @@ def test_localtime_without_arg(self):
def test_mktime(self):
# Issue #1726687
for t in (-2, -1, 0, 1):
t_struct = time.localtime(t)
try:
tt = time.localtime(t)
t1 = time.mktime(t_struct)
except (OverflowError, OSError):
pass
else:
self.assertEqual(time.mktime(tt), t)
self.assertEqual(t1, t)

# 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 :func:`time.gmtime`, :func:`time.localtime`, and 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
129 changes: 109 additions & 20 deletions Python/pytime.c
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,89 @@ _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 */
#define HUNDRED_NS_PER_SEC 10000000LL

// Calculate day of year (0-365) from SYSTEMTIME
static int
_PyTime_calc_yday(const SYSTEMTIME *st)
{
// Cumulative days before each month (non-leap year)
static const int days_before_month[] = {
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
};
int yday = days_before_month[st->wMonth - 1] + st->wDay - 1;
// Account for leap day if we're past February in a leap year.
if (st->wMonth > 2) {
// Leap year rules (Gregorian calendar):
// - Years divisible by 4 are leap years
// - EXCEPT years divisible by 100 are NOT leap years
// - EXCEPT years divisible by 400 ARE leap years
int year = st->wYear;
int is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
yday += is_leap;
}
return yday;
}

// Convert time_t to struct tm using Windows FILETIME API.
// If is_local is true, convert to local time. */
// 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 */

// `time.gmtime` and `time.localtime` will return `struct_time` containing this
// not currently used by `datetime` module
Comment on lines +350 to +351
Copy link
Member

Choose a reason for hiding this comment

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

This function is for the time module. I don't think that this comment about the datetime module is relevant, you can remove it.

tm->tm_yday = _PyTime_calc_yday(&st_result);

/* 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 +965,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 +1323,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 */
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/* For non-negative timestamps, use standard conversion */
/* For non-negative timestamps, use localtime_s() */

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 +1366,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 */
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/* For non-negative timestamps, use standard conversion */
/* For non-negative timestamps, use gmtime_s() */

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