Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,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`.)


Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,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)
Expand Down Expand Up @@ -643,6 +644,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)
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 19 additions & 5 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 38 additions & 4 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -2773,11 +2773,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'))
Expand All @@ -2790,6 +2790,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
Expand Down Expand Up @@ -2824,11 +2839,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))
Expand All @@ -2841,6 +2858,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)
Expand Down
8 changes: 2 additions & 6 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -951,16 +951,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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
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.
Loading
Loading