From 02937aab13ecfe1f67b8de48c37412b0328217ec Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 28 Mar 2015 05:02:39 +0100 Subject: [PATCH] Issue #22117: Add the new _PyTime_ROUND_FLOOR rounding method for the datetime module. time.clock_settime() now uses this rounding method instead of _PyTime_ROUND_DOWN to handle correctly dates before 1970. --- Include/pytime.h | 11 +++++-- Lib/test/test_time.py | 66 +++++++++++---------------------------- Modules/_testcapimodule.c | 3 +- Modules/timemodule.c | 4 +-- Python/pytime.c | 15 +++++++-- 5 files changed, 43 insertions(+), 56 deletions(-) diff --git a/Include/pytime.h b/Include/pytime.h index 654623e24a1..0ff009a9505 100644 --- a/Include/pytime.h +++ b/Include/pytime.h @@ -38,8 +38,12 @@ PyAPI_FUNC(void) _PyTime_gettimeofday(_PyTime_timeval *tp); typedef enum { /* Round towards zero. */ _PyTime_ROUND_DOWN=0, - /* Round away from zero. */ - _PyTime_ROUND_UP + /* Round away from zero. + For example, used for timeout to wait "at least" N seconds. */ + _PyTime_ROUND_UP, + /* Round towards minus infinity (-inf). + For example, used to read a clock. */ + _PyTime_ROUND_FLOOR } _PyTime_round_t; /* Convert a number of seconds, int or float, to time_t. */ @@ -81,6 +85,9 @@ PyAPI_FUNC(int) _PyTime_Init(void); /****************** NEW _PyTime_t API **********************/ #ifdef PY_INT64_T +/* _PyTime_t: Python timestamp with subsecond precision. It can be used to + store a duration, and so indirectly a date (related to another date, like + UNIX epoch). */ typedef PY_INT64_T _PyTime_t; #define _PyTime_MIN PY_LLONG_MIN #define _PyTime_MAX PY_LLONG_MAX diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 78314a7228e..b0c97d5919f 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -28,8 +28,13 @@ class _PyTime(enum.IntEnum): ROUND_DOWN = 0 # Round away from zero ROUND_UP = 1 + # Round towards -Infinity + ROUND_FLOOR = 2 -ALL_ROUNDING_METHODS = (_PyTime.ROUND_UP, _PyTime.ROUND_DOWN) +ALL_ROUNDING_METHODS = ( + _PyTime.ROUND_UP, + _PyTime.ROUND_DOWN, + _PyTime.ROUND_FLOOR) class TimeTestCase(unittest.TestCase): @@ -631,53 +636,6 @@ class TestPytime(unittest.TestCase): self.assertRaises(OverflowError, pytime_object_to_time_t, invalid, rnd) - @support.cpython_only - def test_timeval(self): - from _testcapi import pytime_object_to_timeval - for obj, timeval, rnd in ( - # Round towards zero - (0, (0, 0), _PyTime.ROUND_DOWN), - (-1, (-1, 0), _PyTime.ROUND_DOWN), - (-1.0, (-1, 0), _PyTime.ROUND_DOWN), - (1e-6, (0, 1), _PyTime.ROUND_DOWN), - (1e-7, (0, 0), _PyTime.ROUND_DOWN), - (-1e-6, (-1, 999999), _PyTime.ROUND_DOWN), - (-1e-7, (-1, 999999), _PyTime.ROUND_DOWN), - (-1.2, (-2, 800000), _PyTime.ROUND_DOWN), - (0.9999999, (0, 999999), _PyTime.ROUND_DOWN), - (0.0000041, (0, 4), _PyTime.ROUND_DOWN), - (1.1234560, (1, 123456), _PyTime.ROUND_DOWN), - (1.1234569, (1, 123456), _PyTime.ROUND_DOWN), - (-0.0000040, (-1, 999996), _PyTime.ROUND_DOWN), - (-0.0000041, (-1, 999995), _PyTime.ROUND_DOWN), - (-1.1234560, (-2, 876544), _PyTime.ROUND_DOWN), - (-1.1234561, (-2, 876543), _PyTime.ROUND_DOWN), - # Round away from zero - (0, (0, 0), _PyTime.ROUND_UP), - (-1, (-1, 0), _PyTime.ROUND_UP), - (-1.0, (-1, 0), _PyTime.ROUND_UP), - (1e-6, (0, 1), _PyTime.ROUND_UP), - (1e-7, (0, 1), _PyTime.ROUND_UP), - (-1e-6, (-1, 999999), _PyTime.ROUND_UP), - (-1e-7, (-1, 999999), _PyTime.ROUND_UP), - (-1.2, (-2, 800000), _PyTime.ROUND_UP), - (0.9999999, (1, 0), _PyTime.ROUND_UP), - (0.0000041, (0, 5), _PyTime.ROUND_UP), - (1.1234560, (1, 123457), _PyTime.ROUND_UP), - (1.1234569, (1, 123457), _PyTime.ROUND_UP), - (-0.0000040, (-1, 999996), _PyTime.ROUND_UP), - (-0.0000041, (-1, 999995), _PyTime.ROUND_UP), - (-1.1234560, (-2, 876544), _PyTime.ROUND_UP), - (-1.1234561, (-2, 876543), _PyTime.ROUND_UP), - ): - with self.subTest(obj=obj, round=rnd, timeval=timeval): - self.assertEqual(pytime_object_to_timeval(obj, rnd), timeval) - - rnd = _PyTime.ROUND_DOWN - for invalid in self.invalid_values: - self.assertRaises(OverflowError, - pytime_object_to_timeval, invalid, rnd) - @support.cpython_only def test_timespec(self): from _testcapi import pytime_object_to_timespec @@ -835,24 +793,31 @@ class TestPyTime_t(unittest.TestCase): # Conversion giving different results depending on the rounding method UP = _PyTime.ROUND_UP DOWN = _PyTime.ROUND_DOWN + FLOOR = _PyTime.ROUND_FLOOR for obj, ts, rnd in ( # close to zero ( 1e-10, 1, UP), ( 1e-10, 0, DOWN), + ( 1e-10, 0, FLOOR), (-1e-10, 0, DOWN), (-1e-10, -1, UP), + (-1e-10, -1, FLOOR), # test rounding of the last nanosecond ( 1.1234567899, 1123456790, UP), ( 1.1234567899, 1123456789, DOWN), + ( 1.1234567899, 1123456789, FLOOR), (-1.1234567899, -1123456789, DOWN), (-1.1234567899, -1123456790, UP), + (-1.1234567899, -1123456790, FLOOR), # close to 1 second ( 0.9999999999, 1000000000, UP), ( 0.9999999999, 999999999, DOWN), + ( 0.9999999999, 999999999, FLOOR), (-0.9999999999, -999999999, DOWN), (-0.9999999999, -1000000000, UP), + (-0.9999999999, -1000000000, FLOOR), ): with self.subTest(obj=obj, round=rnd, timestamp=ts): self.assertEqual(PyTime_FromSecondsObject(obj, rnd), ts) @@ -924,18 +889,23 @@ class TestPyTime_t(unittest.TestCase): UP = _PyTime.ROUND_UP DOWN = _PyTime.ROUND_DOWN + FLOOR = _PyTime.ROUND_FLOOR for ns, tv, rnd in ( # nanoseconds (1, (0, 1), UP), (1, (0, 0), DOWN), + (1, (0, 0), FLOOR), (-1, (0, 0), DOWN), (-1, (-1, 999999), UP), + (-1, (-1, 999999), FLOOR), # seconds + nanoseconds (1234567001, (1, 234568), UP), (1234567001, (1, 234567), DOWN), + (1234567001, (1, 234567), FLOOR), (-1234567001, (-2, 765433), DOWN), (-1234567001, (-2, 765432), UP), + (-1234567001, (-2, 765432), FLOOR), ): with self.subTest(nanoseconds=ns, timeval=tv, round=rnd): self.assertEqual(PyTime_AsTimeval(ns, rnd), tv) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 4503dc384f4..5c54ad6785e 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2634,7 +2634,8 @@ run_in_subinterp(PyObject *self, PyObject *args) static int check_time_rounding(int round) { - if (round != _PyTime_ROUND_DOWN && round != _PyTime_ROUND_UP) { + if (round != _PyTime_ROUND_DOWN && round != _PyTime_ROUND_UP + && round != _PyTime_ROUND_FLOOR) { PyErr_SetString(PyExc_ValueError, "invalid rounding"); return -1; } diff --git a/Modules/timemodule.c b/Modules/timemodule.c index 3178fcbf037..99e83cc6dc0 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -173,7 +173,7 @@ time_clock_settime(PyObject *self, PyObject *args) if (!PyArg_ParseTuple(args, "iO:clock_settime", &clk_id, &obj)) return NULL; - if (_PyTime_FromSecondsObject(&t, obj, _PyTime_ROUND_DOWN) < 0) + if (_PyTime_FromSecondsObject(&t, obj, _PyTime_ROUND_FLOOR) < 0) return NULL; if (_PyTime_AsTimespec(t, &tp) == -1) @@ -322,7 +322,7 @@ parse_time_t_args(PyObject *args, char *format, time_t *pwhen) whent = time(NULL); } else { - if (_PyTime_ObjectToTime_t(ot, &whent, _PyTime_ROUND_DOWN) == -1) + if (_PyTime_ObjectToTime_t(ot, &whent, _PyTime_ROUND_FLOOR) == -1) return 0; } *pwhen = whent; diff --git a/Python/pytime.c b/Python/pytime.c index 0f05db424dd..8c6771baf38 100644 --- a/Python/pytime.c +++ b/Python/pytime.c @@ -260,6 +260,14 @@ _PyTime_overflow(void) "timestamp too large to convert to C _PyTime_t"); } +int +_PyTime_RoundTowardsInfinity(int is_neg, _PyTime_round_t round) +{ + if (round == _PyTime_ROUND_FLOOR) + return 0; + return ((round == _PyTime_ROUND_UP) ^ is_neg); +} + _PyTime_t _PyTime_FromNanoseconds(PY_LONG_LONG ns) { @@ -314,7 +322,7 @@ _PyTime_FromSecondsObject(_PyTime_t *t, PyObject *obj, _PyTime_round_t round) d = PyFloat_AsDouble(obj); d *= 1e9; - if ((round == _PyTime_ROUND_UP) ^ (d < 0)) + if (_PyTime_RoundTowardsInfinity(d < 0, round)) d = ceil(d); else d = floor(d); @@ -380,7 +388,7 @@ _PyTime_Multiply(_PyTime_t t, unsigned int multiply, _PyTime_round_t round) _PyTime_t k; if (multiply < SEC_TO_NS) { k = SEC_TO_NS / multiply; - if (round == _PyTime_ROUND_UP) + if (_PyTime_RoundTowardsInfinity(t < 0, round)) return (t + k - 1) / k; else return t / k; @@ -397,6 +405,7 @@ _PyTime_AsMilliseconds(_PyTime_t t, _PyTime_round_t round) return _PyTime_Multiply(t, 1000, round); } +/* FIXME: write unit tests */ _PyTime_t _PyTime_AsMicroseconds(_PyTime_t t, _PyTime_round_t round) { @@ -439,7 +448,7 @@ _PyTime_AsTimeval(_PyTime_t t, struct timeval *tv, _PyTime_round_t round) res = -1; #endif - if ((round == _PyTime_ROUND_UP) ^ (tv->tv_sec < 0)) + if (_PyTime_RoundTowardsInfinity(tv->tv_sec < 0, round)) tv->tv_usec = (int)((ns + US_TO_NS - 1) / US_TO_NS); else tv->tv_usec = (int)(ns / US_TO_NS);