fix: round decimal unit values instead of truncating in integer path

Multiplying a decimal quantity by its unit multiplier in binary floating
point can land just below the exact integer (float('2.05') * 60 ==
122.999...), so int() truncated 2.05m to 122 seconds instead of 123.
Round the sub-second unit contribution before adding whole seconds.
This commit is contained in:
dualfroz committed 2026-09-04 09:22:40 +02:00
1 parent 546a3e846e
commit 7b055c8116
3 files changed
+91 -3

No files matched your search

+10
View File
@@ -358,6 +358,16 @@ class TestTimeparse(unittest.TestCase):
self.assertEqual(timeparse.timeparse('+5.6 weeks'), 3386880)
self.assertEqual(timeparse.timeparse('-5.6 weeks'), -3386880)
def test_timeparse_fractional_rounding(self):
'''Decimal quantities that equal a whole number of seconds must not be
truncated by binary floating-point error (e.g. 2.05 * 60 == 122.999...).'''
self.assertEqual(timeparse.timeparse('2.05m'), 123)
self.assertEqual(timeparse.timeparse('+2.05m'), 123)
self.assertEqual(timeparse.timeparse('-2.05m'), -123)
self.assertEqual(timeparse.timeparse('4.1m'), 246)
self.assertEqual(timeparse.timeparse('1.13h'), 4068)
self.assertEqual(timeparse.timeparse('8.03h'), 28908)
def test_doctest(self):
'''Run timeparse doctests.'''
self.assertTrue(doctest.testmod(timeparse, raise_on_error=True))
+7 -3
View File
@@ -172,10 +172,14 @@ def timeparse(sval, granularity='seconds'):
elif ('secs' not in mdict or
mdict['secs'] is None or
mdict['secs'].isdigit()):
# we will return an integer
# we will return an integer. Round (not truncate) the
# sub-second unit contribution: multiplying a decimal
# quantity such as 2.05 minutes by 60 yields a binary
# float slightly below the exact value (122.999...), and
# int() would truncate it to 122 instead of 123.
return (
sign * int(sum([MULTIPLIERS[k] * float(v) for (k, v) in
list(mdict.items()) if k != 'secs' and v is not None])) +
sign * int(round(sum([MULTIPLIERS[k] * float(v) for (k, v) in
list(mdict.items()) if k != 'secs' and v is not None]))) +
(int(mdict['secs'], 10) if mdict['secs'] else 0))
else:
# SECS is a float, we will return a float