diff --git a/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po b/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po index 6a66589..6aae582 100644 --- a/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po +++ b/src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po @@ -140,7 +140,7 @@ msgstr[1] "milliards" #: src/humanize/number.py:186 msgid "trillion" msgid_plural "trillion" -msgstr[0] "billions" +msgstr[0] "billion" msgstr[1] "billions" #: src/humanize/number.py:187 diff --git a/src/humanize/number.py b/src/humanize/number.py index 8126e35..2ca7a69 100644 --- a/src/humanize/number.py +++ b/src/humanize/number.py @@ -2,6 +2,8 @@ from __future__ import annotations +import bisect + from .i18n import _gettext as _ from .i18n import _ngettext, decimal_separator, thousands_separator from .i18n import _ngettext_noop as NS_ @@ -194,8 +196,8 @@ def intword(value: NumberOrString, format: str = "%.1f") -> str: """Converts a large integer to a friendly text representation. Works best for numbers over 1 million. For example, 1_000_000 becomes "1.0 million", - 1200000 becomes "1.2 million" and "1_200_000_000" becomes "1.2 billion". Supports up - to decillion (33 digits) and googol (100 digits). + 1_200_000 becomes "1.2 million" and "1_200_000_000" becomes "1.2 billion". Supports + up to decillion (33 digits) and googol (100 digits). Examples: ```pycon @@ -241,29 +243,25 @@ def intword(value: NumberOrString, format: str = "%.1f") -> str: negative_prefix = "" if value < powers[0]: - return negative_prefix + str(value) - - for ordinal_, power in enumerate(powers[1:], 1): - if value < power: - chopped = value / float(powers[ordinal_ - 1]) - powers_difference = powers[ordinal_] / powers[ordinal_ - 1] - if float(format % chopped) == powers_difference: - chopped = value / float(powers[ordinal_]) - singular, plural = human_powers[ordinal_] - return ( - negative_prefix - + " ".join( - [format, _ngettext(singular, plural, math.ceil(chopped))] - ) - ) % chopped - - singular, plural = human_powers[ordinal_ - 1] - return ( - negative_prefix - + " ".join([format, _ngettext(singular, plural, math.ceil(chopped))]) - ) % chopped - - return negative_prefix + str(value) + return f"{negative_prefix}{value}" + + ordinal = bisect.bisect_right(powers, value) + largest_ordinal = ordinal == len(powers) + + # Consider the biggest power of 10 that is smaller than value + ordinal -= 1 + power = powers[ordinal] + 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 + + singular, plural = human_powers[ordinal] + unit = _ngettext(singular, plural, math.ceil(rounded_value)) + return f"{negative_prefix}{format % rounded_value} {unit}" def apnumber(value: NumberOrString) -> str: diff --git a/tests/test_i18n.py b/tests/test_i18n.py index d4204f2..6c16c94 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -91,15 +91,56 @@ def test_naturaldelta() -> None: @pytest.mark.parametrize( "locale, number, expected_result", [ - ("es_ES", 1000000, "1.0 millón"), - ("es_ES", 3500000, "3.5 millones"), - ("es_ES", 1000000000, "1.0 billón"), - ("es_ES", 1200000000, "1.2 billones"), - ("es_ES", 1000000000000, "1.0 trillón"), - ("es_ES", 6700000000000, "6.7 trillones"), + ("es_ES", 1_000_000, "1.0 millón"), + ("es_ES", 3_500_000, "3.5 millones"), + ("es_ES", 1_000_000_000, "1.0 billón"), + ("es_ES", 1_200_000_000, "1.2 billones"), + ("es_ES", 1_000_000_000_000, "1.0 trillón"), + ("es_ES", 6_700_000_000_000, "6.7 trillones"), + ("fr_FR", "1_000", "1.0 mille"), + ("fr_FR", "12_400", "12.4 milles"), + ("fr_FR", "12_490", "12.5 milles"), + ("fr_FR", "1_000_000", "1.0 million"), + ("fr_FR", "-1_000_000", "-1.0 million"), + ("fr_FR", "1_200_000", "1.2 millions"), + ("fr_FR", "1_290_000", "1.3 millions"), + ("fr_FR", "999_999_999", "1.0 milliard"), + ("fr_FR", "1_000_000_000", "1.0 milliard"), + ("fr_FR", "-1_000_000_000", "-1.0 milliard"), + ("fr_FR", "2_000_000_000", "2.0 milliards"), + ("fr_FR", "999_999_999_999", "1.0 billion"), + ("fr_FR", "1_000_000_000_000", "1.0 billion"), + ("fr_FR", "6_000_000_000_000", "6.0 billions"), + ("fr_FR", "-6_000_000_000_000", "-6.0 billions"), + ("fr_FR", "999_999_999_999_999", "1.0 billiard"), + ("fr_FR", "1_000_000_000_000_000", "1.0 billiard"), + ("fr_FR", "1_300_000_000_000_000", "1.3 billiards"), + ("fr_FR", "-1_300_000_000_000_000", "-1.3 billiards"), + ("fr_FR", "3_500_000_000_000_000_000_000", "3.5 trilliards"), + ("fr_FR", "8_100_000_000_000_000_000_000_000_000_000_000", "8.1 quintilliards"), + ( + "fr_FR", + "-8_100_000_000_000_000_000_000_000_000_000_000", + "-8.1 quintilliards", + ), + ( + "fr_FR", + 1_000_000_000_000_000_000_000_000_000_000_000_000, + "1000.0 quintilliards", + ), + ( + "fr_FR", + 1_100_000_000_000_000_000_000_000_000_000_000_000, + "1100.0 quintilliards", + ), + ( + "fr_FR", + 2_100_000_000_000_000_000_000_000_000_000_000_000, + "2100.0 quintilliards", + ), ], ) -def test_intword_plurals(locale: str, number: int, expected_result: str) -> None: +def test_intword_i18n(locale: str, number: int, expected_result: str) -> None: try: humanize.i18n.activate(locale) except FileNotFoundError: diff --git a/tests/test_number.py b/tests/test_number.py index 705ed43..cb74fcf 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -119,14 +119,21 @@ def test_intword_powers() -> None: ([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"), + ([2e100], "2.0 googol"), ([None], "None"), (["1230000", "%0.2f"], "1.23 million"), - ([10**101], "1" + "0" * 101), + ([10**101], "10.0 googol"), ([math.nan], "NaN"), ([math.inf], "+Inf"), ([-math.inf], "-Inf"), (["nan"], "NaN"), (["-inf"], "-Inf"), + (["1234567", "%.0f"], "1 million"), + (["1234567", "%.1f"], "1.2 million"), + (["1234567", "%.2f"], "1.23 million"), + (["1234567", "%.3f"], "1.235 million"), + (["999500", "%.0f"], "1 million"), + (["999499", "%.0f"], "999 thousand"), ], ) def test_intword(test_args: list[str], expected: str) -> None: