Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/humanize/locale/fr_FR/LC_MESSAGES/humanize.po
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 23 additions & 25 deletions src/humanize/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
55 changes: 48 additions & 7 deletions tests/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion tests/test_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading