From 4a8dbb85be6c2b4925076dba62dbcc18f1a18a40 Mon Sep 17 00:00:00 2001 From: Sreekant Baheti <60787859+Sreekant13@users.noreply.github.com> Date: Tue, 15 Sep 2026 20:21:53 -0700 Subject: [PATCH] Carry intword() to the next unit for values >= 10^24 The rounding carry compared `rounded_value * power == powers[ordinal + 1]`, a float product against an exact int power. `float(10**k) != 10**k` for k >= 24, so from septillion up the equality was False and the carry was skipped: `10**24 - 1` rendered as "1000.0 sextillion" instead of "1.0 septillion". Compare the rounded mantissa against the exact integer ratio between adjacent powers instead; every power is a power of ten so the ratio is exact, and it stays large across the decillion-to-googol gap, so `10**36` is unchanged at "1000.0 decillion". Fixes #400. --- src/humanize/number.py | 15 +++++++++++++-- tests/test_number.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/humanize/number.py b/src/humanize/number.py index 727435f2..0fe8d1c7 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -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 diff --git a/tests/test_number.py b/tests/test_number.py index cf55ee1d..8f7d3713 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -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 " 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"),