From c0d6e18ff15887e871fe5aeebd2ccbb1b26ec91e Mon Sep 17 00:00:00 2001 From: mmaxjr Date: Thu, 30 Jul 2026 09:55:54 -0300 Subject: [PATCH 1/2] fix(intword): correct decillion-to-googol gap and carry-over rounding `powers` jumps from decillion (10**33) directly to googol (10**100), so any value in [10**36, 10**100) was chopped into a decillion count that grew arbitrarily large (e.g. 10**50 -> "100000000000000000.0 decillion"), and values that should round up into googol (e.g. 10**100 - 10**93) never carried over. Only advance to the next named unit when the value actually rounds up to it; otherwise there is no unit for that magnitude, so fall back to the plain integer, mirroring how intword() already falls back to a plain value below the smallest unit. Fixes #356 Co-Authored-By: Claude Sonnet 4.6 --- src/humanize/number.py | 22 ++++++++++++++++++---- tests/test_number.py | 17 ++++++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/humanize/number.py b/src/humanize/number.py index 2fb22c6..bc24a4b 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -255,10 +255,24 @@ 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 - ordinal += 1 - rounded_value = 1.0 + if not largest_ordinal and rounded_value >= 1000: + next_power = powers[ordinal + 1] + if next_power == power * 1000: + # After rounding, we end up just at the next power + ordinal += 1 + rounded_value = 1.0 + else: + # `powers` has a gap between this unit and the next one (e.g. + # decillion to googol), so there is no unit for "1000+ of the + # current one". Only bump to the next unit if the value is + # close enough to actually round up to it; otherwise there is + # no name for this magnitude, so fall back to the plain number. + next_rounded_value = float(format % (value / next_power)) + if next_rounded_value >= 1.0: + ordinal += 1 + rounded_value = next_rounded_value + else: + return f"{negative_prefix}{value}" singular, plural = human_powers[ordinal] unit = _ngettext(singular, plural, math.ceil(rounded_value)) diff --git a/tests/test_number.py b/tests/test_number.py index 78639c3..a731a83 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -116,10 +116,21 @@ def test_intword_powers() -> None: (["3500000000000000000000"], "3.5 sextillion"), (["8100000000000000000000000000000000"], "8.1 decillion"), (["-8100000000000000000000000000000000"], "-8.1 decillion"), - ([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"), + ( + [1_000_000_000_000_000_000_000_000_000_000_000_000], + "1000000000000000000000000000000000000", + ), + ( + [1_100_000_000_000_000_000_000_000_000_000_000_000], + "1100000000000000000000000000000000000", + ), + ( + [2_100_000_000_000_000_000_000_000_000_000_000_000], + "2100000000000000000000000000000000000", + ), ([2e100], "2.0 googol"), + ([10**50], str(10**50)), + ([10**100 - 10**93], "1.0 googol"), ([None], "None"), (["1230000", "%0.2f"], "1.23 million"), ([10**100], "1.0 googol"), From 702e6318025f408e198c63b4d9335720659c3803 Mon Sep 17 00:00:00 2001 From: mmaxjr Date: Wed, 5 Aug 2026 11:33:24 -0300 Subject: [PATCH 2/2] Respect translated intword units across scale gaps --- src/humanize/number.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/humanize/number.py b/src/humanize/number.py index bc24a4b..f59f725 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -266,13 +266,18 @@ def intword(value: NumberOrString, format: str = "%.1f") -> str: # decillion to googol), so there is no unit for "1000+ of the # current one". Only bump to the next unit if the value is # close enough to actually round up to it; otherwise there is - # no name for this magnitude, so fall back to the plain number. + # no English name for this magnitude. Some locales translate the + # current unit to a larger local scale, so allow those to keep + # using the translated unit instead of falling back. next_rounded_value = float(format % (value / next_power)) if next_rounded_value >= 1.0: ordinal += 1 rounded_value = next_rounded_value else: - return f"{negative_prefix}{value}" + singular, plural = human_powers[ordinal] + translated_unit = _ngettext(singular, plural, math.ceil(rounded_value)) + if translated_unit in (singular, plural): + return f"{negative_prefix}{value}" singular, plural = human_powers[ordinal] unit = _ngettext(singular, plural, math.ceil(rounded_value))