Kaydet (Commit) 992c43fe authored tarafından Victor Stinner's avatar Victor Stinner

Issue #22117: Fix rounding in _PyTime_FromSecondsObject()

* Rename _PyTime_FromObject() to _PyTime_FromSecondsObject()
* Add _PyTime_AsNanosecondsObject() and _testcapi.pytime_fromsecondsobject()
* Add unit tests
üst 79644f9c
...@@ -121,15 +121,18 @@ typedef PY_INT64_T _PyTime_t; ...@@ -121,15 +121,18 @@ typedef PY_INT64_T _PyTime_t;
/* Convert a Python float or int to a timetamp. /* Convert a Python float or int to a timetamp.
Raise an exception and return -1 on error, return 0 on success. */ Raise an exception and return -1 on error, return 0 on success. */
PyAPI_FUNC(int) _PyTime_FromObject(_PyTime_t *t, PyAPI_FUNC(int) _PyTime_FromSecondsObject(_PyTime_t *t,
PyObject *obj, PyObject *obj,
_PyTime_round_t round); _PyTime_round_t round);
/* Convert timestamp to a number of milliseconds (10^-3 seconds). */ /* Convert timestamp to a number of milliseconds (10^-3 seconds). */
PyAPI_FUNC(_PyTime_t) PyAPI_FUNC(_PyTime_t) _PyTime_AsMilliseconds(_PyTime_t t,
_PyTime_AsMilliseconds(_PyTime_t t,
_PyTime_round_t round); _PyTime_round_t round);
/* Convert timestamp to a number of nanoseconds (10^-9 seconds) as a Python int
object. */
PyAPI_FUNC(PyObject *) _PyTime_AsNanosecondsObject(_PyTime_t t);
/* Convert a timestamp to a timeval structure. */ /* Convert a timestamp to a timeval structure. */
PyAPI_FUNC(int) _PyTime_AsTimeval(_PyTime_t t, PyAPI_FUNC(int) _PyTime_AsTimeval(_PyTime_t t,
struct timeval *tv, struct timeval *tv,
......
from test import support from test import support
import time import enum
import unittest
import locale import locale
import sysconfig
import sys
import platform import platform
import sys
import sysconfig
import time
import unittest
try: try:
import threading import threading
except ImportError: except ImportError:
...@@ -14,8 +15,15 @@ except ImportError: ...@@ -14,8 +15,15 @@ except ImportError:
SIZEOF_INT = sysconfig.get_config_var('SIZEOF_INT') or 4 SIZEOF_INT = sysconfig.get_config_var('SIZEOF_INT') or 4
TIME_MAXYEAR = (1 << 8 * SIZEOF_INT - 1) - 1 TIME_MAXYEAR = (1 << 8 * SIZEOF_INT - 1) - 1
TIME_MINYEAR = -TIME_MAXYEAR - 1 TIME_MINYEAR = -TIME_MAXYEAR - 1
_PyTime_ROUND_DOWN = 0
_PyTime_ROUND_UP = 1
class _PyTime(enum.IntEnum):
# Round towards zero
ROUND_DOWN = 0
# Round away from zero
ROUND_UP = 1
ALL_ROUNDING_METHODS = (_PyTime.ROUND_UP, _PyTime.ROUND_DOWN)
class TimeTestCase(unittest.TestCase): class TimeTestCase(unittest.TestCase):
...@@ -596,23 +604,23 @@ class TestPytime(unittest.TestCase): ...@@ -596,23 +604,23 @@ class TestPytime(unittest.TestCase):
from _testcapi import pytime_object_to_time_t from _testcapi import pytime_object_to_time_t
for obj, time_t, rnd in ( for obj, time_t, rnd in (
# Round towards zero # Round towards zero
(0, 0, _PyTime_ROUND_DOWN), (0, 0, _PyTime.ROUND_DOWN),
(-1, -1, _PyTime_ROUND_DOWN), (-1, -1, _PyTime.ROUND_DOWN),
(-1.0, -1, _PyTime_ROUND_DOWN), (-1.0, -1, _PyTime.ROUND_DOWN),
(-1.9, -1, _PyTime_ROUND_DOWN), (-1.9, -1, _PyTime.ROUND_DOWN),
(1.0, 1, _PyTime_ROUND_DOWN), (1.0, 1, _PyTime.ROUND_DOWN),
(1.9, 1, _PyTime_ROUND_DOWN), (1.9, 1, _PyTime.ROUND_DOWN),
# Round away from zero # Round away from zero
(0, 0, _PyTime_ROUND_UP), (0, 0, _PyTime.ROUND_UP),
(-1, -1, _PyTime_ROUND_UP), (-1, -1, _PyTime.ROUND_UP),
(-1.0, -1, _PyTime_ROUND_UP), (-1.0, -1, _PyTime.ROUND_UP),
(-1.9, -2, _PyTime_ROUND_UP), (-1.9, -2, _PyTime.ROUND_UP),
(1.0, 1, _PyTime_ROUND_UP), (1.0, 1, _PyTime.ROUND_UP),
(1.9, 2, _PyTime_ROUND_UP), (1.9, 2, _PyTime.ROUND_UP),
): ):
self.assertEqual(pytime_object_to_time_t(obj, rnd), time_t) self.assertEqual(pytime_object_to_time_t(obj, rnd), time_t)
rnd = _PyTime_ROUND_DOWN rnd = _PyTime.ROUND_DOWN
for invalid in self.invalid_values: for invalid in self.invalid_values:
self.assertRaises(OverflowError, self.assertRaises(OverflowError,
pytime_object_to_time_t, invalid, rnd) pytime_object_to_time_t, invalid, rnd)
...@@ -622,44 +630,44 @@ class TestPytime(unittest.TestCase): ...@@ -622,44 +630,44 @@ class TestPytime(unittest.TestCase):
from _testcapi import pytime_object_to_timeval from _testcapi import pytime_object_to_timeval
for obj, timeval, rnd in ( for obj, timeval, rnd in (
# Round towards zero # Round towards zero
(0, (0, 0), _PyTime_ROUND_DOWN), (0, (0, 0), _PyTime.ROUND_DOWN),
(-1, (-1, 0), _PyTime_ROUND_DOWN), (-1, (-1, 0), _PyTime.ROUND_DOWN),
(-1.0, (-1, 0), _PyTime_ROUND_DOWN), (-1.0, (-1, 0), _PyTime.ROUND_DOWN),
(1e-6, (0, 1), _PyTime_ROUND_DOWN), (1e-6, (0, 1), _PyTime.ROUND_DOWN),
(1e-7, (0, 0), _PyTime_ROUND_DOWN), (1e-7, (0, 0), _PyTime.ROUND_DOWN),
(-1e-6, (-1, 999999), _PyTime_ROUND_DOWN), (-1e-6, (-1, 999999), _PyTime.ROUND_DOWN),
(-1e-7, (-1, 999999), _PyTime_ROUND_DOWN), (-1e-7, (-1, 999999), _PyTime.ROUND_DOWN),
(-1.2, (-2, 800000), _PyTime_ROUND_DOWN), (-1.2, (-2, 800000), _PyTime.ROUND_DOWN),
(0.9999999, (0, 999999), _PyTime_ROUND_DOWN), (0.9999999, (0, 999999), _PyTime.ROUND_DOWN),
(0.0000041, (0, 4), _PyTime_ROUND_DOWN), (0.0000041, (0, 4), _PyTime.ROUND_DOWN),
(1.1234560, (1, 123456), _PyTime_ROUND_DOWN), (1.1234560, (1, 123456), _PyTime.ROUND_DOWN),
(1.1234569, (1, 123456), _PyTime_ROUND_DOWN), (1.1234569, (1, 123456), _PyTime.ROUND_DOWN),
(-0.0000040, (-1, 999996), _PyTime_ROUND_DOWN), (-0.0000040, (-1, 999996), _PyTime.ROUND_DOWN),
(-0.0000041, (-1, 999995), _PyTime_ROUND_DOWN), (-0.0000041, (-1, 999995), _PyTime.ROUND_DOWN),
(-1.1234560, (-2, 876544), _PyTime_ROUND_DOWN), (-1.1234560, (-2, 876544), _PyTime.ROUND_DOWN),
(-1.1234561, (-2, 876543), _PyTime_ROUND_DOWN), (-1.1234561, (-2, 876543), _PyTime.ROUND_DOWN),
# Round away from zero # Round away from zero
(0, (0, 0), _PyTime_ROUND_UP), (0, (0, 0), _PyTime.ROUND_UP),
(-1, (-1, 0), _PyTime_ROUND_UP), (-1, (-1, 0), _PyTime.ROUND_UP),
(-1.0, (-1, 0), _PyTime_ROUND_UP), (-1.0, (-1, 0), _PyTime.ROUND_UP),
(1e-6, (0, 1), _PyTime_ROUND_UP), (1e-6, (0, 1), _PyTime.ROUND_UP),
(1e-7, (0, 1), _PyTime_ROUND_UP), (1e-7, (0, 1), _PyTime.ROUND_UP),
(-1e-6, (-1, 999999), _PyTime_ROUND_UP), (-1e-6, (-1, 999999), _PyTime.ROUND_UP),
(-1e-7, (-1, 999999), _PyTime_ROUND_UP), (-1e-7, (-1, 999999), _PyTime.ROUND_UP),
(-1.2, (-2, 800000), _PyTime_ROUND_UP), (-1.2, (-2, 800000), _PyTime.ROUND_UP),
(0.9999999, (1, 0), _PyTime_ROUND_UP), (0.9999999, (1, 0), _PyTime.ROUND_UP),
(0.0000041, (0, 5), _PyTime_ROUND_UP), (0.0000041, (0, 5), _PyTime.ROUND_UP),
(1.1234560, (1, 123457), _PyTime_ROUND_UP), (1.1234560, (1, 123457), _PyTime.ROUND_UP),
(1.1234569, (1, 123457), _PyTime_ROUND_UP), (1.1234569, (1, 123457), _PyTime.ROUND_UP),
(-0.0000040, (-1, 999996), _PyTime_ROUND_UP), (-0.0000040, (-1, 999996), _PyTime.ROUND_UP),
(-0.0000041, (-1, 999995), _PyTime_ROUND_UP), (-0.0000041, (-1, 999995), _PyTime.ROUND_UP),
(-1.1234560, (-2, 876544), _PyTime_ROUND_UP), (-1.1234560, (-2, 876544), _PyTime.ROUND_UP),
(-1.1234561, (-2, 876543), _PyTime_ROUND_UP), (-1.1234561, (-2, 876543), _PyTime.ROUND_UP),
): ):
with self.subTest(obj=obj, round=rnd, timeval=timeval): with self.subTest(obj=obj, round=rnd, timeval=timeval):
self.assertEqual(pytime_object_to_timeval(obj, rnd), timeval) self.assertEqual(pytime_object_to_timeval(obj, rnd), timeval)
rnd = _PyTime_ROUND_DOWN rnd = _PyTime.ROUND_DOWN
for invalid in self.invalid_values: for invalid in self.invalid_values:
self.assertRaises(OverflowError, self.assertRaises(OverflowError,
pytime_object_to_timeval, invalid, rnd) pytime_object_to_timeval, invalid, rnd)
...@@ -669,38 +677,38 @@ class TestPytime(unittest.TestCase): ...@@ -669,38 +677,38 @@ class TestPytime(unittest.TestCase):
from _testcapi import pytime_object_to_timespec from _testcapi import pytime_object_to_timespec
for obj, timespec, rnd in ( for obj, timespec, rnd in (
# Round towards zero # Round towards zero
(0, (0, 0), _PyTime_ROUND_DOWN), (0, (0, 0), _PyTime.ROUND_DOWN),
(-1, (-1, 0), _PyTime_ROUND_DOWN), (-1, (-1, 0), _PyTime.ROUND_DOWN),
(-1.0, (-1, 0), _PyTime_ROUND_DOWN), (-1.0, (-1, 0), _PyTime.ROUND_DOWN),
(1e-9, (0, 1), _PyTime_ROUND_DOWN), (1e-9, (0, 1), _PyTime.ROUND_DOWN),
(1e-10, (0, 0), _PyTime_ROUND_DOWN), (1e-10, (0, 0), _PyTime.ROUND_DOWN),
(-1e-9, (-1, 999999999), _PyTime_ROUND_DOWN), (-1e-9, (-1, 999999999), _PyTime.ROUND_DOWN),
(-1e-10, (-1, 999999999), _PyTime_ROUND_DOWN), (-1e-10, (-1, 999999999), _PyTime.ROUND_DOWN),
(-1.2, (-2, 800000000), _PyTime_ROUND_DOWN), (-1.2, (-2, 800000000), _PyTime.ROUND_DOWN),
(0.9999999999, (0, 999999999), _PyTime_ROUND_DOWN), (0.9999999999, (0, 999999999), _PyTime.ROUND_DOWN),
(1.1234567890, (1, 123456789), _PyTime_ROUND_DOWN), (1.1234567890, (1, 123456789), _PyTime.ROUND_DOWN),
(1.1234567899, (1, 123456789), _PyTime_ROUND_DOWN), (1.1234567899, (1, 123456789), _PyTime.ROUND_DOWN),
(-1.1234567890, (-2, 876543211), _PyTime_ROUND_DOWN), (-1.1234567890, (-2, 876543211), _PyTime.ROUND_DOWN),
(-1.1234567891, (-2, 876543210), _PyTime_ROUND_DOWN), (-1.1234567891, (-2, 876543210), _PyTime.ROUND_DOWN),
# Round away from zero # Round away from zero
(0, (0, 0), _PyTime_ROUND_UP), (0, (0, 0), _PyTime.ROUND_UP),
(-1, (-1, 0), _PyTime_ROUND_UP), (-1, (-1, 0), _PyTime.ROUND_UP),
(-1.0, (-1, 0), _PyTime_ROUND_UP), (-1.0, (-1, 0), _PyTime.ROUND_UP),
(1e-9, (0, 1), _PyTime_ROUND_UP), (1e-9, (0, 1), _PyTime.ROUND_UP),
(1e-10, (0, 1), _PyTime_ROUND_UP), (1e-10, (0, 1), _PyTime.ROUND_UP),
(-1e-9, (-1, 999999999), _PyTime_ROUND_UP), (-1e-9, (-1, 999999999), _PyTime.ROUND_UP),
(-1e-10, (-1, 999999999), _PyTime_ROUND_UP), (-1e-10, (-1, 999999999), _PyTime.ROUND_UP),
(-1.2, (-2, 800000000), _PyTime_ROUND_UP), (-1.2, (-2, 800000000), _PyTime.ROUND_UP),
(0.9999999999, (1, 0), _PyTime_ROUND_UP), (0.9999999999, (1, 0), _PyTime.ROUND_UP),
(1.1234567890, (1, 123456790), _PyTime_ROUND_UP), (1.1234567890, (1, 123456790), _PyTime.ROUND_UP),
(1.1234567899, (1, 123456790), _PyTime_ROUND_UP), (1.1234567899, (1, 123456790), _PyTime.ROUND_UP),
(-1.1234567890, (-2, 876543211), _PyTime_ROUND_UP), (-1.1234567890, (-2, 876543211), _PyTime.ROUND_UP),
(-1.1234567891, (-2, 876543210), _PyTime_ROUND_UP), (-1.1234567891, (-2, 876543210), _PyTime.ROUND_UP),
): ):
with self.subTest(obj=obj, round=rnd, timespec=timespec): with self.subTest(obj=obj, round=rnd, timespec=timespec):
self.assertEqual(pytime_object_to_timespec(obj, rnd), timespec) self.assertEqual(pytime_object_to_timespec(obj, rnd), timespec)
rnd = _PyTime_ROUND_DOWN rnd = _PyTime.ROUND_DOWN
for invalid in self.invalid_values: for invalid in self.invalid_values:
self.assertRaises(OverflowError, self.assertRaises(OverflowError,
pytime_object_to_timespec, invalid, rnd) pytime_object_to_timespec, invalid, rnd)
...@@ -759,5 +767,91 @@ class TestPytime(unittest.TestCase): ...@@ -759,5 +767,91 @@ class TestPytime(unittest.TestCase):
self.assertIs(lt.tm_zone, None) self.assertIs(lt.tm_zone, None)
@support.cpython_only
class TestPyTime_t(unittest.TestCase):
def test_FromSecondsObject(self):
from _testcapi import pytime_fromsecondsobject
SEC_TO_NS = 10 ** 9
MAX_SEC = 2 ** 63 // 10 ** 9
# Conversion giving the same result for all rounding methods
for rnd in ALL_ROUNDING_METHODS:
for obj, ts in (
# integers
(0, 0),
(1, SEC_TO_NS),
(-3, -3 * SEC_TO_NS),
# float: subseconds
(0.0, 0),
(1e-9, 1),
(1e-6, 10 ** 3),
(1e-3, 10 ** 6),
# float: seconds
(2.0, 2 * SEC_TO_NS),
(123.0, 123 * SEC_TO_NS),
(-7.0, -7 * SEC_TO_NS),
# nanosecond are kept for value <= 2^23 seconds
(2**22 - 1e-9, 4194303999999999),
(2**22, 4194304000000000),
(2**22 + 1e-9, 4194304000000001),
(2**23 - 1e-9, 8388607999999999),
(2**23, 8388608000000000),
# start loosing precision for value > 2^23 seconds
(2**23 + 1e-9, 8388608000000002),
# nanoseconds are lost for value > 2^23 seconds
(2**24 - 1e-9, 16777215999999998),
(2**24, 16777216000000000),
(2**24 + 1e-9, 16777216000000000),
(2**25 - 1e-9, 33554432000000000),
(2**25 , 33554432000000000),
(2**25 + 1e-9, 33554432000000000),
# close to 2^63 nanoseconds
(9223372036, 9223372036 * SEC_TO_NS),
(9223372036.0, 9223372036 * SEC_TO_NS),
(-9223372036, -9223372036 * SEC_TO_NS),
(-9223372036.0, -9223372036 * SEC_TO_NS),
):
with self.subTest(obj=obj, round=rnd, timestamp=ts):
self.assertEqual(pytime_fromsecondsobject(obj, rnd), ts)
with self.subTest(round=rnd):
with self.assertRaises(OverflowError):
pytime_fromsecondsobject(9223372037, rnd)
pytime_fromsecondsobject(9223372037.0, rnd)
pytime_fromsecondsobject(-9223372037, rnd)
pytime_fromsecondsobject(-9223372037.0, rnd)
# Conversion giving different results depending on the rounding method
UP = _PyTime.ROUND_UP
DOWN = _PyTime.ROUND_DOWN
for obj, ts, rnd in (
# close to zero
( 1e-10, 1, UP),
( 1e-10, 0, DOWN),
(-1e-10, 0, DOWN),
(-1e-10, -1, UP),
# test rounding of the last nanosecond
( 1.1234567899, 1123456790, UP),
( 1.1234567899, 1123456789, DOWN),
(-1.1234567899, -1123456789, DOWN),
(-1.1234567899, -1123456790, UP),
# close to 1 second
( 0.9999999999, 1000000000, UP),
( 0.9999999999, 999999999, DOWN),
(-0.9999999999, -999999999, DOWN),
(-0.9999999999, -1000000000, UP),
):
with self.subTest(obj=obj, round=rnd, timestamp=ts):
self.assertEqual(pytime_fromsecondsobject(obj, rnd), ts)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
...@@ -3378,6 +3378,22 @@ return_result_with_error(PyObject *self, PyObject *args) ...@@ -3378,6 +3378,22 @@ return_result_with_error(PyObject *self, PyObject *args)
Py_RETURN_NONE; Py_RETURN_NONE;
} }
static PyObject *
test_pytime_fromsecondsobject(PyObject *self, PyObject *args)
{
PyObject *obj;
int round;
_PyTime_t ts;
if (!PyArg_ParseTuple(args, "Oi", &obj, &round))
return NULL;
if (check_time_rounding(round) < 0)
return NULL;
if (_PyTime_FromSecondsObject(&ts, obj, round) == -1)
return NULL;
return _PyTime_AsNanosecondsObject(ts);
}
static PyMethodDef TestMethods[] = { static PyMethodDef TestMethods[] = {
{"raise_exception", raise_exception, METH_VARARGS}, {"raise_exception", raise_exception, METH_VARARGS},
...@@ -3541,6 +3557,7 @@ static PyMethodDef TestMethods[] = { ...@@ -3541,6 +3557,7 @@ static PyMethodDef TestMethods[] = {
return_null_without_error, METH_NOARGS}, return_null_without_error, METH_NOARGS},
{"return_result_with_error", {"return_result_with_error",
return_result_with_error, METH_NOARGS}, return_result_with_error, METH_NOARGS},
{"pytime_fromsecondsobject", test_pytime_fromsecondsobject, METH_VARARGS},
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
}; };
......
...@@ -221,7 +221,7 @@ static PyObject * ...@@ -221,7 +221,7 @@ static PyObject *
time_sleep(PyObject *self, PyObject *obj) time_sleep(PyObject *self, PyObject *obj)
{ {
_PyTime_t secs; _PyTime_t secs;
if (_PyTime_FromObject(&secs, obj, _PyTime_ROUND_UP)) if (_PyTime_FromSecondsObject(&secs, obj, _PyTime_ROUND_UP))
return NULL; return NULL;
if (secs < 0) { if (secs < 0) {
PyErr_SetString(PyExc_ValueError, PyErr_SetString(PyExc_ValueError,
......
...@@ -424,7 +424,7 @@ _PyTime_FromTimespec(_PyTime_t *tp, struct timespec *ts) ...@@ -424,7 +424,7 @@ _PyTime_FromTimespec(_PyTime_t *tp, struct timespec *ts)
#endif #endif
int int
_PyTime_FromObject(_PyTime_t *t, PyObject *obj, _PyTime_round_t round) _PyTime_FromSecondsObject(_PyTime_t *t, PyObject *obj, _PyTime_round_t round)
{ {
if (PyFloat_Check(obj)) { if (PyFloat_Check(obj)) {
double d, err; double d, err;
...@@ -433,8 +433,7 @@ _PyTime_FromObject(_PyTime_t *t, PyObject *obj, _PyTime_round_t round) ...@@ -433,8 +433,7 @@ _PyTime_FromObject(_PyTime_t *t, PyObject *obj, _PyTime_round_t round)
d = PyFloat_AsDouble(obj); d = PyFloat_AsDouble(obj);
d *= 1e9; d *= 1e9;
/* FIXME: use sign */ if ((round == _PyTime_ROUND_UP) ^ (d < 0))
if (round == _PyTime_ROUND_UP)
d = ceil(d); d = ceil(d);
else else
d = floor(d); d = floor(d);
...@@ -471,6 +470,18 @@ _PyTime_FromObject(_PyTime_t *t, PyObject *obj, _PyTime_round_t round) ...@@ -471,6 +470,18 @@ _PyTime_FromObject(_PyTime_t *t, PyObject *obj, _PyTime_round_t round)
} }
} }
PyObject *
_PyTime_AsNanosecondsObject(_PyTime_t t)
{
#ifdef HAVE_LONG_LONG
assert(sizeof(PY_LONG_LONG) >= sizeof(_PyTime_t));
return PyLong_FromLongLong((PY_LONG_LONG)t);
#else
assert(sizeof(long) >= sizeof(_PyTime_t));
return PyLong_FromLong((long)t);
#endif
}
static _PyTime_t static _PyTime_t
_PyTime_Multiply(_PyTime_t t, unsigned int multiply, _PyTime_round_t round) _PyTime_Multiply(_PyTime_t t, unsigned int multiply, _PyTime_round_t round)
{ {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment