]> granicus.if.org Git - python/commitdiff
Issue #23517: Add "half up" rounding mode to the _PyTime API
authorVictor Stinner <victor.stinner@gmail.com>
Tue, 1 Sep 2015 23:43:56 +0000 (01:43 +0200)
committerVictor Stinner <victor.stinner@gmail.com>
Tue, 1 Sep 2015 23:43:56 +0000 (01:43 +0200)
Include/pytime.h
Lib/test/test_time.py
Modules/_testcapimodule.c
Python/pytime.c

index 027c3d803a4122284b5f120aa46fa997ac12cfb6..98ae12bae52335a87fa1d81859f58ffbe8208ba2 100644 (file)
@@ -30,7 +30,10 @@ typedef enum {
     _PyTime_ROUND_FLOOR=0,
     /* Round towards infinity (+inf).
        For example, used for timeout to wait "at least" N seconds. */
-    _PyTime_ROUND_CEILING
+    _PyTime_ROUND_CEILING=1,
+    /* Round to nearest with ties going away from zero.
+       For example, used to round from a Python float. */
+    _PyTime_ROUND_HALF_UP
 } _PyTime_round_t;
 
 /* Convert a time_t to a PyLong. */
index 6334e022e007c90d22689cee49672db34273c6a2..ed20470912322df7ec17a7d0a04e3ef391d6e136 100644 (file)
@@ -30,8 +30,11 @@ class _PyTime(enum.IntEnum):
     ROUND_FLOOR = 0
     # Round towards infinity (+inf)
     ROUND_CEILING = 1
+    # Round to nearest with ties going away from zero
+    ROUND_HALF_UP = 2
 
-ALL_ROUNDING_METHODS = (_PyTime.ROUND_FLOOR, _PyTime.ROUND_CEILING)
+ALL_ROUNDING_METHODS = (_PyTime.ROUND_FLOOR, _PyTime.ROUND_CEILING,
+                        _PyTime.ROUND_HALF_UP)
 
 
 class TimeTestCase(unittest.TestCase):
@@ -753,11 +756,11 @@ class TestPyTime_t(unittest.TestCase):
                 (123.0, 123 * SEC_TO_NS),
                 (-7.0, -7 * SEC_TO_NS),
 
-                # nanosecond are kept for value <= 2^23 seconds
+                # nanosecond are kept for value <= 2^23 seconds,
+                # except 2**23-1e-9 with HALF_UP
                 (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
@@ -790,24 +793,36 @@ class TestPyTime_t(unittest.TestCase):
         # Conversion giving different results depending on the rounding method
         FLOOR = _PyTime.ROUND_FLOOR
         CEILING = _PyTime.ROUND_CEILING
+        HALF_UP =  _PyTime.ROUND_HALF_UP
         for obj, ts, rnd in (
             # close to zero
             ( 1e-10,  0, FLOOR),
             ( 1e-10,  1, CEILING),
+            ( 1e-10,  0, HALF_UP),
             (-1e-10, -1, FLOOR),
             (-1e-10,  0, CEILING),
+            (-1e-10,  0, HALF_UP),
 
             # test rounding of the last nanosecond
             ( 1.1234567899,  1123456789, FLOOR),
             ( 1.1234567899,  1123456790, CEILING),
+            ( 1.1234567899,  1123456790, HALF_UP),
             (-1.1234567899, -1123456790, FLOOR),
             (-1.1234567899, -1123456789, CEILING),
+            (-1.1234567899, -1123456790, HALF_UP),
 
             # close to 1 second
             ( 0.9999999999,   999999999, FLOOR),
             ( 0.9999999999,  1000000000, CEILING),
+            ( 0.9999999999,  1000000000, HALF_UP),
             (-0.9999999999, -1000000000, FLOOR),
             (-0.9999999999,  -999999999, CEILING),
+            (-0.9999999999, -1000000000, HALF_UP),
+
+            # close to 2^23 seconds
+            (2**23 - 1e-9,  8388607999999999, FLOOR),
+            (2**23 - 1e-9,  8388607999999999, CEILING),
+            (2**23 - 1e-9,  8388608000000000, HALF_UP),
         ):
             with self.subTest(obj=obj, round=rnd, timestamp=ts):
                 self.assertEqual(PyTime_FromSecondsObject(obj, rnd), ts)
@@ -875,18 +890,33 @@ class TestPyTime_t(unittest.TestCase):
 
         FLOOR = _PyTime.ROUND_FLOOR
         CEILING = _PyTime.ROUND_CEILING
+        HALF_UP = _PyTime.ROUND_HALF_UP
         for ns, tv, rnd in (
             # nanoseconds
             (1, (0, 0), FLOOR),
             (1, (0, 1), CEILING),
+            (1, (0, 0), HALF_UP),
             (-1, (-1, 999999), FLOOR),
             (-1, (0, 0), CEILING),
+            (-1, (0, 0), HALF_UP),
 
             # seconds + nanoseconds
             (1234567001, (1, 234567), FLOOR),
             (1234567001, (1, 234568), CEILING),
+            (1234567001, (1, 234567), HALF_UP),
             (-1234567001, (-2, 765432), FLOOR),
             (-1234567001, (-2, 765433), CEILING),
+            (-1234567001, (-2, 765433), HALF_UP),
+
+            # half up
+            (499, (0, 0), HALF_UP),
+            (500, (0, 1), HALF_UP),
+            (501, (0, 1), HALF_UP),
+            (999, (0, 1), HALF_UP),
+            (-499, (0, 0), HALF_UP),
+            (-500, (0, 0), HALF_UP),
+            (-501, (-1, 999999), HALF_UP),
+            (-999, (-1, 999999), HALF_UP),
         ):
             with self.subTest(nanoseconds=ns, timeval=tv, round=rnd):
                 self.assertEqual(PyTime_AsTimeval(ns, rnd), tv)
@@ -929,18 +959,33 @@ class TestPyTime_t(unittest.TestCase):
 
         FLOOR = _PyTime.ROUND_FLOOR
         CEILING = _PyTime.ROUND_CEILING
+        HALF_UP = _PyTime.ROUND_HALF_UP
         for ns, ms, rnd in (
             # nanoseconds
             (1, 0, FLOOR),
             (1, 1, CEILING),
+            (1, 0, HALF_UP),
             (-1, 0, FLOOR),
             (-1, -1, CEILING),
+            (-1, 0, HALF_UP),
 
             # seconds + nanoseconds
             (1234 * MS_TO_NS + 1, 1234, FLOOR),
             (1234 * MS_TO_NS + 1, 1235, CEILING),
+            (1234 * MS_TO_NS + 1, 1234, HALF_UP),
             (-1234 * MS_TO_NS - 1, -1234, FLOOR),
             (-1234 * MS_TO_NS - 1, -1235, CEILING),
+            (-1234 * MS_TO_NS - 1, -1234, HALF_UP),
+
+            # half up
+            (499999, 0, HALF_UP),
+            (499999, 0, HALF_UP),
+            (500000, 1, HALF_UP),
+            (999999, 1, HALF_UP),
+            (-499999, 0, HALF_UP),
+            (-500000, -1, HALF_UP),
+            (-500001, -1, HALF_UP),
+            (-999999, -1, HALF_UP),
         ):
             with self.subTest(nanoseconds=ns, milliseconds=ms, round=rnd):
                 self.assertEqual(PyTime_AsMilliseconds(ns, rnd), ms)
@@ -966,18 +1011,31 @@ class TestPyTime_t(unittest.TestCase):
 
         FLOOR = _PyTime.ROUND_FLOOR
         CEILING = _PyTime.ROUND_CEILING
+        HALF_UP = _PyTime.ROUND_HALF_UP
         for ns, ms, rnd in (
             # nanoseconds
             (1, 0, FLOOR),
             (1, 1, CEILING),
+            (1, 0, HALF_UP),
             (-1, 0, FLOOR),
             (-1, -1, CEILING),
+            (-1, 0, HALF_UP),
 
             # seconds + nanoseconds
             (1234 * US_TO_NS + 1, 1234, FLOOR),
             (1234 * US_TO_NS + 1, 1235, CEILING),
+            (1234 * US_TO_NS + 1, 1234, HALF_UP),
             (-1234 * US_TO_NS - 1, -1234, FLOOR),
             (-1234 * US_TO_NS - 1, -1235, CEILING),
+            (-1234 * US_TO_NS - 1, -1234, HALF_UP),
+
+            # half up
+            (1499, 1, HALF_UP),
+            (1500, 2, HALF_UP),
+            (1501, 2, HALF_UP),
+            (-1499, -1, HALF_UP),
+            (-1500, -2, HALF_UP),
+            (-1501, -2, HALF_UP),
         ):
             with self.subTest(nanoseconds=ns, milliseconds=ms, round=rnd):
                 self.assertEqual(PyTime_AsMicroseconds(ns, rnd), ms)
index ba0a24bd1d2cbe68a11905d56e7fe15666ba6604..c38d9ae6346078a90a9f2d1ad0efc74c5b4af1de 100644 (file)
@@ -2646,7 +2646,9 @@ run_in_subinterp(PyObject *self, PyObject *args)
 static int
 check_time_rounding(int round)
 {
-    if (round != _PyTime_ROUND_FLOOR && round != _PyTime_ROUND_CEILING) {
+    if (round != _PyTime_ROUND_FLOOR
+        && round != _PyTime_ROUND_CEILING
+        && round != _PyTime_ROUND_HALF_UP) {
         PyErr_SetString(PyExc_ValueError, "invalid rounding");
         return -1;
     }
index fcac68a389c3e4a523a511f00d69cca5736c3502..3294e0f49b9643369c16fee8728e7bb9cdad9208 100644 (file)
@@ -60,6 +60,17 @@ _PyLong_FromTime_t(time_t t)
 #endif
 }
 
+static double
+_PyTime_RoundHalfUp(double x)
+{
+    if (x >= 0.0)
+        x = floor(x + 0.5);
+    else
+        x = ceil(x - 0.5);
+    return x;
+}
+
+
 static int
 _PyTime_DoubleToDenominator(double d, time_t *sec, long *numerator,
                             double denominator, _PyTime_round_t round)
@@ -75,7 +86,9 @@ _PyTime_DoubleToDenominator(double d, time_t *sec, long *numerator,
     }
 
     floatpart *= denominator;
-    if (round == _PyTime_ROUND_CEILING) {
+    if (round == _PyTime_ROUND_HALF_UP)
+        floatpart = _PyTime_RoundHalfUp(floatpart);
+    else if (round == _PyTime_ROUND_CEILING) {
         floatpart = ceil(floatpart);
         if (floatpart >= denominator) {
             floatpart = 0.0;
@@ -124,7 +137,9 @@ _PyTime_ObjectToTime_t(PyObject *obj, time_t *sec, _PyTime_round_t round)
         double d, intpart, err;
 
         d = PyFloat_AsDouble(obj);
-        if (round == _PyTime_ROUND_CEILING)
+        if (round == _PyTime_ROUND_HALF_UP)
+            d = _PyTime_RoundHalfUp(d);
+        else if (round == _PyTime_ROUND_CEILING)
             d = ceil(d);
         else
             d = floor(d);
@@ -247,7 +262,9 @@ _PyTime_FromFloatObject(_PyTime_t *t, double value, _PyTime_round_t round,
     d = value;
     d *= to_nanoseconds;
 
-    if (round == _PyTime_ROUND_CEILING)
+    if (round == _PyTime_ROUND_HALF_UP)
+        d = _PyTime_RoundHalfUp(d);
+    else if (round == _PyTime_ROUND_CEILING)
         d = ceil(d);
     else
         d = floor(d);
@@ -333,7 +350,19 @@ static _PyTime_t
 _PyTime_Divide(_PyTime_t t, _PyTime_t k, _PyTime_round_t round)
 {
     assert(k > 1);
-    if (round == _PyTime_ROUND_CEILING) {
+    if (round == _PyTime_ROUND_HALF_UP) {
+        _PyTime_t x, r;
+        x = t / k;
+        r = t % k;
+        if (Py_ABS(r) >= k / 2) {
+            if (t >= 0)
+                x++;
+            else
+                x--;
+        }
+        return x;
+    }
+    else if (round == _PyTime_ROUND_CEILING) {
         if (t >= 0)
             return (t + k - 1) / k;
         else
@@ -359,8 +388,10 @@ static int
 _PyTime_AsTimeval_impl(_PyTime_t t, struct timeval *tv, _PyTime_round_t round,
                        int raise)
 {
+    const long k = US_TO_NS;
     _PyTime_t secs, ns;
     int res = 0;
+    int usec;
 
     secs = t / SEC_TO_NS;
     ns = t % SEC_TO_NS;
@@ -392,20 +423,33 @@ _PyTime_AsTimeval_impl(_PyTime_t t, struct timeval *tv, _PyTime_round_t round,
         res = -1;
 #endif
 
-    if (round == _PyTime_ROUND_CEILING)
-        tv->tv_usec = (int)((ns + US_TO_NS - 1) / US_TO_NS);
+    if (round == _PyTime_ROUND_HALF_UP) {
+        _PyTime_t r;
+        usec = (int)(ns / k);
+        r = ns % k;
+        if (Py_ABS(r) >= k / 2) {
+            if (ns >= 0)
+                usec++;
+            else
+                usec--;
+        }
+    }
+    else if (round == _PyTime_ROUND_CEILING)
+        usec = (int)((ns + k - 1) / k);
     else
-        tv->tv_usec = (int)(ns / US_TO_NS);
+        usec = (int)(ns / k);
 
-    if (tv->tv_usec >= SEC_TO_US) {
-        tv->tv_usec -= SEC_TO_US;
+    if (usec >= SEC_TO_US) {
+        usec -= SEC_TO_US;
         tv->tv_sec += 1;
     }
 
     if (res && raise)
         _PyTime_overflow();
 
-    assert(0 <= tv->tv_usec && tv->tv_usec <= 999999);
+    assert(0 <= usec && usec <= 999999);
+
+    tv->tv_usec = usec;
     return res;
 }