Skip to content

Fix format_timespan() rounding the smallest unit past a whole unit of the next one - #86

Open
afonsojanu wants to merge 1 commit into
xolox:masterfrom
afonsojanu:fix/timespan-smallest-unit-rounding-carry
Open

afonsojanu wants to merge 1 commit into
xolox:masterfrom
afonsojanu:fix/timespan-smallest-unit-rounding-carry

Conversation

@afonsojanu

Copy link
Copy Markdown

When `format_timespan()` is close to a rollover point, the smallest unit it decides to show gets rounded to two decimal places for display, but every larger unit before it is computed by plain truncation. If the leftover fraction is close enough to round all the way up, the rounded count can equal or exceed a whole unit of the coarser value that was already emitted.

A couple of reproductions on the current release:

```pycon

format_timespan(119.999)
'1 minute and 60 seconds'
format_timespan(3599.999)
'59 minutes and 60 seconds'
```

Neither of those is a valid decomposition, "60 seconds" should have carried into the minute (or hour) count.

The fix rounds the total number of seconds to the precision that will end up being shown for the smallest active unit before the per-unit loop runs, instead of rounding only the smallest unit's own count afterwards. That way the existing truncating division for the coarser units naturally absorbs the carry, and no unit ever displays a count that belongs to the next unit up:

```pycon

format_timespan(119.999)
'2 minutes'
format_timespan(3599.999)
'1 hour'
```

I checked this against the existing test suite (all of the `format_timespan`/`parse_timespan`/rounding related cases still pass, including the fixes from issues #10 and #11) and ran a 200k-sample fuzz over random timespans in both detailed and non-detailed mode asserting no unit's rendered count reaches or exceeds the bound of the next coarser unit; it came back clean with the fix applied (and reproduces failures without it).

Added a regression test for the two cases above plus the detailed-mode equivalent.

format_timespan() rounds the display of its smallest shown unit to two
decimal places while every coarser unit is computed by truncation. When
the remaining fraction was close enough to round up to a whole unit, the
result could equal or exceed a full unit of the next larger (already
rendered) unit, e.g. format_timespan(119.999) produced "1 minute and 60
seconds" instead of "2 minutes", and format_timespan(3599.999) produced
"59 minutes and 60 seconds" instead of "1 hour".

Fix this by rounding the whole timespan (in seconds) to the precision
that will end up being displayed for the smallest unit before splitting
it into units, so the usual truncating division naturally carries the
extra unit upward instead of stranding an out of range count on the
smallest unit.

Added a regression test covering both the plain and detailed rendering
of the previously broken cases.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant