diff options
Diffstat (limited to 'babel/numbers.py')
-rw-r--r-- | babel/numbers.py | 205 |
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))) |