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
15 changes: 13 additions & 2 deletions src/humanize/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,19 @@ def intword(value: NumberOrString, format: str = "%.1f") -> str:
chopped = value / power
rounded_value = float(format % chopped)

if not largest_ordinal and rounded_value * power == powers[ordinal + 1]:
# After rounding, we end up just at the next power
if not largest_ordinal and rounded_value >= powers[ordinal + 1] // power:
# After rounding, the mantissa reached the next power's boundary
# (e.g. 999_999 formats to "1000.0", which is 1.0 million).
#
# Compare the rounded mantissa against the integer ratio between
# adjacent powers, not `rounded_value * power == powers[ordinal + 1]`:
# that check multiplied a float by a large int, and `float(10**k)`
# is not exactly `10**k` for k >= 24, so the equality was silently
# False from septillion upward and the carry was skipped
# (`10**24 - 1` rendered as "1000.0 sextillion", not "1.0 septillion").
# Every entry in `powers` is a power of ten, so the ratio is exact,
# and it stays large across the decillion-to-googol gap, so a value
# like `10**36` is left as "1000.0 decillion" exactly as before.
ordinal += 1
rounded_value = 1.0

Expand Down
11 changes: 11 additions & 0 deletions tests/test_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ def test_intword_powers() -> None:
(["3500000000000000000000"], "3.5 sextillion"),
(["8100000000000000000000000000000000"], "8.1 decillion"),
(["-8100000000000000000000000000000000"], "-8.1 decillion"),
# A value just under a power boundary must carry to the next unit after
# rounding. The carry check multiplied the rounded mantissa by the
# power and compared to an exact int power; `float(10**k) != 10**k` for
# k >= 24, so from septillion up these silently rendered as
# "1000.0 <smaller unit>" instead of carrying.
([10**24 - 1], "1.0 septillion"),
([10**27 - 1], "1.0 octillion"),
([10**30 - 1], "1.0 nonillion"),
([10**33 - 1], "1.0 decillion"),
# ...but the decillion-to-googol gap has no unit to carry into, so a
# value in it stays a large decillion count (unchanged behaviour).
([1_000_000_000_000_000_000_000_000_000_000_000_000], "1000.0 decillion"),
([1_100_000_000_000_000_000_000_000_000_000_000_000], "1100.0 decillion"),
([2_100_000_000_000_000_000_000_000_000_000_000_000], "2100.0 decillion"),
Expand Down