Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 10 additions & 0 deletions humanfriendly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,16 @@ def format_timespan(num_seconds, detailed=False, max_units=3):
result = []
num_seconds = decimal.Decimal(str(num_seconds))
relevant_units = list(reversed(time_units[0 if detailed else 3:]))
# Round the total number of seconds to the precision that will
# eventually be displayed for the smallest unit before decomposing
# it into units below. Rounding only the smallest unit's count in
# isolation (as used to happen here) can inflate its displayed
# value up to or past a whole unit of the next larger unit that
# was already emitted, producing nonsensical output such as
# "1 minute and 60 seconds" instead of "2 minutes".
smallest_divider = decimal.Decimal(str(relevant_units[-1]['divider']))
quantum = smallest_divider / 100
num_seconds = (num_seconds / quantum).to_integral_value(rounding=decimal.ROUND_HALF_EVEN) * quantum
for unit in relevant_units:
# Extract the unit count from the remaining time.
divider = decimal.Decimal(str(unit['divider']))
Expand Down
8 changes: 8 additions & 0 deletions humanfriendly/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,14 @@ def test_format_timespan(self):
assert '1 second and 15 milliseconds' == format_timespan(1.015, detailed=True)
assert '10 seconds and 15 milliseconds' == format_timespan(10.015, detailed=True)
assert '1 microsecond and 50 nanoseconds' == format_timespan(0.00000105, detailed=True)
# Test that rounding the smallest displayed unit never produces a
# count that has reached (or gone past) a whole unit of the next
# larger, already emitted unit, which used to render nonsensical
# output such as "1 minute and 60 seconds" instead of properly
# carrying the extra unit over.
assert '2 minutes' == format_timespan(119.999)
assert '1 hour' == format_timespan(3599.999)
assert '1 minute, 59 seconds and 999 milliseconds' == format_timespan(119.999, detailed=True)
# Test the datetime.timedelta support:
# https://github.com/xolox/python-humanfriendly/issues/27
now = datetime.datetime.now()
Expand Down