| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681 |
- """
- babel.numbers
- ~~~~~~~~~~~~~
- Locale dependent formatting and parsing of numeric data.
- The default locale for the functions in this module is determined by the
- following environment variables, in that order:
- * ``LC_MONETARY`` for currency related functions,
- * ``LC_NUMERIC``, and
- * ``LC_ALL``, and
- * ``LANG``
- :copyright: (c) 2013-2026 by the Babel Team.
- :license: BSD, see LICENSE for more details.
- """
- # TODO:
- # Padding and rounding increments in pattern:
- # - https://www.unicode.org/reports/tr35/ (Appendix G.6)
- from __future__ import annotations
- import datetime
- import decimal
- import re
- import warnings
- from typing import Any, Literal, cast, overload
- from babel.core import Locale, default_locale, get_global
- from babel.localedata import LocaleDataDict
- LC_MONETARY = default_locale(('LC_MONETARY', 'LC_NUMERIC'))
- LC_NUMERIC = default_locale('LC_NUMERIC')
- class UnknownCurrencyError(Exception):
- """Exception thrown when a currency is requested for which no data is available."""
- def __init__(self, identifier: str) -> None:
- """Create the exception.
- :param identifier: the identifier string of the unsupported currency
- """
- Exception.__init__(self, f"Unknown currency {identifier!r}.")
- #: The identifier of the locale that could not be found.
- self.identifier = identifier
- def list_currencies(locale: Locale | str | None = None) -> set[str]:
- """Return a `set` of normalized currency codes.
- .. versionadded:: 2.5.0
- :param locale: filters returned currency codes by the provided locale.
- Expected to be a locale instance or code. If no locale is
- provided, returns the list of all currencies from all
- locales.
- """
- # Get locale-scoped currencies.
- if locale:
- return set(Locale.parse(locale).currencies)
- return set(get_global('all_currencies'))
- 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
- the one defined above in ``list_currencies()`` method.
- Raises a `UnknownCurrencyError` exception if the currency is unknown to Babel.
- """
- if currency not in list_currencies(locale):
- raise UnknownCurrencyError(currency)
- 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.
- """
- if not currency or not isinstance(currency, str):
- return False
- try:
- validate_currency(currency, locale)
- except UnknownCurrencyError:
- return False
- return True
- def normalize_currency(currency: str, locale: Locale | str | None = None) -> str | None:
- """Returns the normalized identifier of any currency code.
- Accepts a ``locale`` parameter for fined-grained validation, working as
- the one defined above in ``list_currencies()`` method.
- Returns None if the currency is unknown to Babel.
- """
- if isinstance(currency, str):
- currency = currency.upper()
- if not is_currency(currency, locale):
- return None
- return currency
- def get_currency_name(
- currency: str,
- count: float | decimal.Decimal | None = None,
- locale: Locale | str | None = None,
- ) -> str:
- """Return the name used by the locale for the specified currency.
- >>> get_currency_name('USD', locale='en_US')
- 'US Dollar'
- .. versionadded:: 0.9.4
- :param currency: the currency code.
- :param count: the optional count. If provided the currency name
- will be pluralized to that number if possible.
- :param locale: the `Locale` object or locale identifier.
- Defaults to the system currency locale or numeric locale.
- """
- loc = Locale.parse(locale or LC_MONETARY)
- if count is not None:
- try:
- plural_form = loc.plural_form(count)
- except (OverflowError, ValueError):
- plural_form = 'other'
- plural_names = loc._data['currency_names_plural']
- if currency in plural_names:
- currency_plural_names = plural_names[currency]
- if plural_form in currency_plural_names:
- return currency_plural_names[plural_form]
- if 'other' in currency_plural_names:
- return currency_plural_names['other']
- return loc.currencies.get(currency, currency)
- def get_currency_symbol(currency: str, locale: Locale | str | None = None) -> str:
- """Return the symbol used by the locale for the specified currency.
- >>> get_currency_symbol('USD', locale='en_US')
- '$'
- :param currency: the currency code.
- :param locale: the `Locale` object or locale identifier.
- Defaults to the system currency locale or numeric locale.
- """
- return Locale.parse(locale or LC_MONETARY).currency_symbols.get(currency, 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
- currency's format pattern.
- .. versionadded:: 2.5.0
- :param currency: the currency code.
- """
- precisions = get_global('currency_fractions')
- return precisions.get(currency, precisions['DEFAULT'])[0]
- def get_currency_unit_pattern(
- currency: str, # TODO: unused?!
- count: float | decimal.Decimal | None = None,
- locale: Locale | str | None = None,
- ) -> str:
- """
- Return the unit pattern used for long display of a currency value
- for a given locale.
- This is a string containing ``{0}`` where the numeric part
- should be substituted and ``{1}`` where the currency long display
- name should be substituted.
- >>> get_currency_unit_pattern('USD', locale='en_US', count=10)
- '{0} {1}'
- .. versionadded:: 2.7.0
- :param currency: the currency code.
- :param count: the optional count. If provided the unit
- pattern for that number will be returned.
- :param locale: the `Locale` object or locale identifier.
- Defaults to the system currency locale or numeric locale.
- """
- loc = Locale.parse(locale or LC_MONETARY)
- if count is not None:
- plural_form = loc.plural_form(count)
- try:
- return loc._data['currency_unit_patterns'][plural_form]
- except LookupError:
- # Fall back to 'other'
- pass
- return loc._data['currency_unit_patterns']['other']
- @overload
- def get_territory_currencies(
- territory: str,
- start_date: datetime.date | None = ...,
- end_date: datetime.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: datetime.date | None = ...,
- end_date: datetime.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: datetime.date | None = None,
- end_date: datetime.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
- tender currencies are returned.
- The return value is a list of all currencies roughly ordered by the time
- of when the currency became active. The longer the currency is being in
- use the more to the left of the list it will be.
- The start date defaults to today. If no end date is given it will be the
- same as the start date. Otherwise a range can be defined. For instance
- this can be used to find the currencies in use in Austria between 1995 and
- 2011:
- >>> from datetime import date
- >>> get_territory_currencies('AT', date(1995, 1, 1), date(2011, 1, 1))
- ['ATS', 'EUR']
- Likewise it's also possible to find all the currencies in use on a
- single date:
- >>> get_territory_currencies('AT', date(1995, 1, 1))
- ['ATS']
- >>> get_territory_currencies('AT', date(2011, 1, 1))
- ['EUR']
- By default the return value only includes tender currencies. This
- however can be changed:
- >>> get_territory_currencies('US')
- ['USD']
- >>> get_territory_currencies('US', tender=False, non_tender=True,
- ... start_date=date(2014, 1, 1))
- ['USN', 'USS']
- .. versionadded:: 2.0
- :param territory: the name of the territory to find the currency for.
- :param start_date: the start date. If not given today is assumed.
- :param end_date: the end date. If not given the start date is assumed.
- :param tender: controls whether tender currencies should be included.
- :param non_tender: controls whether non-tender currencies should be
- included.
- :param include_details: if set to `True`, instead of returning currency
- codes the return value will be dictionaries
- with detail information. In that case each
- dictionary will have the keys ``'currency'``,
- ``'from'``, ``'to'``, and ``'tender'``.
- """
- currencies = get_global('territory_currencies')
- if start_date is None:
- start_date = datetime.date.today()
- elif isinstance(start_date, datetime.datetime):
- start_date = start_date.date()
- if end_date is None:
- end_date = start_date
- elif isinstance(end_date, datetime.datetime):
- end_date = end_date.date()
- curs = currencies.get(territory.upper(), ())
- # TODO: validate that the territory exists
- def _is_active(start, end):
- return (start is None or start <= end_date) and (end is None or end >= start_date)
- result = []
- for currency_code, start, end, is_tender in curs:
- if start:
- start = datetime.date(*start)
- if end:
- end = datetime.date(*end)
- if ((is_tender and tender) or (not is_tender and non_tender)) and _is_active(
- start,
- end,
- ):
- if include_details:
- result.append(
- {
- 'currency': currency_code,
- 'from': start,
- 'to': end,
- 'tender': is_tender,
- },
- )
- else:
- result.append(currency_code)
- return result
- def _get_numbering_system(
- locale: Locale,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- if numbering_system == "default":
- return locale.default_numbering_system
- else:
- return numbering_system
- def _get_number_symbols(
- locale: Locale,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> LocaleDataDict:
- numbering_system = _get_numbering_system(locale, numbering_system)
- try:
- return locale.number_symbols[numbering_system]
- except KeyError as error:
- raise UnsupportedNumberingSystemError(
- f"Unknown numbering system {numbering_system} for Locale {locale}.",
- ) from error
- class UnsupportedNumberingSystemError(Exception):
- """Exception thrown when an unsupported numbering system is requested for the given Locale."""
- pass
- def get_decimal_symbol(
- locale: Locale | str | None = None,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return the symbol used by the locale to separate decimal fractions.
- >>> get_decimal_symbol('en_US')
- '.'
- >>> get_decimal_symbol('ar_EG', numbering_system='default')
- '٫'
- >>> get_decimal_symbol('ar_EG', numbering_system='latn')
- '.'
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- return _get_number_symbols(locale, numbering_system=numbering_system).get('decimal', '.')
- def get_plus_sign_symbol(
- locale: Locale | str | None = None,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return the plus sign symbol used by the current locale.
- >>> get_plus_sign_symbol('en_US')
- '+'
- >>> get_plus_sign_symbol('ar_EG', numbering_system='default')
- '\\u061c+'
- >>> get_plus_sign_symbol('ar_EG', numbering_system='latn')
- '\\u200e+'
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- return _get_number_symbols(locale, numbering_system=numbering_system).get('plusSign', '+')
- def get_minus_sign_symbol(
- locale: Locale | str | None = None,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return the plus sign symbol used by the current locale.
- >>> get_minus_sign_symbol('en_US')
- '-'
- >>> get_minus_sign_symbol('ar_EG', numbering_system='default')
- '\\u061c-'
- >>> get_minus_sign_symbol('ar_EG', numbering_system='latn')
- '\\u200e-'
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- return _get_number_symbols(locale, numbering_system=numbering_system).get('minusSign', '-')
- def get_exponential_symbol(
- locale: Locale | str | None = None,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return the symbol used by the locale to separate mantissa and exponent.
- >>> get_exponential_symbol('en_US')
- 'E'
- >>> get_exponential_symbol('ar_EG', numbering_system='default')
- 'أس'
- >>> get_exponential_symbol('ar_EG', numbering_system='latn')
- 'E'
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- return _get_number_symbols(locale, numbering_system=numbering_system).get('exponential', 'E') # fmt: skip
- def get_group_symbol(
- locale: Locale | str | None = None,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return the symbol used by the locale to separate groups of thousands.
- >>> get_group_symbol('en_US')
- ','
- >>> get_group_symbol('ar_EG', numbering_system='default')
- '٬'
- >>> get_group_symbol('ar_EG', numbering_system='latn')
- ','
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- return _get_number_symbols(locale, numbering_system=numbering_system).get('group', ',')
- def get_infinity_symbol(
- locale: Locale | str | None = None,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return the symbol used by the locale to represent infinity.
- >>> get_infinity_symbol('en_US')
- '∞'
- >>> get_infinity_symbol('ar_EG', numbering_system='default')
- '∞'
- >>> get_infinity_symbol('ar_EG', numbering_system='latn')
- '∞'
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- return _get_number_symbols(locale, numbering_system=numbering_system).get('infinity', '∞')
- def format_number(
- number: float | decimal.Decimal | str,
- locale: Locale | str | None = None,
- ) -> str:
- """Return the given number formatted for a specific locale.
- >>> format_number(1099, locale='en_US') # doctest: +SKIP
- '1,099'
- >>> format_number(1099, locale='de_DE') # doctest: +SKIP
- '1.099'
- .. deprecated:: 2.6.0
- Use babel.numbers.format_decimal() instead.
- :param number: the number to format
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- """
- warnings.warn(
- 'Use babel.numbers.format_decimal() instead.',
- DeprecationWarning,
- stacklevel=2,
- )
- return format_decimal(number, locale=locale)
- 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.
- """
- # Copied from: https://github.com/mahmoud/boltons/pull/59
- assert isinstance(number, decimal.Decimal)
- decimal_tuple = number.normalize().as_tuple()
- # Note: DecimalTuple.exponent can be 'n' (qNaN), 'N' (sNaN), or 'F' (Infinity)
- if not isinstance(decimal_tuple.exponent, int) or decimal_tuple.exponent >= 0:
- return 0
- return abs(decimal_tuple.exponent)
- 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: float | decimal.Decimal | str,
- format: str | NumberPattern | None = None,
- locale: Locale | str | None = None,
- decimal_quantization: bool = True,
- group_separator: bool = True,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return the given decimal number formatted for a specific locale.
- >>> format_decimal(1.2345, locale='en_US')
- '1.234'
- >>> format_decimal(1.2346, locale='en_US')
- '1.235'
- >>> format_decimal(-1.2346, locale='en_US')
- '-1.235'
- >>> format_decimal(1.2345, locale='sv_SE')
- '1,234'
- >>> format_decimal(1.2345, locale='de')
- '1,234'
- >>> format_decimal(1.2345, locale='ar_EG', numbering_system='default')
- '1٫234'
- >>> format_decimal(1.2345, locale='ar_EG', numbering_system='latn')
- '1.234'
- The appropriate thousands grouping and the decimal separator are used for
- each locale:
- >>> format_decimal(12345.5, locale='en_US')
- '12,345.5'
- By default the locale is allowed to truncate and round a high-precision
- number by forcing its format pattern onto the decimal part. You can bypass
- this behavior with the `decimal_quantization` parameter:
- >>> format_decimal(1.2346, locale='en_US')
- '1.235'
- >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False)
- '1.2346'
- >>> format_decimal(12345.67, locale='fr_CA', group_separator=False)
- '12345,67'
- >>> format_decimal(12345.67, locale='en_US', group_separator=True)
- '12,345.67'
- :param number: the number to format
- :param format:
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param decimal_quantization: Truncate and round high-precision numbers to
- the format pattern. Defaults to `True`.
- :param group_separator: Boolean to switch group separator on/off in a locale's
- number format.
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- if format is None:
- format = locale.decimal_formats[format]
- pattern = parse_pattern(format)
- return pattern.apply(
- number,
- locale,
- decimal_quantization=decimal_quantization,
- group_separator=group_separator,
- numbering_system=numbering_system,
- )
- def format_compact_decimal(
- number: float | decimal.Decimal | str,
- *,
- format_type: Literal["short", "long"] = "short",
- locale: Locale | str | None = None,
- fraction_digits: int = 0,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return the given decimal number formatted for a specific locale in compact form.
- >>> format_compact_decimal(12345, format_type="short", locale='en_US')
- '12K'
- >>> format_compact_decimal(12345, format_type="long", locale='en_US')
- '12 thousand'
- >>> format_compact_decimal(12345, format_type="short", locale='en_US', fraction_digits=2)
- '12.34K'
- >>> format_compact_decimal(1234567, format_type="short", locale="ja_JP")
- '123万'
- >>> format_compact_decimal(2345678, format_type="long", locale="mk")
- '2 милиони'
- >>> format_compact_decimal(21000000, format_type="long", locale="mk")
- '21 милион'
- >>> format_compact_decimal(12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='default')
- '12٫34\\xa0ألف'
- :param number: the number to format
- :param format_type: Compact format to use ("short" or "long")
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- compact_format = locale.compact_decimal_formats[format_type]
- number, format = _get_compact_format(number, compact_format, locale, fraction_digits)
- # Did not find a format, fall back.
- if format is None:
- format = locale.decimal_formats[None]
- pattern = parse_pattern(format)
- return pattern.apply(
- number,
- locale,
- decimal_quantization=False,
- numbering_system=numbering_system,
- )
- def _get_compact_format(
- number: float | decimal.Decimal | str,
- compact_format: LocaleDataDict,
- locale: Locale,
- 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.
- """
- if not isinstance(number, decimal.Decimal):
- number = decimal.Decimal(str(number))
- if number.is_nan() or number.is_infinite():
- return number, None
- format = None
- for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True):
- if abs(number) >= magnitude:
- # check the pattern using "other" as the amount
- format = compact_format["other"][str(magnitude)]
- pattern = parse_pattern(format).pattern
- # if the pattern is "0", we do not divide the number
- if pattern == "0":
- break
- # otherwise, we need to divide the number by the magnitude but remove zeros
- # equal to the number of 0's in the pattern minus 1
- number = cast(
- decimal.Decimal,
- number / (magnitude // (10 ** (pattern.count("0") - 1))),
- )
- # round to the number of fraction digits requested
- rounded = round(number, fraction_digits)
- # if the remaining number is singular, use the singular format
- plural_form = locale.plural_form(abs(number))
- if plural_form not in compact_format:
- plural_form = "other"
- if number == 1 and "1" in compact_format:
- plural_form = "1"
- if str(magnitude) not in compact_format[plural_form]:
- plural_form = "other" # fall back to other as the implicit default
- format = compact_format[plural_form][str(magnitude)]
- number = rounded
- break
- return number, format
- class UnknownCurrencyFormatError(KeyError):
- """Exception raised when an unknown currency format is requested."""
- def format_currency(
- number: float | decimal.Decimal | str,
- currency: str,
- format: str | NumberPattern | None = None,
- locale: Locale | str | None = None,
- currency_digits: bool = True,
- format_type: Literal["name", "standard", "accounting"] = "standard",
- decimal_quantization: bool = True,
- group_separator: bool = True,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return formatted currency value.
- >>> format_currency(1099.98, 'USD', locale='en_US')
- '$1,099.98'
- >>> format_currency(1099.98, 'USD', locale='es_CO')
- 'US$1.099,98'
- >>> format_currency(1099.98, 'EUR', locale='de_DE')
- '1.099,98\\xa0\\u20ac'
- >>> format_currency(1099.98, 'EGP', locale='ar_EG', numbering_system='default')
- '\\u200f1٬099٫98\\xa0ج.م.\\u200f'
- The format can also be specified explicitly. The currency is
- placed with the '¤' sign. As the sign gets repeated the format
- expands (¤ being the symbol, ¤¤ is the currency abbreviation and
- ¤¤¤ is the full name of the currency):
- >>> format_currency(1099.98, 'EUR', '\\xa4\\xa4 #,##0.00', locale='en_US')
- 'EUR 1,099.98'
- >>> format_currency(1099.98, 'EUR', '#,##0.00 \\xa4\\xa4\\xa4', locale='en_US')
- '1,099.98 euros'
- Currencies usually have a specific number of decimal digits. This function
- favours that information over the given format:
- >>> format_currency(1099.98, 'JPY', locale='en_US')
- '\\xa51,100'
- >>> format_currency(1099.98, 'COP', '#,##0.00', locale='es_ES')
- '1.099,98'
- However, the number of decimal digits can be overridden from the currency
- information, by setting the last parameter to ``False``:
- >>> format_currency(1099.98, 'JPY', locale='en_US', currency_digits=False)
- '\\xa51,099.98'
- >>> format_currency(1099.98, 'COP', '#,##0.00', locale='es_ES', currency_digits=False)
- '1.099,98'
- If a format is not specified the type of currency format to use
- from the locale can be specified:
- >>> format_currency(1099.98, 'EUR', locale='en_US', format_type='standard')
- '\\u20ac1,099.98'
- When the given currency format type is not available, an exception is
- raised:
- >>> format_currency('1099.98', 'EUR', locale='root', format_type='unknown')
- Traceback (most recent call last):
- ...
- UnknownCurrencyFormatError: "'unknown' is not a known currency format type"
- >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False)
- '$101299.98'
- >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True)
- '$101,299.98'
- You can also pass format_type='name' to use long display names. The order of
- the number and currency name, along with the correct localized plural form
- of the currency name, is chosen according to locale:
- >>> format_currency(1, 'USD', locale='en_US', format_type='name')
- '1.00 US dollar'
- >>> format_currency(1099.98, 'USD', locale='en_US', format_type='name')
- '1,099.98 US dollars'
- >>> format_currency(1099.98, 'USD', locale='ee', format_type='name')
- 'us ga dollar 1,099.98'
- By default the locale is allowed to truncate and round a high-precision
- number by forcing its format pattern onto the decimal part. You can bypass
- this behavior with the `decimal_quantization` parameter:
- >>> format_currency(1099.9876, 'USD', locale='en_US')
- '$1,099.99'
- >>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False)
- '$1,099.9876'
- :param number: the number to format
- :param currency: the currency code
- :param format: the format string to use
- :param locale: the `Locale` object or locale identifier.
- Defaults to the system currency locale or numeric locale.
- :param currency_digits: use the currency's natural number of decimal digits
- :param format_type: the currency format type to use
- :param decimal_quantization: Truncate and round high-precision numbers to
- the format pattern. Defaults to `True`.
- :param group_separator: Boolean to switch group separator on/off in a locale's
- number format.
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_MONETARY)
- if format_type == 'name':
- return _format_currency_long_name(
- number,
- currency,
- locale=locale,
- format=format,
- currency_digits=currency_digits,
- decimal_quantization=decimal_quantization,
- group_separator=group_separator,
- numbering_system=numbering_system,
- )
- if format:
- pattern = parse_pattern(format)
- else:
- try:
- pattern = locale.currency_formats[format_type]
- except KeyError:
- raise UnknownCurrencyFormatError(
- f"{format_type!r} is not a known currency format type",
- ) from None
- return pattern.apply(
- number,
- locale,
- currency=currency,
- currency_digits=currency_digits,
- decimal_quantization=decimal_quantization,
- group_separator=group_separator,
- numbering_system=numbering_system,
- )
- def _format_currency_long_name(
- number: float | decimal.Decimal | str,
- currency: str,
- *,
- locale: Locale,
- format: str | NumberPattern | None,
- currency_digits: bool,
- decimal_quantization: bool,
- group_separator: bool,
- numbering_system: Literal["default"] | str,
- ) -> str:
- # Algorithm described here:
- # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies
- # Step 1.
- # There are no examples of items with explicit count (0 or 1) in current
- # locale data. So there is no point implementing that.
- # Step 2.
- # Correct number to numeric type, important for looking up plural rules:
- number_n = float(number) if isinstance(number, str) else number
- # Step 3.
- unit_pattern = get_currency_unit_pattern(currency, count=number_n, locale=locale)
- # Step 4.
- display_name = get_currency_name(currency, count=number_n, locale=locale)
- # Step 5.
- if not format:
- format = locale.decimal_formats[None]
- pattern = parse_pattern(format)
- number_part = pattern.apply(
- number,
- locale,
- currency=currency,
- currency_digits=currency_digits,
- decimal_quantization=decimal_quantization,
- group_separator=group_separator,
- numbering_system=numbering_system,
- )
- return unit_pattern.format(number_part, display_name)
- def format_compact_currency(
- number: float | decimal.Decimal | str,
- currency: str,
- *,
- format_type: Literal["short"] = "short",
- locale: Locale | str | None = None,
- fraction_digits: int = 0,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Format a number as a currency value in compact form.
- >>> format_compact_currency(12345, 'USD', locale='en_US')
- '$12K'
- >>> format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2)
- '$123.46M'
- >>> format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1)
- '123,5\\xa0Mio.\\xa0€'
- :param number: the number to format
- :param currency: the currency code
- :param format_type: the compact format type to use. Defaults to "short".
- :param locale: the `Locale` object or locale identifier.
- Defaults to the system currency locale or numeric locale.
- :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_MONETARY)
- try:
- compact_format = locale.compact_currency_formats[format_type]
- except KeyError as error:
- raise UnknownCurrencyFormatError(
- f"{format_type!r} is not a known compact currency format type",
- ) from error
- number, format = _get_compact_format(number, compact_format, locale, fraction_digits)
- # Did not find a format, fall back.
- if format is None or "¤" not in str(format):
- # find first format that has a currency symbol
- for magnitude in compact_format['other']:
- format = compact_format['other'][magnitude].pattern
- if '¤' not in format:
- continue
- # remove characters that are not the currency symbol, 0's or spaces
- format = re.sub(r'[^0\s\¤]', '', format)
- # compress adjacent spaces into one
- format = re.sub(r'(\s)\s+', r'\1', format).strip()
- break
- if format is None:
- raise ValueError('No compact currency format found for the given number and locale.')
- pattern = parse_pattern(format)
- return pattern.apply(
- number,
- locale,
- currency=currency,
- currency_digits=False,
- decimal_quantization=False,
- numbering_system=numbering_system,
- )
- def format_percent(
- number: float | decimal.Decimal | str,
- format: str | NumberPattern | None = None,
- locale: Locale | str | None = None,
- decimal_quantization: bool = True,
- group_separator: bool = True,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return formatted percent value for a specific locale.
- >>> format_percent(0.34, locale='en_US')
- '34%'
- >>> format_percent(25.1234, locale='en_US')
- '2,512%'
- >>> format_percent(25.1234, locale='sv_SE')
- '2\\xa0512\\xa0%'
- >>> format_percent(25.1234, locale='ar_EG', numbering_system='default')
- '2٬512%'
- The format pattern can also be specified explicitly:
- >>> format_percent(25.1234, '#,##0\\u2030', locale='en_US')
- '25,123‰'
- By default the locale is allowed to truncate and round a high-precision
- number by forcing its format pattern onto the decimal part. You can bypass
- this behavior with the `decimal_quantization` parameter:
- >>> format_percent(23.9876, locale='en_US')
- '2,399%'
- >>> format_percent(23.9876, locale='en_US', decimal_quantization=False)
- '2,398.76%'
- >>> format_percent(229291.1234, locale='pt_BR', group_separator=False)
- '22929112%'
- >>> format_percent(229291.1234, locale='pt_BR', group_separator=True)
- '22.929.112%'
- :param number: the percent number to format
- :param format:
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param decimal_quantization: Truncate and round high-precision numbers to
- the format pattern. Defaults to `True`.
- :param group_separator: Boolean to switch group separator on/off in a locale's
- number format.
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- if not format:
- format = locale.percent_formats[None]
- pattern = parse_pattern(format)
- return pattern.apply(
- number,
- locale,
- decimal_quantization=decimal_quantization,
- group_separator=group_separator,
- numbering_system=numbering_system,
- )
- def format_scientific(
- number: float | decimal.Decimal | str,
- format: str | NumberPattern | None = None,
- locale: Locale | str | None = None,
- decimal_quantization: bool = True,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> str:
- """Return value formatted in scientific notation for a specific locale.
- >>> format_scientific(10000, locale='en_US')
- '1E4'
- >>> format_scientific(10000, locale='ar_EG', numbering_system='default')
- '1أس4'
- The format pattern can also be specified explicitly:
- >>> format_scientific(1234567, '##0.##E00', locale='en_US')
- '1.23E06'
- By default the locale is allowed to truncate and round a high-precision
- number by forcing its format pattern onto the decimal part. You can bypass
- this behavior with the `decimal_quantization` parameter:
- >>> format_scientific(1234.9876, '#.##E0', locale='en_US')
- '1.23E3'
- >>> format_scientific(1234.9876, '#.##E0', locale='en_US', decimal_quantization=False)
- '1.2349876E3'
- :param number: the number to format
- :param format:
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param decimal_quantization: Truncate and round high-precision numbers to
- the format pattern. Defaults to `True`.
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- if not format:
- format = locale.scientific_formats[None]
- pattern = parse_pattern(format)
- return pattern.apply(
- number,
- locale,
- decimal_quantization=decimal_quantization,
- numbering_system=numbering_system,
- )
- class NumberFormatError(ValueError):
- """Exception raised when a string cannot be parsed into a number."""
- def __init__(self, message: str, suggestions: list[str] | None = None) -> None:
- super().__init__(message)
- #: a list of properly formatted numbers derived from the invalid input
- self.suggestions = suggestions
- SPACE_CHARS = {
- ' ', # space
- '\xa0', # no-break space
- '\u202f', # narrow no-break space
- }
- SPACE_CHARS_RE = re.compile('|'.join(SPACE_CHARS))
- def parse_number(
- string: str,
- locale: Locale | str | None = None,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> int:
- """Parse localized number string into an integer.
- >>> parse_number('1,099', locale='en_US')
- 1099
- >>> parse_number('1.099', locale='de_DE')
- 1099
- When the given string cannot be parsed, an exception is raised:
- >>> parse_number('1.099,98', locale='de')
- Traceback (most recent call last):
- ...
- NumberFormatError: '1.099,98' is not a valid number
- :param string: the string to parse
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :return: the parsed number
- :raise `NumberFormatError`: if the string can not be converted to a number
- :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
- """
- group_symbol = get_group_symbol(locale, numbering_system=numbering_system)
- if (
- # if the grouping symbol is a kind of space,
- group_symbol in SPACE_CHARS
- # and the string to be parsed does not contain it,
- and group_symbol not in string
- # but it does contain any other kind of space instead,
- and SPACE_CHARS_RE.search(string)
- ):
- # ... it's reasonable to assume it is taking the place of the grouping symbol.
- string = SPACE_CHARS_RE.sub(group_symbol, string)
- try:
- return int(string.replace(group_symbol, ''))
- except ValueError as ve:
- raise NumberFormatError(f"{string!r} is not a valid number") from ve
- def parse_decimal(
- string: str,
- locale: Locale | str | None = None,
- strict: bool = False,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> decimal.Decimal:
- """Parse localized decimal string into a decimal.
- >>> parse_decimal('1,099.98', locale='en_US')
- Decimal('1099.98')
- >>> parse_decimal('1.099,98', locale='de')
- Decimal('1099.98')
- >>> parse_decimal('12 345,123', locale='ru')
- Decimal('12345.123')
- >>> parse_decimal('1٬099٫98', locale='ar_EG', numbering_system='default')
- Decimal('1099.98')
- When the given string cannot be parsed, an exception is raised:
- >>> parse_decimal('2,109,998', locale='de')
- Traceback (most recent call last):
- ...
- NumberFormatError: '2,109,998' is not a valid decimal number
- If `strict` is set to `True` and the given string contains a number
- formatted in an irregular way, an exception is raised:
- >>> parse_decimal('30.00', locale='de', strict=True)
- Traceback (most recent call last):
- ...
- NumberFormatError: '30.00' is not a properly formatted decimal number. Did you mean '3.000'? Or maybe '30,00'?
- >>> parse_decimal('0.00', locale='de', strict=True)
- Traceback (most recent call last):
- ...
- NumberFormatError: '0.00' is not a properly formatted decimal number. Did you mean '0'?
- :param string: the string to parse
- :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
- :param strict: controls whether numbers formatted in a weird way are
- accepted or rejected
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :raise NumberFormatError: if the string can not be converted to a
- decimal number
- :raise UnsupportedNumberingSystemError: if the numbering system is not supported by the locale.
- """
- locale = Locale.parse(locale or LC_NUMERIC)
- group_symbol = get_group_symbol(locale, numbering_system=numbering_system)
- decimal_symbol = get_decimal_symbol(locale, numbering_system=numbering_system)
- if not strict and (
- group_symbol in SPACE_CHARS # if the grouping symbol is a kind of space,
- and group_symbol not in string # and the string to be parsed does not contain it,
- # but it does contain any other kind of space instead,
- and SPACE_CHARS_RE.search(string)
- ):
- # ... it's reasonable to assume it is taking the place of the grouping symbol.
- string = SPACE_CHARS_RE.sub(group_symbol, string)
- try:
- parsed = decimal.Decimal(string.replace(group_symbol, '').replace(decimal_symbol, '.'))
- except decimal.InvalidOperation as exc:
- raise NumberFormatError(f"{string!r} is not a valid decimal number") from exc
- if strict and group_symbol in string:
- proper = format_decimal(
- parsed,
- locale=locale,
- decimal_quantization=False,
- numbering_system=numbering_system,
- )
- if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol): # fmt: skip
- try:
- parsed_alt = decimal.Decimal(
- string.replace(decimal_symbol, '').replace(group_symbol, '.'),
- )
- except decimal.InvalidOperation as exc:
- raise NumberFormatError(
- f"{string!r} is not a properly formatted decimal number. "
- f"Did you mean {proper!r}?",
- suggestions=[proper],
- ) from exc
- else:
- proper_alt = format_decimal(
- parsed_alt,
- locale=locale,
- decimal_quantization=False,
- numbering_system=numbering_system,
- )
- if proper_alt == proper:
- raise NumberFormatError(
- f"{string!r} is not a properly formatted decimal number. "
- f"Did you mean {proper!r}?",
- suggestions=[proper],
- )
- else:
- raise NumberFormatError(
- f"{string!r} is not a properly formatted decimal number. "
- f"Did you mean {proper!r}? Or maybe {proper_alt!r}?",
- suggestions=[proper, proper_alt],
- )
- return parsed
- def _remove_trailing_zeros_after_decimal(string: str, decimal_symbol: str) -> str:
- """
- Remove trailing zeros from the decimal part of a numeric string.
- This function takes a string representing a numeric value and a decimal symbol.
- It removes any trailing zeros that appear after the decimal symbol in the number.
- If the decimal part becomes empty after removing trailing zeros, the decimal symbol
- is also removed. If the string does not contain the decimal symbol, it is returned unchanged.
- :param string: The numeric string from which to remove trailing zeros.
- :type string: str
- :param decimal_symbol: The symbol used to denote the decimal point.
- :type decimal_symbol: str
- :return: The numeric string with trailing zeros removed from its decimal part.
- :rtype: str
- Example:
- >>> _remove_trailing_zeros_after_decimal("123.4500", ".")
- '123.45'
- >>> _remove_trailing_zeros_after_decimal("100.000", ".")
- '100'
- >>> _remove_trailing_zeros_after_decimal("100", ".")
- '100'
- """
- integer_part, _, decimal_part = string.partition(decimal_symbol)
- if decimal_part:
- decimal_part = decimal_part.rstrip("0")
- if decimal_part:
- return integer_part + decimal_symbol + decimal_part
- return integer_part
- return string
- _number_pattern_re = re.compile(
- r"(?P<prefix>(?:[^'0-9@#.,]|'[^']*')*)"
- r"(?P<number>[0-9@#.,E+]*)"
- r"(?P<suffix>.*)",
- )
- def parse_grouping(p: str) -> tuple[int, int]:
- """Parse primary and secondary digit grouping
- >>> parse_grouping('##')
- (1000, 1000)
- >>> parse_grouping('#,###')
- (3, 3)
- >>> parse_grouping('#,####,###')
- (3, 4)
- """
- width = len(p)
- g1 = p.rfind(',')
- if g1 == -1:
- return 1000, 1000
- g1 = width - g1 - 1
- g2 = p[: -g1 - 1].rfind(',')
- if g2 == -1:
- return g1, g1
- g2 = width - g1 - g2 - 2
- return g1, g2
- def parse_pattern(pattern: NumberPattern | str) -> NumberPattern:
- """Parse number format patterns"""
- if isinstance(pattern, NumberPattern):
- return pattern
- def _match_number(pattern):
- rv = _number_pattern_re.search(pattern)
- if rv is None:
- raise ValueError(f"Invalid number pattern {pattern!r}")
- return rv.groups()
- pos_pattern = pattern
- # Do we have a negative subpattern?
- if ';' in pattern:
- pos_pattern, neg_pattern = pattern.split(';', 1)
- pos_prefix, number, pos_suffix = _match_number(pos_pattern)
- neg_prefix, _, neg_suffix = _match_number(neg_pattern)
- else:
- pos_prefix, number, pos_suffix = _match_number(pos_pattern)
- neg_prefix = f"-{pos_prefix}"
- neg_suffix = pos_suffix
- if 'E' in number:
- number, exp = number.split('E', 1)
- else:
- exp = None
- if '@' in number and '.' in number and '0' in number:
- raise ValueError('Significant digit patterns can not contain "@" or "0"')
- if '.' in number:
- integer, fraction = number.rsplit('.', 1)
- else:
- integer = number
- fraction = ''
- def parse_precision(p):
- """Calculate the min and max allowed digits"""
- min = max = 0
- for c in p:
- if c in '@0':
- min += 1
- max += 1
- elif c == '#':
- max += 1
- elif c == ',':
- continue
- else:
- break
- return min, max
- int_prec = parse_precision(integer)
- frac_prec = parse_precision(fraction)
- if exp:
- exp_plus = exp.startswith('+')
- exp = exp.lstrip('+')
- exp_prec = parse_precision(exp)
- else:
- exp_plus = None
- exp_prec = None
- grouping = parse_grouping(integer)
- return NumberPattern(
- pattern,
- (pos_prefix, neg_prefix),
- (pos_suffix, neg_suffix),
- grouping,
- int_prec,
- frac_prec,
- exp_prec,
- exp_plus,
- number,
- )
- class NumberPattern:
- 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
- self.suffix = suffix
- self.number_pattern = number_pattern
- self.grouping = grouping
- self.int_prec = int_prec
- self.frac_prec = frac_prec
- self.exp_prec = exp_prec
- self.exp_plus = exp_plus
- self.scale = self.compute_scale()
- def __repr__(self) -> str:
- return f"<{type(self).__name__} {self.pattern!r}>"
- 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
- detected in the prefix or suffix of the pattern. Default is to not mess
- with the scale at all and keep it to 0.
- """
- scale = 0
- if '%' in ''.join(self.prefix + self.suffix):
- scale = 2
- elif '‰' in ''.join(self.prefix + self.suffix):
- scale = 3
- return scale
- def scientific_notation_elements(
- self,
- value: decimal.Decimal,
- locale: Locale | str | None,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ) -> tuple[decimal.Decimal, int, str]:
- """Returns normalized scientific notation components of a value."""
- # Normalize value to only have one lead digit.
- exp = value.adjusted()
- value = value * get_decimal_quantum(exp)
- assert value.adjusted() == 0
- # Shift exponent and value by the minimum number of leading digits
- # imposed by the rendering pattern. And always make that number
- # greater or equal to 1.
- lead_shift = max([1, min(self.int_prec)]) - 1
- exp = exp - lead_shift
- value = value * get_decimal_quantum(-lead_shift)
- # Get exponent sign symbol.
- exp_sign = ''
- if exp < 0:
- exp_sign = get_minus_sign_symbol(locale, numbering_system=numbering_system)
- elif self.exp_plus:
- exp_sign = get_plus_sign_symbol(locale, numbering_system=numbering_system)
- # Normalize exponent value now that we have the sign.
- exp = abs(exp)
- return value, exp, exp_sign
- def apply(
- self,
- value: float | decimal.Decimal | str,
- 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,
- *,
- numbering_system: Literal["default"] | str = "latn",
- ):
- """Renders into a string a number following the defined pattern.
- Forced decimal quantization is active by default so we'll produce a
- number string that is strictly following CLDR pattern definitions.
- :param value: The value to format. If this is not a Decimal object,
- it will be cast to one.
- :type value: decimal.Decimal|float|int
- :param locale: The locale to use for formatting.
- :type locale: str|babel.core.Locale
- :param currency: Which currency, if any, to format as.
- :type currency: str|None
- :param currency_digits: Whether or not to use the currency's precision.
- If false, the pattern's precision is used.
- :type currency_digits: bool
- :param decimal_quantization: Whether decimal numbers should be forcibly
- quantized to produce a formatted output
- strictly matching the CLDR definition for
- the locale.
- :type decimal_quantization: bool
- :param force_frac: DEPRECATED - a forced override for `self.frac_prec`
- for a single formatting invocation.
- :param group_separator: Whether to use the locale's number group separator.
- :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
- The special value "default" will use the default numbering system of the locale.
- :return: Formatted decimal string.
- :rtype: str
- :raise UnsupportedNumberingSystemError: If the numbering system is not supported by the locale.
- """
- if not isinstance(value, decimal.Decimal):
- value = decimal.Decimal(str(value))
- value = value.scaleb(self.scale)
- # Separate the absolute value from its sign.
- is_negative = int(value.is_signed())
- value = abs(value).normalize()
- # Prepare scientific notation metadata.
- if self.exp_prec:
- value, exp, exp_sign = self.scientific_notation_elements(
- value,
- locale,
- numbering_system=numbering_system,
- )
- # Adjust the precision of the fractional part and force it to the
- # currency's if necessary.
- if force_frac:
- # TODO (3.x?): Remove this parameter
- warnings.warn(
- 'The force_frac parameter to NumberPattern.apply() is deprecated.',
- DeprecationWarning,
- stacklevel=2,
- )
- frac_prec = force_frac
- elif currency and currency_digits:
- frac_prec = (get_currency_precision(currency),) * 2
- else:
- frac_prec = self.frac_prec
- # Bump decimal precision to the natural precision of the number if it
- # exceeds the one we're about to use. This adaptative precision is only
- # triggered if the decimal quantization is disabled or if a scientific
- # notation pattern has a missing mandatory fractional part (as in the
- # default '#E0' pattern). This special case has been extensively
- # discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 .
- if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)):
- frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)]))
- # Render scientific notation.
- if self.exp_prec:
- number = ''.join([
- self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system),
- get_exponential_symbol(locale, numbering_system=numbering_system),
- exp_sign, # type: ignore # exp_sign is always defined here
- self._format_int(str(exp), self.exp_prec[0], self.exp_prec[1], locale, numbering_system=numbering_system), # type: ignore # exp is always defined here
- ]) # fmt: skip
- # Is it a significant digits pattern?
- elif '@' in self.pattern:
- text = self._format_significant(value, self.int_prec[0], self.int_prec[1])
- a, sep, b = text.partition(".")
- number = self._format_int(a, 0, 1000, locale, numbering_system=numbering_system)
- if sep:
- number += get_decimal_symbol(locale, numbering_system=numbering_system) + b
- # A normal number pattern.
- else:
- number = self._quantize_value(
- value,
- locale,
- frac_prec,
- group_separator,
- numbering_system=numbering_system,
- )
- retval = ''.join(
- (
- self.prefix[is_negative],
- number if self.number_pattern != '' else '',
- self.suffix[is_negative],
- ),
- )
- if '¤' in retval and currency is not None:
- retval = retval.replace('¤¤¤', get_currency_name(currency, value, locale))
- retval = retval.replace('¤¤', currency.upper())
- retval = retval.replace('¤', get_currency_symbol(currency, locale))
- # remove single quotes around text, except for doubled single quotes
- # which are replaced with a single quote
- retval = re.sub(r"'([^']*)'", lambda m: m.group(1) or "'", retval)
- return retval
- #
- # This is one tricky piece of code. The idea is to rely as much as possible
- # on the decimal module to minimize the amount of code.
- #
- # Conceptually, the implementation of this method can be summarized in the
- # following steps:
- #
- # - Move or shift the decimal point (i.e. the exponent) so the maximum
- # amount of significant digits fall into the integer part (i.e. to the
- # left of the decimal point)
- #
- # - Round the number to the nearest integer, discarding all the fractional
- # part which contained extra digits to be eliminated
- #
- # - Convert the rounded integer to a string, that will contain the final
- # sequence of significant digits already trimmed to the maximum
- #
- # - Restore the original position of the decimal point, potentially
- # padding with zeroes on either side
- #
- 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)))
- if scale <= 0:
- result = digits + '0' * -scale
- else:
- intpart = digits[:-scale]
- i = len(intpart)
- j = i + max(minimum - i, 0)
- result = "{intpart}.{pad:0<{fill}}{fracpart}{fracextra}".format(
- intpart=intpart or '0',
- pad='',
- fill=-min(exp + 1, 0),
- fracpart=digits[i:j],
- fracextra=digits[j:].rstrip('0'),
- ).rstrip('.')
- return result
- def _format_int(
- self,
- value: str,
- min: int,
- max: int,
- locale: Locale | str | None,
- *,
- numbering_system: Literal["default"] | str,
- ) -> str:
- width = len(value)
- if width < min:
- value = '0' * (min - width) + value
- gsize = self.grouping[0]
- ret = ''
- symbol = get_group_symbol(locale, numbering_system=numbering_system)
- while len(value) > gsize:
- ret = symbol + value[-gsize:] + ret
- value = value[:-gsize]
- gsize = self.grouping[1]
- return value + ret
- def _quantize_value(
- self,
- value: decimal.Decimal,
- locale: Locale | str | None,
- frac_prec: tuple[int, int],
- group_separator: bool,
- *,
- numbering_system: Literal["default"] | str,
- ) -> str:
- # If the number is +/-Infinity, we can't quantize it
- if value.is_infinite():
- return get_infinity_symbol(locale, numbering_system=numbering_system)
- quantum = get_decimal_quantum(frac_prec[1])
- rounded = value.quantize(quantum)
- a, sep, b = f"{rounded:f}".partition(".")
- integer_part = a
- if group_separator:
- integer_part = self._format_int(
- a,
- self.int_prec[0],
- self.int_prec[1],
- locale,
- numbering_system=numbering_system,
- )
- number = integer_part + self._format_frac(
- b or '0',
- locale=locale,
- force_frac=frac_prec,
- numbering_system=numbering_system,
- )
- return number
- def _format_frac(
- self,
- value: str,
- locale: Locale | str | None,
- force_frac: tuple[int, int] | None = None,
- *,
- numbering_system: Literal["default"] | str,
- ) -> str:
- min, max = force_frac or self.frac_prec
- if len(value) < min:
- value += '0' * (min - len(value))
- if max == 0 or (min == 0 and int(value) == 0):
- return ''
- while len(value) > min and value[-1] == '0':
- value = value[:-1]
- return get_decimal_symbol(locale, numbering_system=numbering_system) + value
|