Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
49 changes: 24 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,26 @@ 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
power = powers[ordinal]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't used:

Suggested change
power = powers[ordinal]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a lot of refactoring. Thanks for noticing that this was still here. You're 100% right. It's not even needed anymore. :)

rounded_value = 1.0

singular, plural = human_powers[ordinal]
unit = _ngettext(singular, plural, math.ceil(rounded_value))
return f"{negative_prefix}{rounded_value} {unit}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this introduces a regression?

Before:

>>> intword(1234567, "%.0f")
1 million

PR:

>>> intword(1234567, "%.0f")
1.0 million

Try this:

Suggested change
return f"{negative_prefix}{rounded_value} {unit}"
return f"{negative_prefix}{format % rounded_value} {unit}"

And we could add some test cases like this to the main test_intword():

        (["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"),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good catch! I added what you suggested. Thank you!



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
3 changes: 2 additions & 1 deletion tests/test_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ 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"),
Expand Down
Loading