Skip to content

Commit adfefa5

Browse files
committed
Issue #23517: Fix implementation of the ROUND_HALF_UP rounding mode in
datetime.datetime.fromtimestamp() and datetime.datetime.utcfromtimestamp(). microseconds sign should be kept before rounding.
1 parent 19bbb9a commit adfefa5

4 files changed

Lines changed: 42 additions & 39 deletions

File tree

Lib/datetime.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,48 +1374,42 @@ def tzinfo(self):
13741374
return self._tzinfo
13751375

13761376
@classmethod
1377-
def fromtimestamp(cls, t, tz=None):
1377+
def _fromtimestamp(cls, t, utc, tz):
13781378
"""Construct a datetime from a POSIX timestamp (like time.time()).
13791379
13801380
A timezone info object may be passed in as well.
13811381
"""
1382-
_check_tzinfo_arg(tz)
1383-
1384-
converter = _time.localtime if tz is None else _time.gmtime
1385-
1386-
t, frac = divmod(t, 1.0)
1382+
frac, t = _math.modf(t)
13871383
us = _round_half_up(frac * 1e6)
1388-
1389-
# If timestamp is less than one microsecond smaller than a
1390-
# full second, us can be rounded up to 1000000. In this case,
1391-
# roll over to seconds, otherwise, ValueError is raised
1392-
# by the constructor.
1393-
if us == 1000000:
1384+
if us >= 1000000:
13941385
t += 1
1395-
us = 0
1386+
us -= 1000000
1387+
elif us < 0:
1388+
t -= 1
1389+
us += 1000000
1390+
1391+
converter = _time.gmtime if utc else _time.localtime
13961392
y, m, d, hh, mm, ss, weekday, jday, dst = converter(t)
13971393
ss = min(ss, 59) # clamp out leap seconds if the platform has them
1398-
result = cls(y, m, d, hh, mm, ss, us, tz)
1394+
return cls(y, m, d, hh, mm, ss, us, tz)
1395+
1396+
@classmethod
1397+
def fromtimestamp(cls, t, tz=None):
1398+
"""Construct a datetime from a POSIX timestamp (like time.time()).
1399+
1400+
A timezone info object may be passed in as well.
1401+
"""
1402+
_check_tzinfo_arg(tz)
1403+
1404+
result = cls._fromtimestamp(t, tz is not None, tz)
13991405
if tz is not None:
14001406
result = tz.fromutc(result)
14011407
return result
14021408

14031409
@classmethod
14041410
def utcfromtimestamp(cls, t):
14051411
"""Construct a naive UTC datetime from a POSIX timestamp."""
1406-
t, frac = divmod(t, 1.0)
1407-
us = _round_half_up(frac * 1e6)
1408-
1409-
# If timestamp is less than one microsecond smaller than a
1410-
# full second, us can be rounded up to 1000000. In this case,
1411-
# roll over to seconds, otherwise, ValueError is raised
1412-
# by the constructor.
1413-
if us == 1000000:
1414-
t += 1
1415-
us = 0
1416-
y, m, d, hh, mm, ss, weekday, jday, dst = _time.gmtime(t)
1417-
ss = min(ss, 59) # clamp out leap seconds if the platform has them
1418-
return cls(y, m, d, hh, mm, ss, us)
1412+
return cls._fromtimestamp(t, True, None)
14191413

14201414
@classmethod
14211415
def now(cls, tz=None):

Lib/test/datetimetester.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,8 @@ def test_microsecond_rounding(self):
668668
eq(td(milliseconds=-0.6/1000), td(microseconds=-1))
669669
eq(td(seconds=0.5/10**6), td(microseconds=1))
670670
eq(td(seconds=-0.5/10**6), td(microseconds=-1))
671+
eq(td(seconds=1/2**7), td(microseconds=7813))
672+
eq(td(seconds=-1/2**7), td(microseconds=-7813))
671673

672674
# Rounding due to contributions from more than one field.
673675
us_per_hour = 3600e6
@@ -1842,8 +1844,8 @@ def test_timestamp_aware(self):
18421844
18000 + 3600 + 2*60 + 3 + 4*1e-6)
18431845

18441846
def test_microsecond_rounding(self):
1845-
for fts in [self.theclass.fromtimestamp,
1846-
self.theclass.utcfromtimestamp]:
1847+
for fts in (datetime.fromtimestamp,
1848+
self.theclass.utcfromtimestamp):
18471849
zero = fts(0)
18481850
self.assertEqual(zero.second, 0)
18491851
self.assertEqual(zero.microsecond, 0)
@@ -1874,6 +1876,12 @@ def test_microsecond_rounding(self):
18741876
t = fts(0.9999999)
18751877
self.assertEqual(t.second, 1)
18761878
self.assertEqual(t.microsecond, 0)
1879+
t = fts(1/2**7)
1880+
self.assertEqual(t.second, 0)
1881+
self.assertEqual(t.microsecond, 7813)
1882+
t = fts(-1/2**7)
1883+
self.assertEqual(t.second, 59)
1884+
self.assertEqual(t.microsecond, 992187)
18771885

18781886
def test_insane_fromtimestamp(self):
18791887
# It's possible that some platform maps time_t to double,

Lib/test/test_time.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@ def test_time_t(self):
655655
pytime_object_to_time_t, invalid, rnd)
656656

657657
@support.cpython_only
658-
def test_timespec(self):
658+
def test_object_to_timespec(self):
659659
from _testcapi import pytime_object_to_timespec
660660

661661
# Conversion giving the same result for all rounding methods
@@ -666,7 +666,7 @@ def test_timespec(self):
666666
(-1, (-1, 0)),
667667

668668
# float
669-
(-1.2, (-2, 800000000)),
669+
(-1/2**7, (-1, 992187500)),
670670
(-1.0, (-1, 0)),
671671
(-1e-9, (-1, 999999999)),
672672
(1e-9, (0, 1)),
@@ -693,7 +693,7 @@ def test_timespec(self):
693693

694694
(1.1234567890, (1, 123456789), FLOOR),
695695
(1.1234567899, (1, 123456789), FLOOR),
696-
(-1.1234567890, (-2, 876543211), FLOOR),
696+
(-1.1234567890, (-2, 876543210), FLOOR),
697697
(-1.1234567891, (-2, 876543210), FLOOR),
698698
# Round towards infinity (+inf)
699699
(1.1234567890, (1, 123456790), CEILING),
@@ -1155,7 +1155,7 @@ def test_time_t(self):
11551155
self.assertRaises(OverflowError,
11561156
pytime_object_to_time_t, invalid, rnd)
11571157

1158-
def test_timeval(self):
1158+
def test_object_to_timeval(self):
11591159
from _testcapi import pytime_object_to_timeval
11601160

11611161
# Conversion giving the same result for all rounding methods
@@ -1167,7 +1167,8 @@ def test_timeval(self):
11671167

11681168
# float
11691169
(-1.0, (-1, 0)),
1170-
(-1.2, (-2, 800000)),
1170+
(1/2**6, (0, 15625)),
1171+
(-1/2**6, (-1, 984375)),
11711172
(-1e-6, (-1, 999999)),
11721173
(1e-6, (0, 1)),
11731174
):
@@ -1225,7 +1226,7 @@ def test_timespec(self):
12251226
(-1.0, (-1, 0)),
12261227
(-1e-9, (-1, 999999999)),
12271228
(1e-9, (0, 1)),
1228-
(-1.2, (-2, 800000000)),
1229+
(-1/2**9, (-1, 998046875)),
12291230
):
12301231
with self.subTest(obj=obj, round=rnd, timespec=timespec):
12311232
self.assertEqual(pytime_object_to_timespec(obj, rnd),

Python/pytime.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,6 @@ _PyTime_DoubleToDenominator(double d, time_t *sec, long *numerator,
8282
volatile double floatpart;
8383

8484
floatpart = modf(d, &intpart);
85-
if (floatpart < 0) {
86-
floatpart += 1.0;
87-
intpart -= 1.0;
88-
}
8985

9086
floatpart *= denominator;
9187
if (round == _PyTime_ROUND_HALF_UP)
@@ -98,6 +94,10 @@ _PyTime_DoubleToDenominator(double d, time_t *sec, long *numerator,
9894
floatpart -= denominator;
9995
intpart += 1.0;
10096
}
97+
else if (floatpart < 0) {
98+
floatpart += denominator;
99+
intpart -= 1.0;
100+
}
101101
assert(0.0 <= floatpart && floatpart < denominator);
102102

103103
*sec = (time_t)intpart;

0 commit comments

Comments
 (0)