Skip to content

Commit eeaccc3

Browse files
author
Daniel Gillet
committed
Fix intword plural in respect to str format
Fixes #175
1 parent bd3a561 commit eeaccc3

File tree

3 files changed

+74
-33
lines changed

3 files changed

+74
-33
lines changed

src/humanize/number.py

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import bisect
6+
57
from .i18n import _gettext as _
68
from .i18n import _ngettext, decimal_separator, thousands_separator
79
from .i18n import _ngettext_noop as NS_
@@ -194,8 +196,8 @@ def intword(value: NumberOrString, format: str = "%.1f") -> str:
194196
"""Converts a large integer to a friendly text representation.
195197
196198
Works best for numbers over 1 million. For example, 1_000_000 becomes "1.0 million",
197-
1200000 becomes "1.2 million" and "1_200_000_000" becomes "1.2 billion". Supports up
198-
to decillion (33 digits) and googol (100 digits).
199+
1_200_000 becomes "1.2 million" and "1_200_000_000" becomes "1.2 billion". Supports
200+
up to decillion (33 digits) and googol (100 digits).
199201
200202
Examples:
201203
```pycon
@@ -241,29 +243,26 @@ def intword(value: NumberOrString, format: str = "%.1f") -> str:
241243
negative_prefix = ""
242244

243245
if value < powers[0]:
244-
return negative_prefix + str(value)
245-
246-
for ordinal_, power in enumerate(powers[1:], 1):
247-
if value < power:
248-
chopped = value / float(powers[ordinal_ - 1])
249-
powers_difference = powers[ordinal_] / powers[ordinal_ - 1]
250-
if float(format % chopped) == powers_difference:
251-
chopped = value / float(powers[ordinal_])
252-
singular, plural = human_powers[ordinal_]
253-
return (
254-
negative_prefix
255-
+ " ".join(
256-
[format, _ngettext(singular, plural, math.ceil(chopped))]
257-
)
258-
) % chopped
259-
260-
singular, plural = human_powers[ordinal_ - 1]
261-
return (
262-
negative_prefix
263-
+ " ".join([format, _ngettext(singular, plural, math.ceil(chopped))])
264-
) % chopped
265-
266-
return negative_prefix + str(value)
246+
return f"{negative_prefix}{value}"
247+
248+
ordinal = bisect.bisect_right(powers, value)
249+
largest_ordinal = ordinal == len(powers)
250+
251+
# Consider the biggest power of 10 that is smaller than value
252+
ordinal -= 1
253+
power = powers[ordinal]
254+
chopped = value / power
255+
rounded_value = float(format % chopped)
256+
257+
if not largest_ordinal and rounded_value * power == powers[ordinal + 1]:
258+
# After rounding, we end up just at the next power
259+
ordinal += 1
260+
power = powers[ordinal]
261+
rounded_value = 1.0
262+
263+
singular, plural = human_powers[ordinal]
264+
unit = _ngettext(singular, plural, math.ceil(rounded_value))
265+
return f"{negative_prefix}{rounded_value} {unit}"
267266

268267

269268
def apnumber(value: NumberOrString) -> str:

tests/test_i18n.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,56 @@ def test_naturaldelta() -> None:
9191
@pytest.mark.parametrize(
9292
"locale, number, expected_result",
9393
[
94-
("es_ES", 1000000, "1.0 millón"),
95-
("es_ES", 3500000, "3.5 millones"),
96-
("es_ES", 1000000000, "1.0 billón"),
97-
("es_ES", 1200000000, "1.2 billones"),
98-
("es_ES", 1000000000000, "1.0 trillón"),
99-
("es_ES", 6700000000000, "6.7 trillones"),
94+
("es_ES", 1_000_000, "1.0 millón"),
95+
("es_ES", 3_500_000, "3.5 millones"),
96+
("es_ES", 1_000_000_000, "1.0 billón"),
97+
("es_ES", 1_200_000_000, "1.2 billones"),
98+
("es_ES", 1_000_000_000_000, "1.0 trillón"),
99+
("es_ES", 6_700_000_000_000, "6.7 trillones"),
100+
("fr_FR", "1_000", "1.0 mille"),
101+
("fr_FR", "12_400", "12.4 milles"),
102+
("fr_FR", "12_490", "12.5 milles"),
103+
("fr_FR", "1_000_000", "1.0 million"),
104+
("fr_FR", "-1_000_000", "-1.0 million"),
105+
("fr_FR", "1_200_000", "1.2 millions"),
106+
("fr_FR", "1_290_000", "1.3 millions"),
107+
("fr_FR", "999_999_999", "1.0 milliard"),
108+
("fr_FR", "1_000_000_000", "1.0 milliard"),
109+
("fr_FR", "-1_000_000_000", "-1.0 milliard"),
110+
("fr_FR", "2_000_000_000", "2.0 milliards"),
111+
("fr_FR", "999_999_999_999", "1.0 billion"),
112+
("fr_FR", "1_000_000_000_000", "1.0 billion"),
113+
("fr_FR", "6_000_000_000_000", "6.0 billions"),
114+
("fr_FR", "-6_000_000_000_000", "-6.0 billions"),
115+
("fr_FR", "999_999_999_999_999", "1.0 billiard"),
116+
("fr_FR", "1_000_000_000_000_000", "1.0 billiard"),
117+
("fr_FR", "1_300_000_000_000_000", "1.3 billiards"),
118+
("fr_FR", "-1_300_000_000_000_000", "-1.3 billiards"),
119+
("fr_FR", "3_500_000_000_000_000_000_000", "3.5 trilliards"),
120+
("fr_FR", "8_100_000_000_000_000_000_000_000_000_000_000", "8.1 quintilliards"),
121+
(
122+
"fr_FR",
123+
"-8_100_000_000_000_000_000_000_000_000_000_000",
124+
"-8.1 quintilliards",
125+
),
126+
(
127+
"fr_FR",
128+
1_000_000_000_000_000_000_000_000_000_000_000_000,
129+
"1000.0 quintilliards",
130+
),
131+
(
132+
"fr_FR",
133+
1_100_000_000_000_000_000_000_000_000_000_000_000,
134+
"1100.0 quintilliards",
135+
),
136+
(
137+
"fr_FR",
138+
2_100_000_000_000_000_000_000_000_000_000_000_000,
139+
"2100.0 quintilliards",
140+
),
100141
],
101142
)
102-
def test_intword_plurals(locale: str, number: int, expected_result: str) -> None:
143+
def test_intword_i18n(locale: str, number: int, expected_result: str) -> None:
103144
try:
104145
humanize.i18n.activate(locale)
105146
except FileNotFoundError:

tests/test_number.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,10 @@ def test_intword_powers() -> None:
119119
([1_000_000_000_000_000_000_000_000_000_000_000_000], "1000.0 decillion"),
120120
([1_100_000_000_000_000_000_000_000_000_000_000_000], "1100.0 decillion"),
121121
([2_100_000_000_000_000_000_000_000_000_000_000_000], "2100.0 decillion"),
122+
([2e100], "2.0 googol"),
122123
([None], "None"),
123124
(["1230000", "%0.2f"], "1.23 million"),
124-
([10**101], "1" + "0" * 101),
125+
([10**101], "10.0 googol"),
125126
([math.nan], "NaN"),
126127
([math.inf], "+Inf"),
127128
([-math.inf], "-Inf"),

0 commit comments

Comments
 (0)