From 81c649efa1327b1b5e4e02d06e68fc2dac4b33ad Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 09:31:35 +0200 Subject: [PATCH] Upstream wroberts/pytimeparse state of the touched files before 3270e0d981 --- PR.md | 74 ------------------------------ pytimeparse/tests/testtimeparse.py | 4 -- pytimeparse/timeparse.py | 2 +- 3 files changed, 1 insertion(+), 79 deletions(-) delete mode 100644 PR.md diff --git a/PR.md b/PR.md deleted file mode 100644 index e641706..0000000 --- a/PR.md +++ /dev/null @@ -1,74 +0,0 @@ -# Fix off-by-one from float truncation in the integer result path - -## Problem - -`timeparse` returns a value one second too small for many decimal quantities -that are in fact an exact whole number of seconds: - -```python ->>> from pytimeparse import parse ->>> parse('2.05m') -122 # 2.05 minutes == 123 seconds ->>> parse('4.1m') -245 # == 246 seconds ->>> parse('1.13h') -4067 # == 4068 seconds -``` - -`parse('1.2 m')` already documents that decimal quantities should yield exact -integers (it returns `72`). The results above break that contract. A scan of -decimal quantities of minutes/hours/days/weeks whose exact value is an integral -number of seconds found 365 such wrong results in the range x.01 .. x.99. - -## Root cause - -`pytimeparse/timeparse.py`, in the branch that returns an integer when the -seconds field is whole or absent: - -```python -return ( - sign * int(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)) -``` - -Multiplying a decimal quantity by its unit multiplier is done in binary -floating point, which for many values lands just below the exact integer: -`float('2.05') * 60 == 122.99999999999999`. `int(...)` then truncates toward -zero, dropping the last second (123 -> 122). - -## Fix - -Round the sub-second unit contribution to the nearest integer before adding the -whole-seconds field: - -```python -sign * int(round(sum([...]))) + (int(mdict['secs'], 10) if mdict['secs'] else 0) -``` - -`round` collapses the `122.999...`/`123.000...1` float artifacts back to the -exact integer. The whole-seconds field is still added exactly, and the float -result path (when the seconds field itself is fractional) is unchanged. - -## Test - -Added `test_timeparse_fractional_rounding` to -`pytimeparse/tests/testtimeparse.py`: - -```python -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) -``` - -## Gate - -- `python -m pytest pytimeparse/tests/testtimeparse.py` -> 50 passed - (49 pre-existing, including the exact-integer cases `1.24 days`, `5.6 wk`, - plus the new test). -- Re-ran the exhaustive x.01 .. x.99 scan for minutes/hours/days/weeks: - 0 remaining truncation mismatches (was 365). - -Counterfactual (fix reverted, test kept): fails with -`AssertionError: 122 != 123`. diff --git a/pytimeparse/tests/testtimeparse.py b/pytimeparse/tests/testtimeparse.py index 29f4cef..c305bb5 100644 --- a/pytimeparse/tests/testtimeparse.py +++ b/pytimeparse/tests/testtimeparse.py @@ -83,10 +83,6 @@ class TestTimeparse(unittest.TestCase): self.assertEqual(timeparse.timeparse('- 32 m 1 s'), -1921) self.assertIsNone(timeparse.timeparse('32 m - 1 s')) self.assertIsNone(timeparse.timeparse('32 m + 1 s')) - # A stray pipe is not a valid sign and must not be silently accepted. - self.assertIsNone(timeparse.timeparse('|5m')) - self.assertIsNone(timeparse.timeparse('| 5m')) - self.assertIsNone(timeparse.timeparse('|1:24')) def test_timeparse_1(self): '''timeparse test case 1.''' diff --git a/pytimeparse/timeparse.py b/pytimeparse/timeparse.py index 346596f..ace5cc3 100644 --- a/pytimeparse/timeparse.py +++ b/pytimeparse/timeparse.py @@ -33,7 +33,7 @@ kinds of time expressions. import re -SIGN = r'(?P[+-])?' +SIGN = r'(?P[+|-])?' #YEARS = r'(?P\d+)\s*(?:ys?|yrs?.?|years?)' #MONTHS = r'(?P\d+)\s*(?:mos?.?|mths?.?|months?)' WEEKS = r'(?P[\d.]+)\s*(?:w|wks?|weeks?)'