summaryrefslogtreecommitdiff
path: root/babel/numbers.py
diff options
context:
space:
mode:
Diffstat (limited to 'babel/numbers.py')
-rw-r--r--babel/numbers.py205
1 files changed, 151 insertions, 54 deletions
diff --git a/babel/numbers.py b/babel/numbers.py
index 94259f8..73155c4 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -18,13 +18,18 @@
# Padding and rounding increments in pattern:
# - https://www.unicode.org/reports/tr35/ (Appendix G.6)
from __future__ import annotations
+
import decimal
import re
-from datetime import date as date_, datetime as datetime_
+from typing import TYPE_CHECKING, Any, overload
import warnings
+from datetime import date as date_, datetime as datetime_
-from babel.core import default_locale, Locale, get_global
+from babel.core import Locale, default_locale, get_global
+from babel.localedata import LocaleDataDict
+if TYPE_CHECKING:
+ from typing_extensions import Literal
LC_NUMERIC = default_locale('LC_NUMERIC')
@@ -33,7 +38,7 @@ class UnknownCurrencyError(Exception):
"""Exception thrown when a currency is requested for which no data is available.
"""
- def __init__(self, identifier):
+ def __init__(self, identifier: str) -> None:
"""Create the exception.
:param identifier: the identifier string of the unsupported currency
"""
@@ -43,7 +48,7 @@ class UnknownCurrencyError(Exception):
self.identifier = identifier
-def list_currencies(locale=None):
+def list_currencies(locale: Locale | str | None = None) -> set[str]:
""" Return a `set` of normalized currency codes.
.. versionadded:: 2.5.0
@@ -61,7 +66,7 @@ def list_currencies(locale=None):
return set(currencies)
-def validate_currency(currency, locale=None):
+def validate_currency(currency: str, locale: Locale | str | None = None) -> None:
""" Check the currency code is recognized by Babel.
Accepts a ``locale`` parameter for fined-grained validation, working as
@@ -73,7 +78,7 @@ def validate_currency(currency, locale=None):
raise UnknownCurrencyError(currency)
-def is_currency(currency, locale=None):
+def is_currency(currency: str, locale: Locale | str | None = None) -> bool:
""" Returns `True` only if a currency is recognized by Babel.
This method always return a Boolean and never raise.
@@ -87,7 +92,7 @@ def is_currency(currency, locale=None):
return True
-def normalize_currency(currency, locale=None):
+def normalize_currency(currency: str, locale: Locale | str | None = None) -> str | None:
"""Returns the normalized sting of any currency code.
Accepts a ``locale`` parameter for fined-grained validation, working as
@@ -102,7 +107,11 @@ def normalize_currency(currency, locale=None):
return currency
-def get_currency_name(currency, count=None, locale=LC_NUMERIC):
+def get_currency_name(
+ currency: str,
+ count: float | decimal.Decimal | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+) -> str:
"""Return the name used by the locale for the specified currency.
>>> get_currency_name('USD', locale='en_US')
@@ -128,7 +137,7 @@ def get_currency_name(currency, count=None, locale=LC_NUMERIC):
return loc.currencies.get(currency, currency)
-def get_currency_symbol(currency, locale=LC_NUMERIC):
+def get_currency_symbol(currency: str, locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the symbol used by the locale for the specified currency.
>>> get_currency_symbol('USD', locale='en_US')
@@ -140,7 +149,7 @@ def get_currency_symbol(currency, locale=LC_NUMERIC):
return Locale.parse(locale).currency_symbols.get(currency, currency)
-def get_currency_precision(currency):
+def get_currency_precision(currency: str) -> int:
"""Return currency's precision.
Precision is the number of decimals found after the decimal point in the
@@ -154,7 +163,11 @@ def get_currency_precision(currency):
return precisions.get(currency, precisions['DEFAULT'])[0]
-def get_currency_unit_pattern(currency, count=None, locale=LC_NUMERIC):
+def get_currency_unit_pattern(
+ currency: str,
+ count: float | decimal.Decimal | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+) -> str:
"""
Return the unit pattern used for long display of a currency value
for a given locale.
@@ -184,9 +197,38 @@ def get_currency_unit_pattern(currency, count=None, locale=LC_NUMERIC):
return loc._data['currency_unit_patterns']['other']
-def get_territory_currencies(territory, start_date=None, end_date=None,
- tender=True, non_tender=False,
- include_details=False):
+@overload
+def get_territory_currencies(
+ territory: str,
+ start_date: date_ | None = ...,
+ end_date: date_ | None = ...,
+ tender: bool = ...,
+ non_tender: bool = ...,
+ include_details: Literal[False] = ...,
+) -> list[str]:
+ ... # pragma: no cover
+
+
+@overload
+def get_territory_currencies(
+ territory: str,
+ start_date: date_ | None = ...,
+ end_date: date_ | None = ...,
+ tender: bool = ...,
+ non_tender: bool = ...,
+ include_details: Literal[True] = ...,
+) -> list[dict[str, Any]]:
+ ... # pragma: no cover
+
+
+def get_territory_currencies(
+ territory: str,
+ start_date: date_ | None = None,
+ end_date: date_ | None = None,
+ tender: bool = True,
+ non_tender: bool = False,
+ include_details: bool = False,
+) -> list[str] | list[dict[str, Any]]:
"""Returns the list of currencies for the given territory that are valid for
the given date range. In addition to that the currency database
distinguishes between tender and non-tender currencies. By default only
@@ -274,7 +316,7 @@ def get_territory_currencies(territory, start_date=None, end_date=None,
return result
-def get_decimal_symbol(locale=LC_NUMERIC):
+def get_decimal_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the symbol used by the locale to separate decimal fractions.
>>> get_decimal_symbol('en_US')
@@ -285,7 +327,7 @@ def get_decimal_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('decimal', u'.')
-def get_plus_sign_symbol(locale=LC_NUMERIC):
+def get_plus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the plus sign symbol used by the current locale.
>>> get_plus_sign_symbol('en_US')
@@ -296,7 +338,7 @@ def get_plus_sign_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('plusSign', u'+')
-def get_minus_sign_symbol(locale=LC_NUMERIC):
+def get_minus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the plus sign symbol used by the current locale.
>>> get_minus_sign_symbol('en_US')
@@ -307,7 +349,7 @@ def get_minus_sign_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('minusSign', u'-')
-def get_exponential_symbol(locale=LC_NUMERIC):
+def get_exponential_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the symbol used by the locale to separate mantissa and exponent.
>>> get_exponential_symbol('en_US')
@@ -318,7 +360,7 @@ def get_exponential_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('exponential', u'E')
-def get_group_symbol(locale=LC_NUMERIC):
+def get_group_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
"""Return the symbol used by the locale to separate groups of thousands.
>>> get_group_symbol('en_US')
@@ -329,7 +371,7 @@ def get_group_symbol(locale=LC_NUMERIC):
return Locale.parse(locale).number_symbols.get('group', u',')
-def format_number(number, locale=LC_NUMERIC):
+def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = LC_NUMERIC) -> str:
u"""Return the given number formatted for a specific locale.
>>> format_number(1099, locale='en_US') # doctest: +SKIP
@@ -350,7 +392,7 @@ def format_number(number, locale=LC_NUMERIC):
return format_decimal(number, locale=locale)
-def get_decimal_precision(number):
+def get_decimal_precision(number: decimal.Decimal) -> int:
"""Return maximum precision of a decimal instance's fractional part.
Precision is extracted from the fractional part only.
@@ -363,14 +405,19 @@ def get_decimal_precision(number):
return abs(decimal_tuple.exponent)
-def get_decimal_quantum(precision):
+def get_decimal_quantum(precision: int | decimal.Decimal) -> decimal.Decimal:
"""Return minimal quantum of a number, as defined by precision."""
assert isinstance(precision, (int, decimal.Decimal))
return decimal.Decimal(10) ** (-precision)
def format_decimal(
- number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True):
+ number: float | decimal.Decimal | str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ decimal_quantization: bool = True,
+ group_separator: bool = True,
+) -> str:
u"""Return the given decimal number formatted for a specific locale.
>>> format_decimal(1.2345, locale='en_US')
@@ -419,7 +466,13 @@ def format_decimal(
number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)
-def format_compact_decimal(number, *, format_type="short", locale=LC_NUMERIC, fraction_digits=0):
+def format_compact_decimal(
+ number: float | decimal.Decimal | str,
+ *,
+ format_type: Literal["short", "long"] = "short",
+ locale: Locale | str | None = LC_NUMERIC,
+ fraction_digits: int = 0,
+) -> str:
u"""Return the given decimal number formatted for a specific locale in compact form.
>>> format_compact_decimal(12345, format_type="short", locale='en_US')
@@ -450,7 +503,12 @@ def format_compact_decimal(number, *, format_type="short", locale=LC_NUMERIC, fr
return pattern.apply(number, locale, decimal_quantization=False)
-def _get_compact_format(number, compact_format, locale, fraction_digits=0):
+def _get_compact_format(
+ number: float | decimal.Decimal | str,
+ compact_format: LocaleDataDict,
+ locale: Locale | str | None,
+ fraction_digits: int,
+) -> tuple[decimal.Decimal, NumberPattern | None]:
"""Returns the number after dividing by the unit and the format pattern to use.
The algorithm is described here:
https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats.
@@ -488,8 +546,15 @@ class UnknownCurrencyFormatError(KeyError):
def format_currency(
- number, currency, format=None, locale=LC_NUMERIC, currency_digits=True,
- format_type='standard', decimal_quantization=True, group_separator=True):
+ number: float | decimal.Decimal | str,
+ currency: str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ currency_digits: bool = True,
+ format_type: Literal["name", "standard", "accounting"] = "standard",
+ decimal_quantization: bool = True,
+ group_separator: bool = True,
+) -> str:
u"""Return formatted currency value.
>>> format_currency(1099.98, 'USD', locale='en_US')
@@ -596,8 +661,15 @@ def format_currency(
def _format_currency_long_name(
- number, currency, format=None, locale=LC_NUMERIC, currency_digits=True,
- format_type='standard', decimal_quantization=True, group_separator=True):
+ number: float | decimal.Decimal | str,
+ currency: str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ currency_digits: bool = True,
+ format_type: Literal["name", "standard", "accounting"] = "standard",
+ decimal_quantization: bool = True,
+ group_separator: bool = True,
+) -> str:
# Algorithm described here:
# https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies
locale = Locale.parse(locale)
@@ -631,7 +703,14 @@ def _format_currency_long_name(
return unit_pattern.format(number_part, display_name)
-def format_compact_currency(number, currency, *, format_type="short", locale=LC_NUMERIC, fraction_digits=0):
+def format_compact_currency(
+ number: float | decimal.Decimal | str,
+ currency: str,
+ *,
+ format_type: Literal["short"] = "short",
+ locale: Locale | str | None = LC_NUMERIC,
+ fraction_digits: int = 0
+) -> str:
u"""Format a number as a currency value in compact form.
>>> format_compact_currency(12345, 'USD', locale='en_US')
@@ -670,7 +749,12 @@ def format_compact_currency(number, currency, *, format_type="short", locale=LC_
def format_percent(
- number, format=None, locale=LC_NUMERIC, decimal_quantization=True, group_separator=True):
+ number: float | decimal.Decimal | str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ decimal_quantization: bool = True,
+ group_separator: bool = True,
+) -> str:
"""Return formatted percent value for a specific locale.
>>> format_percent(0.34, locale='en_US')
@@ -717,7 +801,11 @@ def format_percent(
def format_scientific(
- number, format=None, locale=LC_NUMERIC, decimal_quantization=True):
+ number: float | decimal.Decimal | str,
+ format: str | None = None,
+ locale: Locale | str | None = LC_NUMERIC,
+ decimal_quantization: bool = True,
+) -> str:
"""Return value formatted in scientific notation for a specific locale.
>>> format_scientific(10000, locale='en_US')
@@ -754,13 +842,13 @@ def format_scientific(
class NumberFormatError(ValueError):
"""Exception raised when a string cannot be parsed into a number."""
- def __init__(self, message, suggestions=None):
+ def __init__(self, message: str, suggestions: str | None = None) -> None:
super().__init__(message)
#: a list of properly formatted numbers derived from the invalid input
self.suggestions = suggestions
-def parse_number(string, locale=LC_NUMERIC):
+def parse_number(string: str, locale: Locale | str | None = LC_NUMERIC) -> int:
"""Parse localized number string into an integer.
>>> parse_number('1,099', locale='en_US')
@@ -786,7 +874,7 @@ def parse_number(string, locale=LC_NUMERIC):
raise NumberFormatError(f"{string!r} is not a valid number")
-def parse_decimal(string, locale=LC_NUMERIC, strict=False):
+def parse_decimal(string: str, locale: Locale | str | None = LC_NUMERIC, strict: bool = False) -> decimal.Decimal:
"""Parse localized decimal string into a decimal.
>>> parse_decimal('1,099.98', locale='en_US')
@@ -879,7 +967,7 @@ SUFFIX_PATTERN = r"(?P<suffix>.*)"
number_re = re.compile(f"{PREFIX_PATTERN}{NUMBER_PATTERN}{SUFFIX_PATTERN}")
-def parse_grouping(p):
+def parse_grouping(p: str) -> tuple[int, int]:
"""Parse primary and secondary digit grouping
>>> parse_grouping('##')
@@ -901,7 +989,7 @@ def parse_grouping(p):
return g1, g2
-def parse_pattern(pattern):
+def parse_pattern(pattern: NumberPattern | str) -> NumberPattern:
"""Parse number format patterns"""
if isinstance(pattern, NumberPattern):
return pattern
@@ -970,9 +1058,18 @@ def parse_pattern(pattern):
class NumberPattern:
- def __init__(self, pattern, prefix, suffix, grouping,
- int_prec, frac_prec, exp_prec, exp_plus,
- number_pattern: str | None = None):
+ def __init__(
+ self,
+ pattern: str,
+ prefix: tuple[str, str],
+ suffix: tuple[str, str],
+ grouping: tuple[int, int],
+ int_prec: tuple[int, int],
+ frac_prec: tuple[int, int],
+ exp_prec: tuple[int, int] | None,
+ exp_plus: bool | None,
+ number_pattern: str | None = None,
+ ) -> None:
# Metadata of the decomposed parsed pattern.
self.pattern = pattern
self.prefix = prefix
@@ -985,10 +1082,10 @@ class NumberPattern:
self.exp_plus = exp_plus
self.scale = self.compute_scale()
- def __repr__(self):
+ def __repr__(self) -> str:
return f"<{type(self).__name__} {self.pattern!r}>"
- def compute_scale(self):
+ def compute_scale(self) -> Literal[0, 2, 3]:
"""Return the scaling factor to apply to the number before rendering.
Auto-set to a factor of 2 or 3 if presence of a ``%`` or ``‰`` sign is
@@ -1002,7 +1099,7 @@ class NumberPattern:
scale = 3
return scale
- def scientific_notation_elements(self, value, locale):
+ def scientific_notation_elements(self, value: decimal.Decimal, locale: Locale | str | None) -> tuple[decimal.Decimal, int, str]:
""" Returns normalized scientific notation components of a value.
"""
# Normalize value to only have one lead digit.
@@ -1031,13 +1128,13 @@ class NumberPattern:
def apply(
self,
- value,
- locale,
- currency=None,
- currency_digits=True,
- decimal_quantization=True,
- force_frac=None,
- group_separator=True,
+ value: float | decimal.Decimal,
+ locale: Locale | str | None,
+ currency: str | None = None,
+ currency_digits: bool = True,
+ decimal_quantization: bool = True,
+ force_frac: tuple[int, int] | None = None,
+ group_separator: bool = True,
):
"""Renders into a string a number following the defined pattern.
@@ -1157,7 +1254,7 @@ class NumberPattern:
# - Restore the original position of the decimal point, potentially
# padding with zeroes on either side
#
- def _format_significant(self, value, minimum, maximum):
+ def _format_significant(self, value: decimal.Decimal, minimum: int, maximum: int) -> str:
exp = value.adjusted()
scale = maximum - 1 - exp
digits = str(value.scaleb(scale).quantize(decimal.Decimal(1)))
@@ -1176,7 +1273,7 @@ class NumberPattern:
).rstrip('.')
return result
- def _format_int(self, value, min, max, locale):
+ def _format_int(self, value: str, min: int, max: int, locale: Locale | str | None) -> str:
width = len(value)
if width < min:
value = '0' * (min - width) + value
@@ -1189,7 +1286,7 @@ class NumberPattern:
gsize = self.grouping[1]
return value + ret
- def _quantize_value(self, value, locale, frac_prec, group_separator):
+ def _quantize_value(self, value: decimal.Decimal, locale: Locale | str | None, frac_prec: tuple[int, int], group_separator: bool) -> str:
quantum = get_decimal_quantum(frac_prec[1])
rounded = value.quantize(quantum)
a, sep, b = f"{rounded:f}".partition(".")
@@ -1199,7 +1296,7 @@ class NumberPattern:
number = integer_part + self._format_frac(b or '0', locale, frac_prec)
return number
- def _format_frac(self, value, locale, force_frac=None):
+ def _format_frac(self, value: str, locale: Locale | str | None, force_frac: tuple[int, int] | None = None) -> str:
min, max = force_frac or self.frac_prec
if len(value) < min:
value += ('0' * (min - len(value)))