diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 3baae534041446..2d7f3070e67c07 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -568,7 +568,8 @@ Other language changes * Functions that take timestamp or timeout arguments now accept any real numbers (such as :class:`~decimal.Decimal` and :class:`~fractions.Fraction`), - not only integers or floats, although this does not improve precision. + not only integers or floats. + This allows to avoid the precision loss caused by the :class:`float` type. (Contributed by Serhiy Storchaka in :gh:`67795`.) .. _whatsnew315-bytearray-take-bytes: diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index f7d3dcd440aaf1..f6147b7f175ed8 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1701,6 +1701,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(default)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(defaultaction)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(delete)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(denominator)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(depth)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(desired_access)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(detect_types)); @@ -1951,6 +1952,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(nt)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(null)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(number)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(numerator)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(obj)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(object)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(offset)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 22494b1798cc53..a42495d72003ed 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -424,6 +424,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(default) STRUCT_FOR_ID(defaultaction) STRUCT_FOR_ID(delete) + STRUCT_FOR_ID(denominator) STRUCT_FOR_ID(depth) STRUCT_FOR_ID(desired_access) STRUCT_FOR_ID(detect_types) @@ -674,6 +675,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(nt) STRUCT_FOR_ID(null) STRUCT_FOR_ID(number) + STRUCT_FOR_ID(numerator) STRUCT_FOR_ID(obj) STRUCT_FOR_ID(object) STRUCT_FOR_ID(offset) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 892c3cdd9623a2..9fb2faa36e289e 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1699,6 +1699,7 @@ extern "C" { INIT_ID(default), \ INIT_ID(defaultaction), \ INIT_ID(delete), \ + INIT_ID(denominator), \ INIT_ID(depth), \ INIT_ID(desired_access), \ INIT_ID(detect_types), \ @@ -1949,6 +1950,7 @@ extern "C" { INIT_ID(nt), \ INIT_ID(null), \ INIT_ID(number), \ + INIT_ID(numerator), \ INIT_ID(obj), \ INIT_ID(object), \ INIT_ID(offset), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index f0fc3c4f5b0900..36968e643c6c47 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1476,6 +1476,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(denominator); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(depth); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); @@ -2476,6 +2480,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(numerator); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(obj); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b6d68f2372850a..51ab83948c2a3e 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -1857,14 +1857,28 @@ def _fromtimestamp(cls, t, utc, tz): A timezone info object may be passed in as well. """ - frac, t = _math.modf(t) - us = round(frac * 1e6) - if us >= 1000000: + if isinstance(t, float): + frac, t = _math.modf(t) + us = round(frac * 1e6) + else: + try: + try: + n, d = t.as_integer_ratio() + except AttributeError: + n = t.numerator + d = t.denumerator + except AttributeError: + frac, t = _math.modf(t) + us = round(frac * 1e6) + else: + t, n = divmod(n, d) + us = _divide_and_round(n * 1_000_000, d) + if us >= 1_000_000: t += 1 - us -= 1000000 + us -= 1_000_000 elif us < 0: t -= 1 - us += 1000000 + us += 1_000_000 converter = _time.gmtime if utc else _time.localtime y, m, d, hh, mm, ss, weekday, jday, dst = converter(t) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 5d5b8e415f3cd2..be8b6416683e25 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -2802,11 +2802,11 @@ def utcfromtimestamp(*args, **kwargs): self.assertEqual(t, zero) t = fts(D('0.000_000_5')) self.assertEqual(t, zero) - t = fts(D('0.000_000_500_000_000_000_000_1')) + t = fts(D('0.000_000_500_000_000_000_000_000_000_000_000_000_001')) self.assertEqual(t, one) t = fts(D('0.000_000_9')) self.assertEqual(t, one) - t = fts(D('0.999_999_499_999_999_9')) + t = fts(D('0.999_999_499_999_999_999_999_999_999_999_999_999_999')) self.assertEqual(t.second, 0) self.assertEqual(t.microsecond, 999_999) t = fts(D('0.999_999_5')) @@ -2819,6 +2819,21 @@ def utcfromtimestamp(*args, **kwargs): self.assertEqual(t.second, 0) self.assertEqual(t.microsecond, 7812) + t = fts(D('2_147_475_000.000_000_5')) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 0) + t = fts(D('2_147_475_000' + '.000_000_500_000_000_000_000_000_000_000_000_000_001')) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 1) + t = fts(D('2_147_475_000' + '.999_999_499_999_999_999_999_999_999_999_999_999_999')) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 999_999) + t = fts(D('2_147_475_000.999_999_5')) + self.assertEqual(t.second, 1) + self.assertEqual(t.microsecond, 0) + @support.run_with_tz('MSK-03') # Something east of Greenwich def test_microsecond_rounding_fraction(self): F = fractions.Fraction @@ -2849,11 +2864,13 @@ def utcfromtimestamp(*args, **kwargs): self.assertEqual(t, zero) t = fts(F(5, 10_000_000)) self.assertEqual(t, zero) - t = fts(F(5_000_000_000, 9_999_999_999_999_999)) + t = fts(F( 5_000_000_000_000_000_000_000_000_000_000_000, + 9_999_999_999_999_999_999_999_999_999_999_999_999_999)) self.assertEqual(t, one) t = fts(F(9, 10_000_000)) self.assertEqual(t, one) - t = fts(F(9_999_995_000_000_000, 10_000_000_000_000_001)) + t = fts(F( 9_999_995_000_000_000_000_000_000_000_000_000_000_000, + 10_000_000_000_000_000_000_000_000_000_000_000_000_001)) self.assertEqual(t.second, 0) self.assertEqual(t.microsecond, 999_999) t = fts(F(9_999_995, 10_000_000)) @@ -2866,6 +2883,23 @@ def utcfromtimestamp(*args, **kwargs): self.assertEqual(t.second, 0) self.assertEqual(t.microsecond, 7812) + t = fts(2_147_475_000 + F(5, 10_000_000)) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 0) + t = fts(2_147_475_000 + + F( 5_000_000_000_000_000_000_000_000_000_000_000, + 9_999_999_999_999_999_999_999_999_999_999_999_999_999)) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 1) + t = fts(2_147_475_000 + + F( 9_999_995_000_000_000_000_000_000_000_000_000_000_000, + 10_000_000_000_000_000_000_000_000_000_000_000_000_001)) + self.assertEqual(t.second, 0) + self.assertEqual(t.microsecond, 999_999) + t = fts(2_147_475_000 + F(9_999_995, 10_000_000)) + self.assertEqual(t.second, 1) + self.assertEqual(t.microsecond, 0) + def test_timestamp_limits(self): with self.subTest("minimum UTC"): min_dt = self.theclass.min.replace(tzinfo=timezone.utc) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 7e670e5a139d99..f6541d9eb3c60d 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -1110,16 +1110,12 @@ def ns_to_sec(ns): @staticmethod def ns_to_sec_decimal(ns): # Convert a number of nanosecond (int) to a number of seconds (Decimal). - # Round towards infinity by adding 0.5 nanosecond to avoid rounding - # issue, os.utime() rounds towards minus infinity. - return decimal.Decimal('1e-9') * ns + decimal.Decimal('0.5e-9') + return decimal.Decimal('1e-9') * ns @staticmethod def ns_to_sec_fraction(ns): # Convert a number of nanosecond (int) to a number of seconds (Fraction). - # Round towards infinity by adding 0.5 nanosecond to avoid rounding - # issue, os.utime() rounds towards minus infinity. - return fractions.Fraction(ns, 10**9) + fractions.Fraction(1, 2*10**9) + return fractions.Fraction(ns, 10**9) def test_utime_by_indexed(self): # pass times as floating-point seconds as the second indexed parameter diff --git a/Misc/NEWS.d/next/Library/2026-05-04-20-10-04.gh-issue-67795.v9wb5q.rst b/Misc/NEWS.d/next/Library/2026-05-04-20-10-04.gh-issue-67795.v9wb5q.rst new file mode 100644 index 00000000000000..dcc0c1d65b161a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-04-20-10-04.gh-issue-67795.v9wb5q.rst @@ -0,0 +1,3 @@ +Avoid the precision loss caused by the :class:`float` type in functions that +take more precise numbers (such as :class:`~decimal.Decimal` and +:class:`~fractions.Fraction`) as timestamp or timeout arguments. diff --git a/Python/pytime.c b/Python/pytime.c index 399ff59ad01ab6..b350bd5f5cb718 100644 --- a/Python/pytime.c +++ b/Python/pytime.c @@ -1,5 +1,6 @@ #include "Python.h" #include "pycore_initconfig.h" // _PyStatus_ERR +#include "pycore_long.h" // _PyLong_DivmodNear() #include "pycore_pystate.h" // _Py_AssertHoldsTstate() #include "pycore_runtime.h" // _PyRuntime #include "pycore_time.h" // export _PyLong_FromTime_t() @@ -449,34 +450,242 @@ pytime_double_to_denominator(double d, time_t *sec, long *numerator, } +/* Convert a number to a fraction representation. + * + * Set *ratio to a 2-tuple (numerator, denominator) and return 1 on success. + * Return 0 if the number has neither the as_integer_ratio() method nor + * the numerator and denominator attributes. + * Return -1 on error. + */ +static int +maybe_as_integer_ratio(PyObject *number, PyObject **ratio) +{ + *ratio = NULL; + if (PyType_Check(number)) { + PyErr_SetString(PyExc_TypeError, + "required a number, not type"); + return -1; + } + PyObject *meth; + if (PyObject_GetOptionalAttr(number, &_Py_ID(as_integer_ratio), &meth) < 0) { + return -1; + } + if (meth) { + *ratio = PyObject_CallNoArgs(meth); + Py_DECREF(meth); + if (*ratio == NULL) { + return -1; + } + if (!PyTuple_Check(*ratio)) { + PyErr_Format(PyExc_TypeError, + "unexpected return type from %T.as_integer_ratio(): " + "expected tuple, not '%T'", + number, *ratio); + Py_CLEAR(*ratio); + return -1; + } + if (PyTuple_GET_SIZE(*ratio) != 2) { + PyErr_Format(PyExc_ValueError, + "%T.as_integer_ratio() must return a 2-tuple", + number); + Py_CLEAR(*ratio); + return -1; + } + return 1; + } + + PyObject *numerator, *denominator; + int rc = PyObject_GetOptionalAttr(number, &_Py_ID(numerator), &numerator); + if (rc <= 0) { + return rc; + } + rc = PyObject_GetOptionalAttr(number, &_Py_ID(denominator), &denominator); + if (rc <= 0) { + Py_DECREF(numerator); + return rc; + } + *ratio = PyTuple_Pack(2, numerator, denominator); + Py_DECREF(numerator); + Py_DECREF(denominator); + return *ratio ? 1 : -1; +} + +/* PyNumber_Divmod() that always returns a 2-tuple. */ +static PyObject * +checked_divmod(PyObject *a, PyObject *b) +{ + PyObject *result = PyNumber_Divmod(a, b); + if (result != NULL) { + if (!PyTuple_Check(result)) { + PyErr_Format(PyExc_TypeError, + "divmod() returned non-tuple (type %T)", + result); + Py_DECREF(result); + return NULL; + } + if (PyTuple_GET_SIZE(result) != 2) { + PyErr_Format(PyExc_TypeError, + "divmod() returned a tuple of size %zd", + PyTuple_GET_SIZE(result)); + Py_DECREF(result); + return NULL; + } + } + return result; +} + +/* Calculate numerator / denominator rounded to integer using + * the specified rounding mode. */ +static PyObject * +divide_and_round(PyObject *numerator, PyObject *denominator, _PyTime_round_t round) +{ + if (round == _PyTime_ROUND_UP) { + int isneg = PyObject_RichCompareBool(numerator, _PyLong_GetZero(), Py_LT); + if (isneg < 0) { + return NULL; + } + round = isneg ? _PyTime_ROUND_FLOOR : _PyTime_ROUND_CEILING; + } + + if (round == _PyTime_ROUND_FLOOR) { + return PyNumber_FloorDivide(numerator, denominator); + } + + PyObject *divmod, *result; + if (round == _PyTime_ROUND_CEILING) { + divmod = checked_divmod(numerator, denominator); + if (divmod == NULL) { + return NULL; + } + int nonzero = PyObject_IsTrue(PyTuple_GET_ITEM(divmod, 1)); + if (nonzero < 0) { + result = NULL; + } + else if (nonzero) { + result = PyNumber_Add(PyTuple_GET_ITEM(divmod, 0), _PyLong_GetOne()); + } + else { + result = Py_NewRef(PyTuple_GET_ITEM(divmod, 0)); + } + } + else { + assert(round == _PyTime_ROUND_HALF_EVEN); + divmod = _PyLong_DivmodNear(numerator, denominator); + if (divmod == NULL) { + return NULL; + } + result = Py_NewRef(PyTuple_GET_ITEM(divmod, 0)); + } + Py_DECREF(divmod); + return result; +} + +/* Calculate scale * numerator / denominator rounded to integer using + * the specified rounding mode. */ +static PyObject * +multiply_divide_and_round(long scale, PyObject *numerator, PyObject *denominator, + _PyTime_round_t round) +{ + PyObject *scaleobj = PyLong_FromLong(scale); + if (scaleobj == NULL) { + return NULL; + } + numerator = PyNumber_Multiply(numerator, scaleobj); + Py_DECREF(scaleobj); + if (numerator == NULL) { + return NULL; + } + PyObject *result = divide_and_round(numerator, denominator, round); + Py_DECREF(numerator); + return result; +} + +static int +pytime_ratio_to_denominator(PyObject *numerator, PyObject *denominator, + time_t *sec, long *subsec, + long scale, _PyTime_round_t round) +{ + PyObject *divmod = checked_divmod(numerator, denominator); + if (divmod == NULL) { + return -1; + } + *sec = _PyLong_AsTime_t(PyTuple_GET_ITEM(divmod, 0)); + if (*sec == (time_t)-1 && PyErr_Occurred()) { + Py_DECREF(divmod); + return -1; + } + PyObject *tmp = multiply_divide_and_round(scale, + PyTuple_GET_ITEM(divmod, 1), + denominator, + _PyTime_ROUND_HALF_EVEN); + Py_DECREF(divmod); + *subsec = PyLong_AsLong(tmp); + Py_DECREF(tmp); + if (*subsec == -1 && PyErr_Occurred()) { + return -1; + } + if (*subsec < 0) { + *subsec += scale; + if (*sec <= PY_TIME_T_MIN) { + pytime_time_t_overflow(); + return -1; + } + *sec -= 1; + } + else if (*subsec >= scale) { + *subsec -= scale; + if (*sec >= PY_TIME_T_MAX) { + pytime_time_t_overflow(); + return -1; + } + *sec += 1; + } + return 0; +} + + static int pytime_object_to_denominator(PyObject *obj, time_t *sec, long *numerator, long denominator, _PyTime_round_t round) { assert(denominator >= 1); + *numerator = 0; if (PyIndex_Check(obj)) { *sec = _PyLong_AsTime_t(obj); - *numerator = 0; if (*sec == (time_t)-1 && PyErr_Occurred()) { return -1; } return 0; } - else { + else if (PyFloat_Check(obj)) { +fromfloat:; double d = PyFloat_AsDouble(obj); if (d == -1 && PyErr_Occurred()) { - *numerator = 0; return -1; } if (isnan(d)) { - *numerator = 0; PyErr_SetString(PyExc_ValueError, "Invalid value NaN (not a number)"); return -1; } return pytime_double_to_denominator(d, sec, numerator, denominator, round); } + else { + PyObject *ratio; + if (maybe_as_integer_ratio(obj, &ratio) < 0) { + return -1; + } + if (ratio == NULL) { + goto fromfloat; + } + int rc = pytime_ratio_to_denominator(PyTuple_GET_ITEM(ratio, 0), + PyTuple_GET_ITEM(ratio, 1), + sec, numerator, + denominator, round); + Py_DECREF(ratio); + return rc; + } } @@ -490,7 +699,8 @@ _PyTime_ObjectToTime_t(PyObject *obj, time_t *sec, _PyTime_round_t round) } return 0; } - else { + else if (PyFloat_Check(obj)) { +fromfloat:; double intpart; /* volatile avoids optimization changing how numbers are rounded */ volatile double d; @@ -515,6 +725,28 @@ _PyTime_ObjectToTime_t(PyObject *obj, time_t *sec, _PyTime_round_t round) *sec = (time_t)intpart; return 0; } + else { + PyObject *ratio; + if (maybe_as_integer_ratio(obj, &ratio) < 0) { + return -1; + } + if (ratio == NULL) { + goto fromfloat; + } + PyObject *secobj = divide_and_round(PyTuple_GET_ITEM(ratio, 0), + PyTuple_GET_ITEM(ratio, 1), + round); + Py_DECREF(ratio); + if (secobj == NULL) { + return -1; + } + *sec = _PyLong_AsTime_t(secobj); + Py_DECREF(secobj); + if (*sec == (time_t)-1 && PyErr_Occurred()) { + return -1; + } + return 0; + } } @@ -671,7 +903,8 @@ pytime_from_object(PyTime_t *tp, PyObject *obj, _PyTime_round_t round, *tp = ns; return 0; } - else { + else if (PyFloat_Check(obj)) { +fromfloat:; double d; d = PyFloat_AsDouble(obj); if (d == -1 && PyErr_Occurred()) { @@ -683,6 +916,26 @@ pytime_from_object(PyTime_t *tp, PyObject *obj, _PyTime_round_t round, } return pytime_from_double(tp, d, round, unit_to_ns); } + else { + PyObject *ratio; + if (maybe_as_integer_ratio(obj, &ratio) < 0) { + return -1; + } + if (ratio == NULL) { + goto fromfloat; + } + PyObject *nsobj = multiply_divide_and_round(unit_to_ns, + PyTuple_GET_ITEM(ratio, 0), + PyTuple_GET_ITEM(ratio, 1), + round); + Py_DECREF(ratio); + if (nsobj == NULL) { + return -1; + } + int rc = PyLong_AsInt64(nsobj, tp); + Py_DECREF(nsobj); + return rc; + } }