Skip to content

Commit 90f8bf2

Browse files
gh-67795: Improve precision of non-float timestamp and timeout arguments
Use the as_integer_ratio() or the numerator and denominator attributes to represent the number as an integer ratio and perform scaling and rounding exactly. This allows to avoid the precision loss and double rounding error due to conversion to float.
1 parent 1a2e00c commit 90f8bf2

File tree

10 files changed

+322
-23
lines changed

10 files changed

+322
-23
lines changed

Doc/whatsnew/3.15.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,8 @@ Other language changes
281281

282282
* Functions that take timestamp or timeout arguments now accept any real
283283
numbers (such as :class:`~decimal.Decimal` and :class:`~fractions.Fraction`),
284-
not only integers or floats, although this does not improve precision.
284+
not only integers or floats.
285+
This allows to avoid the precision loss caused by the :class:`float` type.
285286
(Contributed by Serhiy Storchaka in :gh:`67795`.)
286287

287288

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@ struct _Py_global_strings {
401401
STRUCT_FOR_ID(default)
402402
STRUCT_FOR_ID(defaultaction)
403403
STRUCT_FOR_ID(delete)
404+
STRUCT_FOR_ID(denominator)
404405
STRUCT_FOR_ID(depth)
405406
STRUCT_FOR_ID(desired_access)
406407
STRUCT_FOR_ID(detect_types)
@@ -643,6 +644,7 @@ struct _Py_global_strings {
643644
STRUCT_FOR_ID(nt)
644645
STRUCT_FOR_ID(null)
645646
STRUCT_FOR_ID(number)
647+
STRUCT_FOR_ID(numerator)
646648
STRUCT_FOR_ID(obj)
647649
STRUCT_FOR_ID(object)
648650
STRUCT_FOR_ID(offset)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/_pydatetime.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1857,14 +1857,28 @@ def _fromtimestamp(cls, t, utc, tz):
18571857
18581858
A timezone info object may be passed in as well.
18591859
"""
1860-
frac, t = _math.modf(t)
1861-
us = round(frac * 1e6)
1862-
if us >= 1000000:
1860+
if isinstance(t, float):
1861+
frac, t = _math.modf(t)
1862+
us = round(frac * 1e6)
1863+
else:
1864+
try:
1865+
try:
1866+
n, d = t.as_integer_ratio()
1867+
except AttributeError:
1868+
n = t.numerator
1869+
d = t.denumerator
1870+
except AttributeError:
1871+
frac, t = _math.modf(t)
1872+
us = round(frac * 1e6)
1873+
else:
1874+
t, n = divmod(n, d)
1875+
us = _divide_and_round(n * 1_000_000, d)
1876+
if us >= 1_000_000:
18631877
t += 1
1864-
us -= 1000000
1878+
us -= 1_000_000
18651879
elif us < 0:
18661880
t -= 1
1867-
us += 1000000
1881+
us += 1_000_000
18681882

18691883
converter = _time.gmtime if utc else _time.localtime
18701884
y, m, d, hh, mm, ss, weekday, jday, dst = converter(t)

Lib/test/datetimetester.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2773,11 +2773,11 @@ def utcfromtimestamp(*args, **kwargs):
27732773
self.assertEqual(t, zero)
27742774
t = fts(D('0.000_000_5'))
27752775
self.assertEqual(t, zero)
2776-
t = fts(D('0.000_000_500_000_000_000_000_1'))
2776+
t = fts(D('0.000_000_500_000_000_000_000_000_000_000_000_000_001'))
27772777
self.assertEqual(t, one)
27782778
t = fts(D('0.000_000_9'))
27792779
self.assertEqual(t, one)
2780-
t = fts(D('0.999_999_499_999_999_9'))
2780+
t = fts(D('0.999_999_499_999_999_999_999_999_999_999_999_999_999'))
27812781
self.assertEqual(t.second, 0)
27822782
self.assertEqual(t.microsecond, 999_999)
27832783
t = fts(D('0.999_999_5'))
@@ -2790,6 +2790,21 @@ def utcfromtimestamp(*args, **kwargs):
27902790
self.assertEqual(t.second, 0)
27912791
self.assertEqual(t.microsecond, 7812)
27922792

2793+
t = fts(D('2_147_475_000.000_000_5'))
2794+
self.assertEqual(t.second, 0)
2795+
self.assertEqual(t.microsecond, 0)
2796+
t = fts(D('2_147_475_000'
2797+
'.000_000_500_000_000_000_000_000_000_000_000_000_001'))
2798+
self.assertEqual(t.second, 0)
2799+
self.assertEqual(t.microsecond, 1)
2800+
t = fts(D('2_147_475_000'
2801+
'.999_999_499_999_999_999_999_999_999_999_999_999_999'))
2802+
self.assertEqual(t.second, 0)
2803+
self.assertEqual(t.microsecond, 999_999)
2804+
t = fts(D('2_147_475_000.999_999_5'))
2805+
self.assertEqual(t.second, 1)
2806+
self.assertEqual(t.microsecond, 0)
2807+
27932808
@support.run_with_tz('MSK-03') # Something east of Greenwich
27942809
def test_microsecond_rounding_fraction(self):
27952810
F = fractions.Fraction
@@ -2824,11 +2839,13 @@ def utcfromtimestamp(*args, **kwargs):
28242839
self.assertEqual(t, zero)
28252840
t = fts(F(5, 10_000_000))
28262841
self.assertEqual(t, zero)
2827-
t = fts(F(5_000_000_000, 9_999_999_999_999_999))
2842+
t = fts(F( 5_000_000_000_000_000_000_000_000_000_000_000,
2843+
9_999_999_999_999_999_999_999_999_999_999_999_999_999))
28282844
self.assertEqual(t, one)
28292845
t = fts(F(9, 10_000_000))
28302846
self.assertEqual(t, one)
2831-
t = fts(F(9_999_995_000_000_000, 10_000_000_000_000_001))
2847+
t = fts(F( 9_999_995_000_000_000_000_000_000_000_000_000_000_000,
2848+
10_000_000_000_000_000_000_000_000_000_000_000_000_001))
28322849
self.assertEqual(t.second, 0)
28332850
self.assertEqual(t.microsecond, 999_999)
28342851
t = fts(F(9_999_995, 10_000_000))
@@ -2841,6 +2858,23 @@ def utcfromtimestamp(*args, **kwargs):
28412858
self.assertEqual(t.second, 0)
28422859
self.assertEqual(t.microsecond, 7812)
28432860

2861+
t = fts(2_147_475_000 + F(5, 10_000_000))
2862+
self.assertEqual(t.second, 0)
2863+
self.assertEqual(t.microsecond, 0)
2864+
t = fts(2_147_475_000 +
2865+
F( 5_000_000_000_000_000_000_000_000_000_000_000,
2866+
9_999_999_999_999_999_999_999_999_999_999_999_999_999))
2867+
self.assertEqual(t.second, 0)
2868+
self.assertEqual(t.microsecond, 1)
2869+
t = fts(2_147_475_000 +
2870+
F( 9_999_995_000_000_000_000_000_000_000_000_000_000_000,
2871+
10_000_000_000_000_000_000_000_000_000_000_000_000_001))
2872+
self.assertEqual(t.second, 0)
2873+
self.assertEqual(t.microsecond, 999_999)
2874+
t = fts(2_147_475_000 + F(9_999_995, 10_000_000))
2875+
self.assertEqual(t.second, 1)
2876+
self.assertEqual(t.microsecond, 0)
2877+
28442878
def test_timestamp_limits(self):
28452879
with self.subTest("minimum UTC"):
28462880
min_dt = self.theclass.min.replace(tzinfo=timezone.utc)

Lib/test/test_os.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -951,16 +951,12 @@ def ns_to_sec(ns):
951951
@staticmethod
952952
def ns_to_sec_decimal(ns):
953953
# Convert a number of nanosecond (int) to a number of seconds (Decimal).
954-
# Round towards infinity by adding 0.5 nanosecond to avoid rounding
955-
# issue, os.utime() rounds towards minus infinity.
956-
return decimal.Decimal('1e-9') * ns + decimal.Decimal('0.5e-9')
954+
return decimal.Decimal('1e-9') * ns
957955

958956
@staticmethod
959957
def ns_to_sec_fraction(ns):
960958
# Convert a number of nanosecond (int) to a number of seconds (Fraction).
961-
# Round towards infinity by adding 0.5 nanosecond to avoid rounding
962-
# issue, os.utime() rounds towards minus infinity.
963-
return fractions.Fraction(ns, 10**9) + fractions.Fraction(1, 2*10**9)
959+
return fractions.Fraction(ns, 10**9)
964960

965961
def test_utime_by_indexed(self):
966962
# pass times as floating-point seconds as the second indexed parameter
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
Functions that take timestamp or timeout arguments now accept any real
22
numbers (such as :class:`~decimal.Decimal` and :class:`~fractions.Fraction`),
3-
not only integers or floats, although this does not improve precision.
3+
not only integers or floats.
4+
This allows to avoid the precision loss caused by the :class:`float` type.

0 commit comments

Comments
 (0)