intword() has a rounding-carry step so that a value just under a power boundary reads as the next unit: 999_999 becomes "1.0 million", not "1000.0 thousand". That carry silently stops working from septillion (10^24) upward, so large numbers just under a boundary render as "1000.0 <smaller unit>".
Version: humanize main (4.x). Python 3.10+ (independent of version; it is a float-representation issue).
Repro
from humanize import intword
print(intword(10**24 - 1)) # '1000.0 sextillion' <- want '1.0 septillion'
print(intword(10**27 - 1)) # '1000.0 septillion' <- want '1.0 octillion'
print(intword(10**30 - 1)) # '1000.0 octillion' <- want '1.0 nonillion'
print(intword(10**33 - 1)) # '1000.0 nonillion' <- want '1.0 decillion'
For comparison, the same shape one bucket lower is fine, because the carry still fires there:
print(intword(10**9 - 1)) # '1.0 billion' (correct)
print(intword(10**21 - 1)) # '1.0 sextillion' (correct)
So the boundary is exactly 10**24: 10**6 .. 10**21 carry, 10**24 .. 10**33 do not.
Root cause
The carry check multiplies the rounded mantissa (a float) by the power and compares it to the next power as an exact int:
if not largest_ordinal and rounded_value * power == powers[ordinal + 1]:
powers holds Python ints (10**24, 10**27, ...), but float(10**k) != 10**k once k >= 24 (a float has 52 bits of mantissa, and 10**24 needs more). So for 10**24 - 1:
rounded_value = 1000.0 # float("%.1f" % 999.999...)
power = 10**21
rounded_value * power == 10**24 # -> False: LHS is an inexact float, RHS an exact int
The equality is False, the carry is skipped, and the output keeps the smaller unit with a 1000.0 mantissa. Below 10**24 the intermediate float(10**k) is exact, so the same expression is True and the carry works, which is why this went unnoticed.
>>> 1000.0 * 10**21 == 10**24
False
>>> 1000.0 * 10**18 == 10**21
True
Not the same as #356
#356 is about the gap between decillion (10**33) and googol (10**100), where there is no unit to use and values render as large decillion counts (e.g. 10**36 -> "1000.0 decillion"). That is a missing-unit question. This issue is a plain carry that should fire and does not, purely because of the float-vs-int comparison; 10**24 through 10**33 all have a real next unit to carry into.
Fix
Compare the rounded mantissa against the integer ratio between adjacent powers instead of multiplying a float by a large int:
if not largest_ordinal and rounded_value >= powers[ordinal + 1] // power:
ordinal += 1
rounded_value = 1.0
Every entry in powers is a power of ten, so powers[ordinal + 1] // power is exact, and it stays large (10**67) across the decillion-to-googol gap, so 10**36 is still left as "1000.0 decillion" (the #356 behaviour is unchanged). PR with tests to follow.
intword()has a rounding-carry step so that a value just under a power boundary reads as the next unit:999_999becomes"1.0 million", not"1000.0 thousand". That carry silently stops working from septillion (10^24) upward, so large numbers just under a boundary render as"1000.0 <smaller unit>".Version: humanize main (4.x). Python 3.10+ (independent of version; it is a float-representation issue).
Repro
For comparison, the same shape one bucket lower is fine, because the carry still fires there:
So the boundary is exactly
10**24:10**6 .. 10**21carry,10**24 .. 10**33do not.Root cause
The carry check multiplies the rounded mantissa (a float) by the power and compares it to the next power as an exact
int:powersholds Python ints (10**24,10**27, ...), butfloat(10**k) != 10**koncek >= 24(a float has 52 bits of mantissa, and10**24needs more). So for10**24 - 1:The equality is
False, the carry is skipped, and the output keeps the smaller unit with a1000.0mantissa. Below10**24the intermediatefloat(10**k)is exact, so the same expression isTrueand the carry works, which is why this went unnoticed.Not the same as #356
#356 is about the gap between decillion (
10**33) and googol (10**100), where there is no unit to use and values render as large decillion counts (e.g.10**36->"1000.0 decillion"). That is a missing-unit question. This issue is a plain carry that should fire and does not, purely because of the float-vs-int comparison;10**24through10**33all have a real next unit to carry into.Fix
Compare the rounded mantissa against the integer ratio between adjacent powers instead of multiplying a float by a large int:
Every entry in
powersis a power of ten, sopowers[ordinal + 1] // poweris exact, and it stays large (10**67) across the decillion-to-googol gap, so10**36is still left as"1000.0 decillion"(the #356 behaviour is unchanged). PR with tests to follow.