Skip to content

Commit bde649f

Browse files
authored
Performance improvements: 1.07x - 8.4x (#315)
1 parent e7c04f4 commit bde649f

5 files changed

Lines changed: 47 additions & 79 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ lint.select = [
6767
"ICN", # flake8-import-conventions
6868
"ISC", # flake8-implicit-str-concat
6969
"LOG", # flake8-logging
70+
"PERF", # perflint
7071
"PGH", # pygrep-hooks
7172
"PIE", # flake8-pie
7273
"PT", # flake8-pytest-style

src/humanize/i18n.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020

2121
# Mapping of locale to thousands separator
22-
_THOUSANDS_SEPARATOR = {
22+
_THOUSANDS_SEPARATOR: dict[str | None, str] = {
2323
"de_DE": ".",
2424
"fr_FR": " ",
2525
"it_IT": ".",
@@ -29,7 +29,7 @@
2929
}
3030

3131
# Mapping of locale to decimal separator
32-
_DECIMAL_SEPARATOR = {
32+
_DECIMAL_SEPARATOR: dict[str | None, str] = {
3333
"de_DE": ",",
3434
"fr_FR": ".",
3535
"it_IT": ",",
@@ -51,10 +51,7 @@ def _get_default_locale_path() -> pathlib.Path | None:
5151

5252

5353
def get_translation() -> gettext_module.NullTranslations:
54-
try:
55-
return _TRANSLATIONS[_CURRENT.locale]
56-
except (AttributeError, KeyError):
57-
return _TRANSLATIONS[None]
54+
return _TRANSLATIONS.get(getattr(_CURRENT, "locale", None), _TRANSLATIONS[None])
5855

5956

6057
def activate(
@@ -188,11 +185,7 @@ def thousands_separator() -> str:
188185
Returns:
189186
str: Thousands separator.
190187
"""
191-
try:
192-
sep = _THOUSANDS_SEPARATOR[_CURRENT.locale]
193-
except (AttributeError, KeyError):
194-
sep = ","
195-
return sep
188+
return _THOUSANDS_SEPARATOR.get(getattr(_CURRENT, "locale", None), ",")
196189

197190

198191
def decimal_separator() -> str:
@@ -201,8 +194,4 @@ def decimal_separator() -> str:
201194
Returns:
202195
str: Decimal separator.
203196
"""
204-
try:
205-
sep = _DECIMAL_SEPARATOR[_CURRENT.locale]
206-
except (AttributeError, KeyError):
207-
sep = "."
208-
return sep
197+
return _DECIMAL_SEPARATOR.get(getattr(_CURRENT, "locale", None), ".")

src/humanize/lists.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ def natural_list(items: list[Any]) -> str:
3333
elif len(items) == 2:
3434
return f"{str(items[0])} and {str(items[1])}"
3535
else:
36-
return ", ".join(str(item) for item in items[:-1]) + f" and {str(items[-1])}"
36+
return ", ".join([str(item) for item in items[:-1]]) + f" and {str(items[-1])}"

src/humanize/number.py

Lines changed: 28 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@
3131
"9": "⁹",
3232
"-": "⁻",
3333
}
34+
_SUPERSCRIPT_TRANS = str.maketrans(_SUPERSCRIPT_MAP)
35+
36+
_ORDINAL_SUFFIXES = ("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th")
37+
_APNUMBER_WORDS = (
38+
"zero",
39+
"one",
40+
"two",
41+
"three",
42+
"four",
43+
"five",
44+
"six",
45+
"seven",
46+
"eight",
47+
"nine",
48+
)
3449

3550

3651
def _format_not_finite(value: float) -> str:
@@ -91,35 +106,9 @@ def ordinal(value: NumberOrString, gender: str = "male") -> str:
91106
value = int(value)
92107
except (TypeError, ValueError):
93108
return str(value)
94-
if gender == "male":
95-
t = (
96-
P_("0 (male)", "th"),
97-
P_("1 (male)", "st"),
98-
P_("2 (male)", "nd"),
99-
P_("3 (male)", "rd"),
100-
P_("4 (male)", "th"),
101-
P_("5 (male)", "th"),
102-
P_("6 (male)", "th"),
103-
P_("7 (male)", "th"),
104-
P_("8 (male)", "th"),
105-
P_("9 (male)", "th"),
106-
)
107-
else:
108-
t = (
109-
P_("0 (female)", "th"),
110-
P_("1 (female)", "st"),
111-
P_("2 (female)", "nd"),
112-
P_("3 (female)", "rd"),
113-
P_("4 (female)", "th"),
114-
P_("5 (female)", "th"),
115-
P_("6 (female)", "th"),
116-
P_("7 (female)", "th"),
117-
P_("8 (female)", "th"),
118-
P_("9 (female)", "th"),
119-
)
120-
if value % 100 in (11, 12, 13): # special case
121-
return f"{value}{t[0]}"
122-
return f"{value}{t[value % 10]}"
109+
gender = "male" if gender == "male" else "female"
110+
digit = 0 if value % 100 in (11, 12, 13) else value % 10
111+
return f"{value}{P_(f'{digit} ({gender})', _ORDINAL_SUFFIXES[digit])}"
123112

124113

125114
def intcomma(value: NumberOrString, ndigits: int | None = None) -> str:
@@ -177,17 +166,12 @@ def intcomma(value: NumberOrString, ndigits: int | None = None) -> str:
177166
return str(value)
178167

179168
if ndigits is not None:
180-
orig = "{0:.{1}f}".format(value, ndigits)
169+
result = f"{value:,.{ndigits}f}"
181170
else:
182-
orig = str(value)
183-
orig = orig.replace(".", decimal_sep)
184-
import re
185-
186-
while True:
187-
new = re.sub(r"^(-?\d+)(\d{3})", rf"\g<1>{thousands_sep}\g<2>", orig)
188-
if orig == new:
189-
return new
190-
orig = new
171+
result = f"{value:,}"
172+
if thousands_sep != "," or decimal_sep != ".":
173+
result = result.translate(str.maketrans(",.", thousands_sep + decimal_sep))
174+
return result
191175

192176

193177
powers = [10**x for x in (3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 100)]
@@ -319,18 +303,7 @@ def apnumber(value: NumberOrString) -> str:
319303
return str(value)
320304
if not 0 <= value < 10:
321305
return str(value)
322-
return (
323-
_("zero"),
324-
_("one"),
325-
_("two"),
326-
_("three"),
327-
_("four"),
328-
_("five"),
329-
_("six"),
330-
_("seven"),
331-
_("eight"),
332-
_("nine"),
333-
)[value]
306+
return _(_APNUMBER_WORDS[value])
334307

335308

336309
def fractional(value: NumberOrString) -> str:
@@ -436,14 +409,12 @@ def scientific(value: NumberOrString, precision: int = 2) -> str:
436409
return _format_not_finite(value)
437410
except (ValueError, TypeError):
438411
return str(value)
439-
fmt = f"{{:.{str(int(precision))}e}}"
412+
fmt = f"{{:.{int(precision)}e}}"
440413
n = fmt.format(value)
441414
part1, part2 = n.split("e")
442-
# Remove redundant leading '+' or '0's (preserving the last '0' for 10⁰).
443-
import re
444-
445-
part2 = re.sub(r"^\+?(\-?)0*(.+)$", r"\1\2", part2)
446-
return part1 + " x 10" + "".join([_SUPERSCRIPT_MAP[char] for char in part2])
415+
# Normalise exponent: int() strips the "+" sign and leading zeros,
416+
# while preserving "-" and a single "0" for 10⁰.
417+
return part1 + " x 10" + str(int(part2)).translate(_SUPERSCRIPT_TRANS)
447418

448419

449420
def clamp(

src/humanize/time.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ def _convert_aware_datetime(
301301
value: dt.datetime | dt.timedelta | float | None,
302302
) -> Any:
303303
"""Convert aware datetime to naive datetime and pass through any other type."""
304+
if value is None:
305+
return None
306+
304307
import datetime as dt
305308

306309
if isinstance(value, dt.datetime) and value.tzinfo is not None:
@@ -552,9 +555,13 @@ def precisedelta(
552555
secs = delta.seconds
553556
usecs = delta.microseconds
554557

555-
MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS, MONTHS, YEARS = list(
556-
Unit
557-
)
558+
MILLISECONDS = Unit.MILLISECONDS
559+
SECONDS = Unit.SECONDS
560+
MINUTES = Unit.MINUTES
561+
HOURS = Unit.HOURS
562+
DAYS = Unit.DAYS
563+
MONTHS = Unit.MONTHS
564+
YEARS = Unit.YEARS
558565

559566
# Given DAYS compute YEARS and the remainder of DAYS as follows:
560567
# if YEARS is the minimum unit, we cannot use DAYS so
@@ -631,14 +638,14 @@ def precisedelta(
631638
("%d microsecond", "%d microseconds", usecs),
632639
]
633640

641+
import math
642+
634643
texts: list[str] = []
635644
for unit, fmt in zip(reversed(Unit), fmts):
636645
singular_txt, plural_txt, fmt_value = fmt
637646
if fmt_value > 0 or (not texts and unit == min_unit):
638647
_fmt_value = 2 if 1 < fmt_value < 2 else int(fmt_value)
639648
fmt_txt = _ngettext(singular_txt, plural_txt, _fmt_value)
640-
import math
641-
642649
if unit == min_unit and math.modf(fmt_value)[0] > 0:
643650
fmt_txt = fmt_txt.replace("%d", format)
644651
elif unit == YEARS:

0 commit comments

Comments
 (0)