Skip to content

Commit 511491a

Browse files
committed
Issue #23517: Fix rounding in fromtimestamp() and utcfromtimestamp() methods
of datetime.datetime: microseconds are now rounded to nearest with ties going to nearest even integer (ROUND_HALF_EVEN), instead of being rounding towards zero (ROUND_DOWN). It's important that these methods use the same rounding mode than datetime.timedelta to keep the property: (datetime(1970,1,1) + timedelta(seconds=t)) == datetime.utcfromtimestamp(t) It also the rounding mode used by round(float) for example. Add more unit tests on the rounding mode in test_datetime.
1 parent e3bcbd2 commit 511491a

4 files changed

Lines changed: 113 additions & 41 deletions

File tree

Lib/datetime.py

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1362,49 +1362,42 @@ def tzinfo(self):
13621362
return self._tzinfo
13631363

13641364
@classmethod
1365-
def fromtimestamp(cls, t, tz=None):
1365+
def _fromtimestamp(cls, t, utc, tz):
13661366
"""Construct a datetime from a POSIX timestamp (like time.time()).
13671367
13681368
A timezone info object may be passed in as well.
13691369
"""
1370+
frac, t = _math.modf(t)
1371+
us = round(frac * 1e6)
1372+
if us >= 1000000:
1373+
t += 1
1374+
us -= 1000000
1375+
elif us < 0:
1376+
t -= 1
1377+
us += 1000000
13701378

1371-
_check_tzinfo_arg(tz)
1379+
converter = _time.gmtime if utc else _time.localtime
1380+
y, m, d, hh, mm, ss, weekday, jday, dst = converter(t)
1381+
ss = min(ss, 59) # clamp out leap seconds if the platform has them
1382+
return cls(y, m, d, hh, mm, ss, us, tz)
13721383

1373-
converter = _time.localtime if tz is None else _time.gmtime
1384+
@classmethod
1385+
def fromtimestamp(cls, t, tz=None):
1386+
"""Construct a datetime from a POSIX timestamp (like time.time()).
13741387
1375-
t, frac = divmod(t, 1.0)
1376-
us = int(frac * 1e6)
1388+
A timezone info object may be passed in as well.
1389+
"""
1390+
_check_tzinfo_arg(tz)
13771391

1378-
# If timestamp is less than one microsecond smaller than a
1379-
# full second, us can be rounded up to 1000000. In this case,
1380-
# roll over to seconds, otherwise, ValueError is raised
1381-
# by the constructor.
1382-
if us == 1000000:
1383-
t += 1
1384-
us = 0
1385-
y, m, d, hh, mm, ss, weekday, jday, dst = converter(t)
1386-
ss = min(ss, 59) # clamp out leap seconds if the platform has them
1387-
result = cls(y, m, d, hh, mm, ss, us, tz)
1392+
result = cls._fromtimestamp(t, tz is not None, tz)
13881393
if tz is not None:
13891394
result = tz.fromutc(result)
13901395
return result
13911396

13921397
@classmethod
13931398
def utcfromtimestamp(cls, t):
1394-
"Construct a UTC datetime from a POSIX timestamp (like time.time())."
1395-
t, frac = divmod(t, 1.0)
1396-
us = int(frac * 1e6)
1397-
1398-
# If timestamp is less than one microsecond smaller than a
1399-
# full second, us can be rounded up to 1000000. In this case,
1400-
# roll over to seconds, otherwise, ValueError is raised
1401-
# by the constructor.
1402-
if us == 1000000:
1403-
t += 1
1404-
us = 0
1405-
y, m, d, hh, mm, ss, weekday, jday, dst = _time.gmtime(t)
1406-
ss = min(ss, 59) # clamp out leap seconds if the platform has them
1407-
return cls(y, m, d, hh, mm, ss, us)
1399+
"""Construct a naive UTC datetime from a POSIX timestamp."""
1400+
return cls._fromtimestamp(t, True, None)
14081401

14091402
# XXX This is supposed to do better than we *can* do by using time.time(),
14101403
# XXX if the platform supports a more accurate way. The C implementation

Lib/test/datetimetester.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,16 @@ def test_microsecond_rounding(self):
650650
# Single-field rounding.
651651
eq(td(milliseconds=0.4/1000), td(0)) # rounds to 0
652652
eq(td(milliseconds=-0.4/1000), td(0)) # rounds to 0
653+
eq(td(milliseconds=0.5/1000), td(microseconds=0))
654+
eq(td(milliseconds=-0.5/1000), td(microseconds=-0))
653655
eq(td(milliseconds=0.6/1000), td(microseconds=1))
654656
eq(td(milliseconds=-0.6/1000), td(microseconds=-1))
657+
eq(td(milliseconds=1.5/1000), td(microseconds=2))
658+
eq(td(milliseconds=-1.5/1000), td(microseconds=-2))
659+
eq(td(seconds=0.5/10**6), td(microseconds=0))
660+
eq(td(seconds=-0.5/10**6), td(microseconds=-0))
661+
eq(td(seconds=1/2**7), td(microseconds=7812))
662+
eq(td(seconds=-1/2**7), td(microseconds=-7812))
655663

656664
# Rounding due to contributions from more than one field.
657665
us_per_hour = 3600e6
@@ -1824,12 +1832,14 @@ def test_timestamp_aware(self):
18241832
tzinfo=timezone(timedelta(hours=-5), 'EST'))
18251833
self.assertEqual(t.timestamp(),
18261834
18000 + 3600 + 2*60 + 3 + 4*1e-6)
1835+
18271836
def test_microsecond_rounding(self):
18281837
for fts in [self.theclass.fromtimestamp,
18291838
self.theclass.utcfromtimestamp]:
18301839
zero = fts(0)
18311840
self.assertEqual(zero.second, 0)
18321841
self.assertEqual(zero.microsecond, 0)
1842+
one = fts(1e-6)
18331843
try:
18341844
minus_one = fts(-1e-6)
18351845
except OSError:
@@ -1840,22 +1850,28 @@ def test_microsecond_rounding(self):
18401850
self.assertEqual(minus_one.microsecond, 999999)
18411851

18421852
t = fts(-1e-8)
1843-
self.assertEqual(t, minus_one)
1853+
self.assertEqual(t, zero)
18441854
t = fts(-9e-7)
18451855
self.assertEqual(t, minus_one)
18461856
t = fts(-1e-7)
1847-
self.assertEqual(t, minus_one)
1857+
self.assertEqual(t, zero)
1858+
t = fts(-1/2**7)
1859+
self.assertEqual(t.second, 59)
1860+
self.assertEqual(t.microsecond, 992188)
18481861

18491862
t = fts(1e-7)
18501863
self.assertEqual(t, zero)
18511864
t = fts(9e-7)
1852-
self.assertEqual(t, zero)
1865+
self.assertEqual(t, one)
18531866
t = fts(0.99999949)
18541867
self.assertEqual(t.second, 0)
18551868
self.assertEqual(t.microsecond, 999999)
18561869
t = fts(0.9999999)
1870+
self.assertEqual(t.second, 1)
1871+
self.assertEqual(t.microsecond, 0)
1872+
t = fts(1/2**7)
18571873
self.assertEqual(t.second, 0)
1858-
self.assertEqual(t.microsecond, 999999)
1874+
self.assertEqual(t.microsecond, 7812)
18591875

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

Misc/NEWS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ Core and Builtins
8181
Library
8282
-------
8383

84+
- Issue #23517: Fix rounding in fromtimestamp() and utcfromtimestamp() methods
85+
of datetime.datetime: microseconds are now rounded to nearest with ties
86+
going to nearest even integer (ROUND_HALF_EVEN), instead of being rounding
87+
towards zero (ROUND_DOWN). It's important that these methods use the same
88+
rounding mode than datetime.timedelta to keep the property:
89+
(datetime(1970,1,1) + timedelta(seconds=t)) == datetime.utcfromtimestamp(t).
90+
It also the rounding mode used by round(float) for example.
91+
8492
- Issue #24684: socket.socket.getaddrinfo() now calls
8593
PyUnicode_AsEncodedString() instead of calling the encode() method of the
8694
host, to handle correctly custom string with an encode() method which doesn't

Modules/_datetimemodule.c

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4113,6 +4113,44 @@ datetime_from_timet_and_us(PyObject *cls, TM_FUNC f, time_t timet, int us,
41134113
tzinfo);
41144114
}
41154115

4116+
static time_t
4117+
_PyTime_DoubleToTimet(double x)
4118+
{
4119+
time_t result;
4120+
double diff;
4121+
4122+
result = (time_t)x;
4123+
/* How much info did we lose? time_t may be an integral or
4124+
* floating type, and we don't know which. If it's integral,
4125+
* we don't know whether C truncates, rounds, returns the floor,
4126+
* etc. If we lost a second or more, the C rounding is
4127+
* unreasonable, or the input just doesn't fit in a time_t;
4128+
* call it an error regardless. Note that the original cast to
4129+
* time_t can cause a C error too, but nothing we can do to
4130+
* worm around that.
4131+
*/
4132+
diff = x - (double)result;
4133+
if (diff <= -1.0 || diff >= 1.0) {
4134+
PyErr_SetString(PyExc_OverflowError,
4135+
"timestamp out of range for platform time_t");
4136+
result = (time_t)-1;
4137+
}
4138+
return result;
4139+
}
4140+
4141+
/* Round a double to the nearest long. |x| must be small enough to fit
4142+
* in a C long; this is not checked.
4143+
*/
4144+
static double
4145+
_PyTime_RoundHalfEven(double x)
4146+
{
4147+
double rounded = round(x);
4148+
if (fabs(x-rounded) == 0.5)
4149+
/* halfway case: round to even */
4150+
rounded = 2.0*round(x/2.0);
4151+
return rounded;
4152+
}
4153+
41164154
/* Internal helper.
41174155
* Build datetime from a Python timestamp. Pass localtime or gmtime for f,
41184156
* to control the interpretation of the timestamp. Since a double doesn't
@@ -4121,15 +4159,32 @@ datetime_from_timet_and_us(PyObject *cls, TM_FUNC f, time_t timet, int us,
41214159
* to get that much precision (e.g., C time() isn't good enough).
41224160
*/
41234161
static PyObject *
4124-
datetime_from_timestamp(PyObject *cls, TM_FUNC f, PyObject *timestamp,
4162+
datetime_from_timestamp(PyObject *cls, TM_FUNC f, double timestamp,
41254163
PyObject *tzinfo)
41264164
{
41274165
time_t timet;
4128-
long us;
4166+
double fraction;
4167+
int us;
41294168

4130-
if (_PyTime_ObjectToTimeval(timestamp, &timet, &us, _PyTime_ROUND_DOWN) == -1)
4169+
timet = _PyTime_DoubleToTimet(timestamp);
4170+
if (timet == (time_t)-1 && PyErr_Occurred())
41314171
return NULL;
4132-
return datetime_from_timet_and_us(cls, f, timet, (int)us, tzinfo);
4172+
fraction = timestamp - (double)timet;
4173+
us = (int)_PyTime_RoundHalfEven(fraction * 1e6);
4174+
if (us < 0) {
4175+
/* Truncation towards zero is not what we wanted
4176+
for negative numbers (Python's mod semantics) */
4177+
timet -= 1;
4178+
us += 1000000;
4179+
}
4180+
/* If timestamp is less than one microsecond smaller than a
4181+
* full second, round up. Otherwise, ValueErrors are raised
4182+
* for some floats. */
4183+
if (us == 1000000) {
4184+
timet += 1;
4185+
us = 0;
4186+
}
4187+
return datetime_from_timet_and_us(cls, f, timet, us, tzinfo);
41334188
}
41344189

41354190
/* Internal helper.
@@ -4231,11 +4286,11 @@ static PyObject *
42314286
datetime_fromtimestamp(PyObject *cls, PyObject *args, PyObject *kw)
42324287
{
42334288
PyObject *self;
4234-
PyObject *timestamp;
4289+
double timestamp;
42354290
PyObject *tzinfo = Py_None;
42364291
static char *keywords[] = {"timestamp", "tz", NULL};
42374292

4238-
if (! PyArg_ParseTupleAndKeywords(args, kw, "O|O:fromtimestamp",
4293+
if (! PyArg_ParseTupleAndKeywords(args, kw, "d|O:fromtimestamp",
42394294
keywords, &timestamp, &tzinfo))
42404295
return NULL;
42414296
if (check_tzinfo_subclass(tzinfo) < 0)
@@ -4259,10 +4314,10 @@ datetime_fromtimestamp(PyObject *cls, PyObject *args, PyObject *kw)
42594314
static PyObject *
42604315
datetime_utcfromtimestamp(PyObject *cls, PyObject *args)
42614316
{
4262-
PyObject *timestamp;
4317+
double timestamp;
42634318
PyObject *result = NULL;
42644319

4265-
if (PyArg_ParseTuple(args, "O:utcfromtimestamp", &timestamp))
4320+
if (PyArg_ParseTuple(args, "d:utcfromtimestamp", &timestamp))
42664321
result = datetime_from_timestamp(cls, gmtime, timestamp,
42674322
Py_None);
42684323
return result;

0 commit comments

Comments
 (0)