Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/babel/numbers.py: 17%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2 babel.numbers
3 ~~~~~~~~~~~~~
5 Locale dependent formatting and parsing of numeric data.
7 The default locale for the functions in this module is determined by the
8 following environment variables, in that order:
10 * ``LC_MONETARY`` for currency related functions,
11 * ``LC_NUMERIC``, and
12 * ``LC_ALL``, and
13 * ``LANG``
15 :copyright: (c) 2013-2025 by the Babel Team.
16 :license: BSD, see LICENSE for more details.
17"""
18# TODO:
19# Padding and rounding increments in pattern:
20# - https://www.unicode.org/reports/tr35/ (Appendix G.6)
21from __future__ import annotations
23import datetime
24import decimal
25import re
26import warnings
27from typing import Any, Literal, cast, overload
29from babel.core import Locale, default_locale, get_global
30from babel.localedata import LocaleDataDict
32LC_MONETARY = default_locale(('LC_MONETARY', 'LC_NUMERIC'))
33LC_NUMERIC = default_locale('LC_NUMERIC')
36class UnknownCurrencyError(Exception):
37 """Exception thrown when a currency is requested for which no data is available.
38 """
40 def __init__(self, identifier: str) -> None:
41 """Create the exception.
42 :param identifier: the identifier string of the unsupported currency
43 """
44 Exception.__init__(self, f"Unknown currency {identifier!r}.")
46 #: The identifier of the locale that could not be found.
47 self.identifier = identifier
50def list_currencies(locale: Locale | str | None = None) -> set[str]:
51 """ Return a `set` of normalized currency codes.
53 .. versionadded:: 2.5.0
55 :param locale: filters returned currency codes by the provided locale.
56 Expected to be a locale instance or code. If no locale is
57 provided, returns the list of all currencies from all
58 locales.
59 """
60 # Get locale-scoped currencies.
61 if locale:
62 return set(Locale.parse(locale).currencies)
63 return set(get_global('all_currencies'))
66def validate_currency(currency: str, locale: Locale | str | None = None) -> None:
67 """ Check the currency code is recognized by Babel.
69 Accepts a ``locale`` parameter for fined-grained validation, working as
70 the one defined above in ``list_currencies()`` method.
72 Raises a `UnknownCurrencyError` exception if the currency is unknown to Babel.
73 """
74 if currency not in list_currencies(locale):
75 raise UnknownCurrencyError(currency)
78def is_currency(currency: str, locale: Locale | str | None = None) -> bool:
79 """ Returns `True` only if a currency is recognized by Babel.
81 This method always return a Boolean and never raise.
82 """
83 if not currency or not isinstance(currency, str):
84 return False
85 try:
86 validate_currency(currency, locale)
87 except UnknownCurrencyError:
88 return False
89 return True
92def normalize_currency(currency: str, locale: Locale | str | None = None) -> str | None:
93 """Returns the normalized identifier of any currency code.
95 Accepts a ``locale`` parameter for fined-grained validation, working as
96 the one defined above in ``list_currencies()`` method.
98 Returns None if the currency is unknown to Babel.
99 """
100 if isinstance(currency, str):
101 currency = currency.upper()
102 if not is_currency(currency, locale):
103 return None
104 return currency
107def get_currency_name(
108 currency: str,
109 count: float | decimal.Decimal | None = None,
110 locale: Locale | str | None = None,
111) -> str:
112 """Return the name used by the locale for the specified currency.
114 >>> get_currency_name('USD', locale='en_US')
115 u'US Dollar'
117 .. versionadded:: 0.9.4
119 :param currency: the currency code.
120 :param count: the optional count. If provided the currency name
121 will be pluralized to that number if possible.
122 :param locale: the `Locale` object or locale identifier.
123 Defaults to the system currency locale or numeric locale.
124 """
125 loc = Locale.parse(locale or LC_MONETARY)
126 if count is not None:
127 try:
128 plural_form = loc.plural_form(count)
129 except (OverflowError, ValueError):
130 plural_form = 'other'
131 plural_names = loc._data['currency_names_plural']
132 if currency in plural_names:
133 currency_plural_names = plural_names[currency]
134 if plural_form in currency_plural_names:
135 return currency_plural_names[plural_form]
136 if 'other' in currency_plural_names:
137 return currency_plural_names['other']
138 return loc.currencies.get(currency, currency)
141def get_currency_symbol(currency: str, locale: Locale | str | None = None) -> str:
142 """Return the symbol used by the locale for the specified currency.
144 >>> get_currency_symbol('USD', locale='en_US')
145 u'$'
147 :param currency: the currency code.
148 :param locale: the `Locale` object or locale identifier.
149 Defaults to the system currency locale or numeric locale.
150 """
151 return Locale.parse(locale or LC_MONETARY).currency_symbols.get(currency, currency)
154def get_currency_precision(currency: str) -> int:
155 """Return currency's precision.
157 Precision is the number of decimals found after the decimal point in the
158 currency's format pattern.
160 .. versionadded:: 2.5.0
162 :param currency: the currency code.
163 """
164 precisions = get_global('currency_fractions')
165 return precisions.get(currency, precisions['DEFAULT'])[0]
168def get_currency_unit_pattern(
169 currency: str, # TODO: unused?!
170 count: float | decimal.Decimal | None = None,
171 locale: Locale | str | None = None,
172) -> str:
173 """
174 Return the unit pattern used for long display of a currency value
175 for a given locale.
176 This is a string containing ``{0}`` where the numeric part
177 should be substituted and ``{1}`` where the currency long display
178 name should be substituted.
180 >>> get_currency_unit_pattern('USD', locale='en_US', count=10)
181 u'{0} {1}'
183 .. versionadded:: 2.7.0
185 :param currency: the currency code.
186 :param count: the optional count. If provided the unit
187 pattern for that number will be returned.
188 :param locale: the `Locale` object or locale identifier.
189 Defaults to the system currency locale or numeric locale.
190 """
191 loc = Locale.parse(locale or LC_MONETARY)
192 if count is not None:
193 plural_form = loc.plural_form(count)
194 try:
195 return loc._data['currency_unit_patterns'][plural_form]
196 except LookupError:
197 # Fall back to 'other'
198 pass
200 return loc._data['currency_unit_patterns']['other']
203@overload
204def get_territory_currencies(
205 territory: str,
206 start_date: datetime.date | None = ...,
207 end_date: datetime.date | None = ...,
208 tender: bool = ...,
209 non_tender: bool = ...,
210 include_details: Literal[False] = ...,
211) -> list[str]:
212 ... # pragma: no cover
215@overload
216def get_territory_currencies(
217 territory: str,
218 start_date: datetime.date | None = ...,
219 end_date: datetime.date | None = ...,
220 tender: bool = ...,
221 non_tender: bool = ...,
222 include_details: Literal[True] = ...,
223) -> list[dict[str, Any]]:
224 ... # pragma: no cover
227def get_territory_currencies(
228 territory: str,
229 start_date: datetime.date | None = None,
230 end_date: datetime.date | None = None,
231 tender: bool = True,
232 non_tender: bool = False,
233 include_details: bool = False,
234) -> list[str] | list[dict[str, Any]]:
235 """Returns the list of currencies for the given territory that are valid for
236 the given date range. In addition to that the currency database
237 distinguishes between tender and non-tender currencies. By default only
238 tender currencies are returned.
240 The return value is a list of all currencies roughly ordered by the time
241 of when the currency became active. The longer the currency is being in
242 use the more to the left of the list it will be.
244 The start date defaults to today. If no end date is given it will be the
245 same as the start date. Otherwise a range can be defined. For instance
246 this can be used to find the currencies in use in Austria between 1995 and
247 2011:
249 >>> from datetime import date
250 >>> get_territory_currencies('AT', date(1995, 1, 1), date(2011, 1, 1))
251 ['ATS', 'EUR']
253 Likewise it's also possible to find all the currencies in use on a
254 single date:
256 >>> get_territory_currencies('AT', date(1995, 1, 1))
257 ['ATS']
258 >>> get_territory_currencies('AT', date(2011, 1, 1))
259 ['EUR']
261 By default the return value only includes tender currencies. This
262 however can be changed:
264 >>> get_territory_currencies('US')
265 ['USD']
266 >>> get_territory_currencies('US', tender=False, non_tender=True,
267 ... start_date=date(2014, 1, 1))
268 ['USN', 'USS']
270 .. versionadded:: 2.0
272 :param territory: the name of the territory to find the currency for.
273 :param start_date: the start date. If not given today is assumed.
274 :param end_date: the end date. If not given the start date is assumed.
275 :param tender: controls whether tender currencies should be included.
276 :param non_tender: controls whether non-tender currencies should be
277 included.
278 :param include_details: if set to `True`, instead of returning currency
279 codes the return value will be dictionaries
280 with detail information. In that case each
281 dictionary will have the keys ``'currency'``,
282 ``'from'``, ``'to'``, and ``'tender'``.
283 """
284 currencies = get_global('territory_currencies')
285 if start_date is None:
286 start_date = datetime.date.today()
287 elif isinstance(start_date, datetime.datetime):
288 start_date = start_date.date()
289 if end_date is None:
290 end_date = start_date
291 elif isinstance(end_date, datetime.datetime):
292 end_date = end_date.date()
294 curs = currencies.get(territory.upper(), ())
295 # TODO: validate that the territory exists
297 def _is_active(start, end):
298 return (start is None or start <= end_date) and \
299 (end is None or end >= start_date)
301 result = []
302 for currency_code, start, end, is_tender in curs:
303 if start:
304 start = datetime.date(*start)
305 if end:
306 end = datetime.date(*end)
307 if ((is_tender and tender) or
308 (not is_tender and non_tender)) and _is_active(start, end):
309 if include_details:
310 result.append({
311 'currency': currency_code,
312 'from': start,
313 'to': end,
314 'tender': is_tender,
315 })
316 else:
317 result.append(currency_code)
319 return result
322def _get_numbering_system(locale: Locale, numbering_system: Literal["default"] | str = "latn") -> str:
323 if numbering_system == "default":
324 return locale.default_numbering_system
325 else:
326 return numbering_system
329def _get_number_symbols(
330 locale: Locale,
331 *,
332 numbering_system: Literal["default"] | str = "latn",
333) -> LocaleDataDict:
334 numbering_system = _get_numbering_system(locale, numbering_system)
335 try:
336 return locale.number_symbols[numbering_system]
337 except KeyError as error:
338 raise UnsupportedNumberingSystemError(f"Unknown numbering system {numbering_system} for Locale {locale}.") from error
341class UnsupportedNumberingSystemError(Exception):
342 """Exception thrown when an unsupported numbering system is requested for the given Locale."""
343 pass
346def get_decimal_symbol(
347 locale: Locale | str | None = None,
348 *,
349 numbering_system: Literal["default"] | str = "latn",
350) -> str:
351 """Return the symbol used by the locale to separate decimal fractions.
353 >>> get_decimal_symbol('en_US')
354 u'.'
355 >>> get_decimal_symbol('ar_EG', numbering_system='default')
356 u'٫'
357 >>> get_decimal_symbol('ar_EG', numbering_system='latn')
358 u'.'
360 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
361 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
362 The special value "default" will use the default numbering system of the locale.
363 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
364 """
365 locale = Locale.parse(locale or LC_NUMERIC)
366 return _get_number_symbols(locale, numbering_system=numbering_system).get('decimal', '.')
369def get_plus_sign_symbol(
370 locale: Locale | str | None = None,
371 *,
372 numbering_system: Literal["default"] | str = "latn",
373) -> str:
374 """Return the plus sign symbol used by the current locale.
376 >>> get_plus_sign_symbol('en_US')
377 u'+'
378 >>> get_plus_sign_symbol('ar_EG', numbering_system='default')
379 u'\u061c+'
380 >>> get_plus_sign_symbol('ar_EG', numbering_system='latn')
381 u'\u200e+'
383 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
384 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
385 The special value "default" will use the default numbering system of the locale.
386 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
387 """
388 locale = Locale.parse(locale or LC_NUMERIC)
389 return _get_number_symbols(locale, numbering_system=numbering_system).get('plusSign', '+')
392def get_minus_sign_symbol(
393 locale: Locale | str | None = None,
394 *,
395 numbering_system: Literal["default"] | str = "latn",
396) -> str:
397 """Return the plus sign symbol used by the current locale.
399 >>> get_minus_sign_symbol('en_US')
400 u'-'
401 >>> get_minus_sign_symbol('ar_EG', numbering_system='default')
402 u'\u061c-'
403 >>> get_minus_sign_symbol('ar_EG', numbering_system='latn')
404 u'\u200e-'
406 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
407 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
408 The special value "default" will use the default numbering system of the locale.
409 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
410 """
411 locale = Locale.parse(locale or LC_NUMERIC)
412 return _get_number_symbols(locale, numbering_system=numbering_system).get('minusSign', '-')
415def get_exponential_symbol(
416 locale: Locale | str | None = None,
417 *,
418 numbering_system: Literal["default"] | str = "latn",
419) -> str:
420 """Return the symbol used by the locale to separate mantissa and exponent.
422 >>> get_exponential_symbol('en_US')
423 u'E'
424 >>> get_exponential_symbol('ar_EG', numbering_system='default')
425 u'أس'
426 >>> get_exponential_symbol('ar_EG', numbering_system='latn')
427 u'E'
429 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
430 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
431 The special value "default" will use the default numbering system of the locale.
432 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
433 """
434 locale = Locale.parse(locale or LC_NUMERIC)
435 return _get_number_symbols(locale, numbering_system=numbering_system).get('exponential', 'E')
438def get_group_symbol(
439 locale: Locale | str | None = None,
440 *,
441 numbering_system: Literal["default"] | str = "latn",
442) -> str:
443 """Return the symbol used by the locale to separate groups of thousands.
445 >>> get_group_symbol('en_US')
446 u','
447 >>> get_group_symbol('ar_EG', numbering_system='default')
448 u'٬'
449 >>> get_group_symbol('ar_EG', numbering_system='latn')
450 u','
452 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
453 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
454 The special value "default" will use the default numbering system of the locale.
455 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
456 """
457 locale = Locale.parse(locale or LC_NUMERIC)
458 return _get_number_symbols(locale, numbering_system=numbering_system).get('group', ',')
461def get_infinity_symbol(
462 locale: Locale | str | None = None,
463 *,
464 numbering_system: Literal["default"] | str = "latn",
465) -> str:
466 """Return the symbol used by the locale to represent infinity.
468 >>> get_infinity_symbol('en_US')
469 u'∞'
470 >>> get_infinity_symbol('ar_EG', numbering_system='default')
471 u'∞'
472 >>> get_infinity_symbol('ar_EG', numbering_system='latn')
473 u'∞'
475 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
476 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
477 The special value "default" will use the default numbering system of the locale.
478 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
479 """
480 locale = Locale.parse(locale or LC_NUMERIC)
481 return _get_number_symbols(locale, numbering_system=numbering_system).get('infinity', '∞')
484def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = None) -> str:
485 """Return the given number formatted for a specific locale.
487 >>> format_number(1099, locale='en_US') # doctest: +SKIP
488 u'1,099'
489 >>> format_number(1099, locale='de_DE') # doctest: +SKIP
490 u'1.099'
492 .. deprecated:: 2.6.0
494 Use babel.numbers.format_decimal() instead.
496 :param number: the number to format
497 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
500 """
501 warnings.warn('Use babel.numbers.format_decimal() instead.', DeprecationWarning, stacklevel=2)
502 return format_decimal(number, locale=locale)
505def get_decimal_precision(number: decimal.Decimal) -> int:
506 """Return maximum precision of a decimal instance's fractional part.
508 Precision is extracted from the fractional part only.
509 """
510 # Copied from: https://github.com/mahmoud/boltons/pull/59
511 assert isinstance(number, decimal.Decimal)
512 decimal_tuple = number.normalize().as_tuple()
513 # Note: DecimalTuple.exponent can be 'n' (qNaN), 'N' (sNaN), or 'F' (Infinity)
514 if not isinstance(decimal_tuple.exponent, int) or decimal_tuple.exponent >= 0:
515 return 0
516 return abs(decimal_tuple.exponent)
519def get_decimal_quantum(precision: int | decimal.Decimal) -> decimal.Decimal:
520 """Return minimal quantum of a number, as defined by precision."""
521 assert isinstance(precision, (int, decimal.Decimal))
522 return decimal.Decimal(10) ** (-precision)
525def format_decimal(
526 number: float | decimal.Decimal | str,
527 format: str | NumberPattern | None = None,
528 locale: Locale | str | None = None,
529 decimal_quantization: bool = True,
530 group_separator: bool = True,
531 *,
532 numbering_system: Literal["default"] | str = "latn",
533) -> str:
534 """Return the given decimal number formatted for a specific locale.
536 >>> format_decimal(1.2345, locale='en_US')
537 u'1.234'
538 >>> format_decimal(1.2346, locale='en_US')
539 u'1.235'
540 >>> format_decimal(-1.2346, locale='en_US')
541 u'-1.235'
542 >>> format_decimal(1.2345, locale='sv_SE')
543 u'1,234'
544 >>> format_decimal(1.2345, locale='de')
545 u'1,234'
546 >>> format_decimal(1.2345, locale='ar_EG', numbering_system='default')
547 u'1٫234'
548 >>> format_decimal(1.2345, locale='ar_EG', numbering_system='latn')
549 u'1.234'
551 The appropriate thousands grouping and the decimal separator are used for
552 each locale:
554 >>> format_decimal(12345.5, locale='en_US')
555 u'12,345.5'
557 By default the locale is allowed to truncate and round a high-precision
558 number by forcing its format pattern onto the decimal part. You can bypass
559 this behavior with the `decimal_quantization` parameter:
561 >>> format_decimal(1.2346, locale='en_US')
562 u'1.235'
563 >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False)
564 u'1.2346'
565 >>> format_decimal(12345.67, locale='fr_CA', group_separator=False)
566 u'12345,67'
567 >>> format_decimal(12345.67, locale='en_US', group_separator=True)
568 u'12,345.67'
570 :param number: the number to format
571 :param format:
572 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
573 :param decimal_quantization: Truncate and round high-precision numbers to
574 the format pattern. Defaults to `True`.
575 :param group_separator: Boolean to switch group separator on/off in a locale's
576 number format.
577 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
578 The special value "default" will use the default numbering system of the locale.
579 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
580 """
581 locale = Locale.parse(locale or LC_NUMERIC)
582 if format is None:
583 format = locale.decimal_formats[format]
584 pattern = parse_pattern(format)
585 return pattern.apply(
586 number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system)
589def format_compact_decimal(
590 number: float | decimal.Decimal | str,
591 *,
592 format_type: Literal["short", "long"] = "short",
593 locale: Locale | str | None = None,
594 fraction_digits: int = 0,
595 numbering_system: Literal["default"] | str = "latn",
596) -> str:
597 """Return the given decimal number formatted for a specific locale in compact form.
599 >>> format_compact_decimal(12345, format_type="short", locale='en_US')
600 u'12K'
601 >>> format_compact_decimal(12345, format_type="long", locale='en_US')
602 u'12 thousand'
603 >>> format_compact_decimal(12345, format_type="short", locale='en_US', fraction_digits=2)
604 u'12.34K'
605 >>> format_compact_decimal(1234567, format_type="short", locale="ja_JP")
606 u'123万'
607 >>> format_compact_decimal(2345678, format_type="long", locale="mk")
608 u'2 милиони'
609 >>> format_compact_decimal(21000000, format_type="long", locale="mk")
610 u'21 милион'
611 >>> format_compact_decimal(12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='default')
612 u'12٫34\xa0ألف'
614 :param number: the number to format
615 :param format_type: Compact format to use ("short" or "long")
616 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
617 :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
618 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
619 The special value "default" will use the default numbering system of the locale.
620 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
621 """
622 locale = Locale.parse(locale or LC_NUMERIC)
623 compact_format = locale.compact_decimal_formats[format_type]
624 number, format = _get_compact_format(number, compact_format, locale, fraction_digits)
625 # Did not find a format, fall back.
626 if format is None:
627 format = locale.decimal_formats[None]
628 pattern = parse_pattern(format)
629 return pattern.apply(number, locale, decimal_quantization=False, numbering_system=numbering_system)
632def _get_compact_format(
633 number: float | decimal.Decimal | str,
634 compact_format: LocaleDataDict,
635 locale: Locale,
636 fraction_digits: int,
637) -> tuple[decimal.Decimal, NumberPattern | None]:
638 """Returns the number after dividing by the unit and the format pattern to use.
639 The algorithm is described here:
640 https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats.
641 """
642 if not isinstance(number, decimal.Decimal):
643 number = decimal.Decimal(str(number))
644 if number.is_nan() or number.is_infinite():
645 return number, None
646 format = None
647 for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True):
648 if abs(number) >= magnitude:
649 # check the pattern using "other" as the amount
650 format = compact_format["other"][str(magnitude)]
651 pattern = parse_pattern(format).pattern
652 # if the pattern is "0", we do not divide the number
653 if pattern == "0":
654 break
655 # otherwise, we need to divide the number by the magnitude but remove zeros
656 # equal to the number of 0's in the pattern minus 1
657 number = cast(decimal.Decimal, number / (magnitude // (10 ** (pattern.count("0") - 1))))
658 # round to the number of fraction digits requested
659 rounded = round(number, fraction_digits)
660 # if the remaining number is singular, use the singular format
661 plural_form = locale.plural_form(abs(number))
662 if plural_form not in compact_format:
663 plural_form = "other"
664 if number == 1 and "1" in compact_format:
665 plural_form = "1"
666 format = compact_format[plural_form][str(magnitude)]
667 number = rounded
668 break
669 return number, format
672class UnknownCurrencyFormatError(KeyError):
673 """Exception raised when an unknown currency format is requested."""
676def format_currency(
677 number: float | decimal.Decimal | str,
678 currency: str,
679 format: str | NumberPattern | None = None,
680 locale: Locale | str | None = None,
681 currency_digits: bool = True,
682 format_type: Literal["name", "standard", "accounting"] = "standard",
683 decimal_quantization: bool = True,
684 group_separator: bool = True,
685 *,
686 numbering_system: Literal["default"] | str = "latn",
687) -> str:
688 """Return formatted currency value.
690 >>> format_currency(1099.98, 'USD', locale='en_US')
691 '$1,099.98'
692 >>> format_currency(1099.98, 'USD', locale='es_CO')
693 u'US$1.099,98'
694 >>> format_currency(1099.98, 'EUR', locale='de_DE')
695 u'1.099,98\\xa0\\u20ac'
696 >>> format_currency(1099.98, 'EGP', locale='ar_EG', numbering_system='default')
697 u'\u200f1٬099٫98\xa0ج.م.\u200f'
699 The format can also be specified explicitly. The currency is
700 placed with the '¤' sign. As the sign gets repeated the format
701 expands (¤ being the symbol, ¤¤ is the currency abbreviation and
702 ¤¤¤ is the full name of the currency):
704 >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US')
705 u'EUR 1,099.98'
706 >>> format_currency(1099.98, 'EUR', u'#,##0.00 \xa4\xa4\xa4', locale='en_US')
707 u'1,099.98 euros'
709 Currencies usually have a specific number of decimal digits. This function
710 favours that information over the given format:
712 >>> format_currency(1099.98, 'JPY', locale='en_US')
713 u'\\xa51,100'
714 >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES')
715 u'1.099,98'
717 However, the number of decimal digits can be overridden from the currency
718 information, by setting the last parameter to ``False``:
720 >>> format_currency(1099.98, 'JPY', locale='en_US', currency_digits=False)
721 u'\\xa51,099.98'
722 >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES', currency_digits=False)
723 u'1.099,98'
725 If a format is not specified the type of currency format to use
726 from the locale can be specified:
728 >>> format_currency(1099.98, 'EUR', locale='en_US', format_type='standard')
729 u'\\u20ac1,099.98'
731 When the given currency format type is not available, an exception is
732 raised:
734 >>> format_currency('1099.98', 'EUR', locale='root', format_type='unknown')
735 Traceback (most recent call last):
736 ...
737 UnknownCurrencyFormatError: "'unknown' is not a known currency format type"
739 >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False)
740 u'$101299.98'
742 >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True)
743 u'$101,299.98'
745 You can also pass format_type='name' to use long display names. The order of
746 the number and currency name, along with the correct localized plural form
747 of the currency name, is chosen according to locale:
749 >>> format_currency(1, 'USD', locale='en_US', format_type='name')
750 u'1.00 US dollar'
751 >>> format_currency(1099.98, 'USD', locale='en_US', format_type='name')
752 u'1,099.98 US dollars'
753 >>> format_currency(1099.98, 'USD', locale='ee', format_type='name')
754 u'us ga dollar 1,099.98'
756 By default the locale is allowed to truncate and round a high-precision
757 number by forcing its format pattern onto the decimal part. You can bypass
758 this behavior with the `decimal_quantization` parameter:
760 >>> format_currency(1099.9876, 'USD', locale='en_US')
761 u'$1,099.99'
762 >>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False)
763 u'$1,099.9876'
765 :param number: the number to format
766 :param currency: the currency code
767 :param format: the format string to use
768 :param locale: the `Locale` object or locale identifier.
769 Defaults to the system currency locale or numeric locale.
770 :param currency_digits: use the currency's natural number of decimal digits
771 :param format_type: the currency format type to use
772 :param decimal_quantization: Truncate and round high-precision numbers to
773 the format pattern. Defaults to `True`.
774 :param group_separator: Boolean to switch group separator on/off in a locale's
775 number format.
776 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
777 The special value "default" will use the default numbering system of the locale.
778 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
779 """
780 locale = Locale.parse(locale or LC_MONETARY)
782 if format_type == 'name':
783 return _format_currency_long_name(
784 number,
785 currency,
786 locale=locale,
787 format=format,
788 currency_digits=currency_digits,
789 decimal_quantization=decimal_quantization,
790 group_separator=group_separator,
791 numbering_system=numbering_system,
792 )
794 if format:
795 pattern = parse_pattern(format)
796 else:
797 try:
798 pattern = locale.currency_formats[format_type]
799 except KeyError:
800 raise UnknownCurrencyFormatError(f"{format_type!r} is not a known currency format type") from None
802 return pattern.apply(
803 number, locale, currency=currency, currency_digits=currency_digits,
804 decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system)
807def _format_currency_long_name(
808 number: float | decimal.Decimal | str,
809 currency: str,
810 *,
811 locale: Locale,
812 format: str | NumberPattern | None,
813 currency_digits: bool,
814 decimal_quantization: bool,
815 group_separator: bool,
816 numbering_system: Literal["default"] | str,
817) -> str:
818 # Algorithm described here:
819 # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies
821 # Step 1.
822 # There are no examples of items with explicit count (0 or 1) in current
823 # locale data. So there is no point implementing that.
824 # Step 2.
826 # Correct number to numeric type, important for looking up plural rules:
827 number_n = float(number) if isinstance(number, str) else number
829 # Step 3.
830 unit_pattern = get_currency_unit_pattern(currency, count=number_n, locale=locale)
832 # Step 4.
833 display_name = get_currency_name(currency, count=number_n, locale=locale)
835 # Step 5.
836 if not format:
837 format = locale.decimal_formats[None]
839 pattern = parse_pattern(format)
841 number_part = pattern.apply(
842 number, locale, currency=currency, currency_digits=currency_digits,
843 decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system)
845 return unit_pattern.format(number_part, display_name)
848def format_compact_currency(
849 number: float | decimal.Decimal | str,
850 currency: str,
851 *,
852 format_type: Literal["short"] = "short",
853 locale: Locale | str | None = None,
854 fraction_digits: int = 0,
855 numbering_system: Literal["default"] | str = "latn",
856) -> str:
857 """Format a number as a currency value in compact form.
859 >>> format_compact_currency(12345, 'USD', locale='en_US')
860 u'$12K'
861 >>> format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2)
862 u'$123.46M'
863 >>> format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1)
864 '123,5\xa0Mio.\xa0€'
866 :param number: the number to format
867 :param currency: the currency code
868 :param format_type: the compact format type to use. Defaults to "short".
869 :param locale: the `Locale` object or locale identifier.
870 Defaults to the system currency locale or numeric locale.
871 :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
872 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
873 The special value "default" will use the default numbering system of the locale.
874 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
875 """
876 locale = Locale.parse(locale or LC_MONETARY)
877 try:
878 compact_format = locale.compact_currency_formats[format_type]
879 except KeyError as error:
880 raise UnknownCurrencyFormatError(f"{format_type!r} is not a known compact currency format type") from error
881 number, format = _get_compact_format(number, compact_format, locale, fraction_digits)
882 # Did not find a format, fall back.
883 if format is None or "¤" not in str(format):
884 # find first format that has a currency symbol
885 for magnitude in compact_format['other']:
886 format = compact_format['other'][magnitude].pattern
887 if '¤' not in format:
888 continue
889 # remove characters that are not the currency symbol, 0's or spaces
890 format = re.sub(r'[^0\s\¤]', '', format)
891 # compress adjacent spaces into one
892 format = re.sub(r'(\s)\s+', r'\1', format).strip()
893 break
894 if format is None:
895 raise ValueError('No compact currency format found for the given number and locale.')
896 pattern = parse_pattern(format)
897 return pattern.apply(number, locale, currency=currency, currency_digits=False, decimal_quantization=False,
898 numbering_system=numbering_system)
901def format_percent(
902 number: float | decimal.Decimal | str,
903 format: str | NumberPattern | None = None,
904 locale: Locale | str | None = None,
905 decimal_quantization: bool = True,
906 group_separator: bool = True,
907 *,
908 numbering_system: Literal["default"] | str = "latn",
909) -> str:
910 """Return formatted percent value for a specific locale.
912 >>> format_percent(0.34, locale='en_US')
913 u'34%'
914 >>> format_percent(25.1234, locale='en_US')
915 u'2,512%'
916 >>> format_percent(25.1234, locale='sv_SE')
917 u'2\\xa0512\\xa0%'
918 >>> format_percent(25.1234, locale='ar_EG', numbering_system='default')
919 u'2٬512%'
921 The format pattern can also be specified explicitly:
923 >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US')
924 u'25,123\u2030'
926 By default the locale is allowed to truncate and round a high-precision
927 number by forcing its format pattern onto the decimal part. You can bypass
928 this behavior with the `decimal_quantization` parameter:
930 >>> format_percent(23.9876, locale='en_US')
931 u'2,399%'
932 >>> format_percent(23.9876, locale='en_US', decimal_quantization=False)
933 u'2,398.76%'
935 >>> format_percent(229291.1234, locale='pt_BR', group_separator=False)
936 u'22929112%'
938 >>> format_percent(229291.1234, locale='pt_BR', group_separator=True)
939 u'22.929.112%'
941 :param number: the percent number to format
942 :param format:
943 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
944 :param decimal_quantization: Truncate and round high-precision numbers to
945 the format pattern. Defaults to `True`.
946 :param group_separator: Boolean to switch group separator on/off in a locale's
947 number format.
948 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
949 The special value "default" will use the default numbering system of the locale.
950 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
951 """
952 locale = Locale.parse(locale or LC_NUMERIC)
953 if not format:
954 format = locale.percent_formats[None]
955 pattern = parse_pattern(format)
956 return pattern.apply(
957 number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator,
958 numbering_system=numbering_system,
959 )
962def format_scientific(
963 number: float | decimal.Decimal | str,
964 format: str | NumberPattern | None = None,
965 locale: Locale | str | None = None,
966 decimal_quantization: bool = True,
967 *,
968 numbering_system: Literal["default"] | str = "latn",
969) -> str:
970 """Return value formatted in scientific notation for a specific locale.
972 >>> format_scientific(10000, locale='en_US')
973 u'1E4'
974 >>> format_scientific(10000, locale='ar_EG', numbering_system='default')
975 u'1أس4'
977 The format pattern can also be specified explicitly:
979 >>> format_scientific(1234567, u'##0.##E00', locale='en_US')
980 u'1.23E06'
982 By default the locale is allowed to truncate and round a high-precision
983 number by forcing its format pattern onto the decimal part. You can bypass
984 this behavior with the `decimal_quantization` parameter:
986 >>> format_scientific(1234.9876, u'#.##E0', locale='en_US')
987 u'1.23E3'
988 >>> format_scientific(1234.9876, u'#.##E0', locale='en_US', decimal_quantization=False)
989 u'1.2349876E3'
991 :param number: the number to format
992 :param format:
993 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
994 :param decimal_quantization: Truncate and round high-precision numbers to
995 the format pattern. Defaults to `True`.
996 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
997 The special value "default" will use the default numbering system of the locale.
998 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
999 """
1000 locale = Locale.parse(locale or LC_NUMERIC)
1001 if not format:
1002 format = locale.scientific_formats[None]
1003 pattern = parse_pattern(format)
1004 return pattern.apply(
1005 number, locale, decimal_quantization=decimal_quantization, numbering_system=numbering_system)
1008class NumberFormatError(ValueError):
1009 """Exception raised when a string cannot be parsed into a number."""
1011 def __init__(self, message: str, suggestions: list[str] | None = None) -> None:
1012 super().__init__(message)
1013 #: a list of properly formatted numbers derived from the invalid input
1014 self.suggestions = suggestions
1017SPACE_CHARS = {
1018 ' ', # space
1019 '\xa0', # no-break space
1020 '\u202f', # narrow no-break space
1021}
1023SPACE_CHARS_RE = re.compile('|'.join(SPACE_CHARS))
1026def parse_number(
1027 string: str,
1028 locale: Locale | str | None = None,
1029 *,
1030 numbering_system: Literal["default"] | str = "latn",
1031) -> int:
1032 """Parse localized number string into an integer.
1034 >>> parse_number('1,099', locale='en_US')
1035 1099
1036 >>> parse_number('1.099', locale='de_DE')
1037 1099
1039 When the given string cannot be parsed, an exception is raised:
1041 >>> parse_number('1.099,98', locale='de')
1042 Traceback (most recent call last):
1043 ...
1044 NumberFormatError: '1.099,98' is not a valid number
1046 :param string: the string to parse
1047 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
1048 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
1049 The special value "default" will use the default numbering system of the locale.
1050 :return: the parsed number
1051 :raise `NumberFormatError`: if the string can not be converted to a number
1052 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
1053 """
1054 group_symbol = get_group_symbol(locale, numbering_system=numbering_system)
1056 if (
1057 group_symbol in SPACE_CHARS and # if the grouping symbol is a kind of space,
1058 group_symbol not in string and # and the string to be parsed does not contain it,
1059 SPACE_CHARS_RE.search(string) # but it does contain any other kind of space instead,
1060 ):
1061 # ... it's reasonable to assume it is taking the place of the grouping symbol.
1062 string = SPACE_CHARS_RE.sub(group_symbol, string)
1064 try:
1065 return int(string.replace(group_symbol, ''))
1066 except ValueError as ve:
1067 raise NumberFormatError(f"{string!r} is not a valid number") from ve
1070def parse_decimal(
1071 string: str,
1072 locale: Locale | str | None = None,
1073 strict: bool = False,
1074 *,
1075 numbering_system: Literal["default"] | str = "latn",
1076) -> decimal.Decimal:
1077 """Parse localized decimal string into a decimal.
1079 >>> parse_decimal('1,099.98', locale='en_US')
1080 Decimal('1099.98')
1081 >>> parse_decimal('1.099,98', locale='de')
1082 Decimal('1099.98')
1083 >>> parse_decimal('12 345,123', locale='ru')
1084 Decimal('12345.123')
1085 >>> parse_decimal('1٬099٫98', locale='ar_EG', numbering_system='default')
1086 Decimal('1099.98')
1088 When the given string cannot be parsed, an exception is raised:
1090 >>> parse_decimal('2,109,998', locale='de')
1091 Traceback (most recent call last):
1092 ...
1093 NumberFormatError: '2,109,998' is not a valid decimal number
1095 If `strict` is set to `True` and the given string contains a number
1096 formatted in an irregular way, an exception is raised:
1098 >>> parse_decimal('30.00', locale='de', strict=True)
1099 Traceback (most recent call last):
1100 ...
1101 NumberFormatError: '30.00' is not a properly formatted decimal number. Did you mean '3.000'? Or maybe '30,00'?
1103 >>> parse_decimal('0.00', locale='de', strict=True)
1104 Traceback (most recent call last):
1105 ...
1106 NumberFormatError: '0.00' is not a properly formatted decimal number. Did you mean '0'?
1108 :param string: the string to parse
1109 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
1110 :param strict: controls whether numbers formatted in a weird way are
1111 accepted or rejected
1112 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
1113 The special value "default" will use the default numbering system of the locale.
1114 :raise NumberFormatError: if the string can not be converted to a
1115 decimal number
1116 :raise UnsupportedNumberingSystemError: if the numbering system is not supported by the locale.
1117 """
1118 locale = Locale.parse(locale or LC_NUMERIC)
1119 group_symbol = get_group_symbol(locale, numbering_system=numbering_system)
1120 decimal_symbol = get_decimal_symbol(locale, numbering_system=numbering_system)
1122 if not strict and (
1123 group_symbol in SPACE_CHARS and # if the grouping symbol is a kind of space,
1124 group_symbol not in string and # and the string to be parsed does not contain it,
1125 SPACE_CHARS_RE.search(string) # but it does contain any other kind of space instead,
1126 ):
1127 # ... it's reasonable to assume it is taking the place of the grouping symbol.
1128 string = SPACE_CHARS_RE.sub(group_symbol, string)
1130 try:
1131 parsed = decimal.Decimal(string.replace(group_symbol, '')
1132 .replace(decimal_symbol, '.'))
1133 except decimal.InvalidOperation as exc:
1134 raise NumberFormatError(f"{string!r} is not a valid decimal number") from exc
1135 if strict and group_symbol in string:
1136 proper = format_decimal(parsed, locale=locale, decimal_quantization=False, numbering_system=numbering_system)
1137 if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol):
1138 try:
1139 parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '')
1140 .replace(group_symbol, '.'))
1141 except decimal.InvalidOperation as exc:
1142 raise NumberFormatError(
1143 f"{string!r} is not a properly formatted decimal number. "
1144 f"Did you mean {proper!r}?",
1145 suggestions=[proper],
1146 ) from exc
1147 else:
1148 proper_alt = format_decimal(
1149 parsed_alt,
1150 locale=locale,
1151 decimal_quantization=False,
1152 numbering_system=numbering_system,
1153 )
1154 if proper_alt == proper:
1155 raise NumberFormatError(
1156 f"{string!r} is not a properly formatted decimal number. "
1157 f"Did you mean {proper!r}?",
1158 suggestions=[proper],
1159 )
1160 else:
1161 raise NumberFormatError(
1162 f"{string!r} is not a properly formatted decimal number. "
1163 f"Did you mean {proper!r}? Or maybe {proper_alt!r}?",
1164 suggestions=[proper, proper_alt],
1165 )
1166 return parsed
1169def _remove_trailing_zeros_after_decimal(string: str, decimal_symbol: str) -> str:
1170 """
1171 Remove trailing zeros from the decimal part of a numeric string.
1173 This function takes a string representing a numeric value and a decimal symbol.
1174 It removes any trailing zeros that appear after the decimal symbol in the number.
1175 If the decimal part becomes empty after removing trailing zeros, the decimal symbol
1176 is also removed. If the string does not contain the decimal symbol, it is returned unchanged.
1178 :param string: The numeric string from which to remove trailing zeros.
1179 :type string: str
1180 :param decimal_symbol: The symbol used to denote the decimal point.
1181 :type decimal_symbol: str
1182 :return: The numeric string with trailing zeros removed from its decimal part.
1183 :rtype: str
1185 Example:
1186 >>> _remove_trailing_zeros_after_decimal("123.4500", ".")
1187 '123.45'
1188 >>> _remove_trailing_zeros_after_decimal("100.000", ".")
1189 '100'
1190 >>> _remove_trailing_zeros_after_decimal("100", ".")
1191 '100'
1192 """
1193 integer_part, _, decimal_part = string.partition(decimal_symbol)
1195 if decimal_part:
1196 decimal_part = decimal_part.rstrip("0")
1197 if decimal_part:
1198 return integer_part + decimal_symbol + decimal_part
1199 return integer_part
1201 return string
1204PREFIX_END = r'[^0-9@#.,]'
1205NUMBER_TOKEN = r'[0-9@#.,E+]'
1207PREFIX_PATTERN = r"(?P<prefix>(?:'[^']*'|%s)*)" % PREFIX_END
1208NUMBER_PATTERN = r"(?P<number>%s*)" % NUMBER_TOKEN
1209SUFFIX_PATTERN = r"(?P<suffix>.*)"
1211number_re = re.compile(f"{PREFIX_PATTERN}{NUMBER_PATTERN}{SUFFIX_PATTERN}")
1214def parse_grouping(p: str) -> tuple[int, int]:
1215 """Parse primary and secondary digit grouping
1217 >>> parse_grouping('##')
1218 (1000, 1000)
1219 >>> parse_grouping('#,###')
1220 (3, 3)
1221 >>> parse_grouping('#,####,###')
1222 (3, 4)
1223 """
1224 width = len(p)
1225 g1 = p.rfind(',')
1226 if g1 == -1:
1227 return 1000, 1000
1228 g1 = width - g1 - 1
1229 g2 = p[:-g1 - 1].rfind(',')
1230 if g2 == -1:
1231 return g1, g1
1232 g2 = width - g1 - g2 - 2
1233 return g1, g2
1236def parse_pattern(pattern: NumberPattern | str) -> NumberPattern:
1237 """Parse number format patterns"""
1238 if isinstance(pattern, NumberPattern):
1239 return pattern
1241 def _match_number(pattern):
1242 rv = number_re.search(pattern)
1243 if rv is None:
1244 raise ValueError(f"Invalid number pattern {pattern!r}")
1245 return rv.groups()
1247 pos_pattern = pattern
1249 # Do we have a negative subpattern?
1250 if ';' in pattern:
1251 pos_pattern, neg_pattern = pattern.split(';', 1)
1252 pos_prefix, number, pos_suffix = _match_number(pos_pattern)
1253 neg_prefix, _, neg_suffix = _match_number(neg_pattern)
1254 else:
1255 pos_prefix, number, pos_suffix = _match_number(pos_pattern)
1256 neg_prefix = f"-{pos_prefix}"
1257 neg_suffix = pos_suffix
1258 if 'E' in number:
1259 number, exp = number.split('E', 1)
1260 else:
1261 exp = None
1262 if '@' in number and '.' in number and '0' in number:
1263 raise ValueError('Significant digit patterns can not contain "@" or "0"')
1264 if '.' in number:
1265 integer, fraction = number.rsplit('.', 1)
1266 else:
1267 integer = number
1268 fraction = ''
1270 def parse_precision(p):
1271 """Calculate the min and max allowed digits"""
1272 min = max = 0
1273 for c in p:
1274 if c in '@0':
1275 min += 1
1276 max += 1
1277 elif c == '#':
1278 max += 1
1279 elif c == ',':
1280 continue
1281 else:
1282 break
1283 return min, max
1285 int_prec = parse_precision(integer)
1286 frac_prec = parse_precision(fraction)
1287 if exp:
1288 exp_plus = exp.startswith('+')
1289 exp = exp.lstrip('+')
1290 exp_prec = parse_precision(exp)
1291 else:
1292 exp_plus = None
1293 exp_prec = None
1294 grouping = parse_grouping(integer)
1295 return NumberPattern(pattern, (pos_prefix, neg_prefix),
1296 (pos_suffix, neg_suffix), grouping,
1297 int_prec, frac_prec,
1298 exp_prec, exp_plus, number)
1301class NumberPattern:
1303 def __init__(
1304 self,
1305 pattern: str,
1306 prefix: tuple[str, str],
1307 suffix: tuple[str, str],
1308 grouping: tuple[int, int],
1309 int_prec: tuple[int, int],
1310 frac_prec: tuple[int, int],
1311 exp_prec: tuple[int, int] | None,
1312 exp_plus: bool | None,
1313 number_pattern: str | None = None,
1314 ) -> None:
1315 # Metadata of the decomposed parsed pattern.
1316 self.pattern = pattern
1317 self.prefix = prefix
1318 self.suffix = suffix
1319 self.number_pattern = number_pattern
1320 self.grouping = grouping
1321 self.int_prec = int_prec
1322 self.frac_prec = frac_prec
1323 self.exp_prec = exp_prec
1324 self.exp_plus = exp_plus
1325 self.scale = self.compute_scale()
1327 def __repr__(self) -> str:
1328 return f"<{type(self).__name__} {self.pattern!r}>"
1330 def compute_scale(self) -> Literal[0, 2, 3]:
1331 """Return the scaling factor to apply to the number before rendering.
1333 Auto-set to a factor of 2 or 3 if presence of a ``%`` or ``‰`` sign is
1334 detected in the prefix or suffix of the pattern. Default is to not mess
1335 with the scale at all and keep it to 0.
1336 """
1337 scale = 0
1338 if '%' in ''.join(self.prefix + self.suffix):
1339 scale = 2
1340 elif '‰' in ''.join(self.prefix + self.suffix):
1341 scale = 3
1342 return scale
1344 def scientific_notation_elements(
1345 self,
1346 value: decimal.Decimal,
1347 locale: Locale | str | None,
1348 *,
1349 numbering_system: Literal["default"] | str = "latn",
1350 ) -> tuple[decimal.Decimal, int, str]:
1351 """ Returns normalized scientific notation components of a value.
1352 """
1353 # Normalize value to only have one lead digit.
1354 exp = value.adjusted()
1355 value = value * get_decimal_quantum(exp)
1356 assert value.adjusted() == 0
1358 # Shift exponent and value by the minimum number of leading digits
1359 # imposed by the rendering pattern. And always make that number
1360 # greater or equal to 1.
1361 lead_shift = max([1, min(self.int_prec)]) - 1
1362 exp = exp - lead_shift
1363 value = value * get_decimal_quantum(-lead_shift)
1365 # Get exponent sign symbol.
1366 exp_sign = ''
1367 if exp < 0:
1368 exp_sign = get_minus_sign_symbol(locale, numbering_system=numbering_system)
1369 elif self.exp_plus:
1370 exp_sign = get_plus_sign_symbol(locale, numbering_system=numbering_system)
1372 # Normalize exponent value now that we have the sign.
1373 exp = abs(exp)
1375 return value, exp, exp_sign
1377 def apply(
1378 self,
1379 value: float | decimal.Decimal | str,
1380 locale: Locale | str | None,
1381 currency: str | None = None,
1382 currency_digits: bool = True,
1383 decimal_quantization: bool = True,
1384 force_frac: tuple[int, int] | None = None,
1385 group_separator: bool = True,
1386 *,
1387 numbering_system: Literal["default"] | str = "latn",
1388 ):
1389 """Renders into a string a number following the defined pattern.
1391 Forced decimal quantization is active by default so we'll produce a
1392 number string that is strictly following CLDR pattern definitions.
1394 :param value: The value to format. If this is not a Decimal object,
1395 it will be cast to one.
1396 :type value: decimal.Decimal|float|int
1397 :param locale: The locale to use for formatting.
1398 :type locale: str|babel.core.Locale
1399 :param currency: Which currency, if any, to format as.
1400 :type currency: str|None
1401 :param currency_digits: Whether or not to use the currency's precision.
1402 If false, the pattern's precision is used.
1403 :type currency_digits: bool
1404 :param decimal_quantization: Whether decimal numbers should be forcibly
1405 quantized to produce a formatted output
1406 strictly matching the CLDR definition for
1407 the locale.
1408 :type decimal_quantization: bool
1409 :param force_frac: DEPRECATED - a forced override for `self.frac_prec`
1410 for a single formatting invocation.
1411 :param group_separator: Whether to use the locale's number group separator.
1412 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
1413 The special value "default" will use the default numbering system of the locale.
1414 :return: Formatted decimal string.
1415 :rtype: str
1416 :raise UnsupportedNumberingSystemError: If the numbering system is not supported by the locale.
1417 """
1418 if not isinstance(value, decimal.Decimal):
1419 value = decimal.Decimal(str(value))
1421 value = value.scaleb(self.scale)
1423 # Separate the absolute value from its sign.
1424 is_negative = int(value.is_signed())
1425 value = abs(value).normalize()
1427 # Prepare scientific notation metadata.
1428 if self.exp_prec:
1429 value, exp, exp_sign = self.scientific_notation_elements(value, locale, numbering_system=numbering_system)
1431 # Adjust the precision of the fractional part and force it to the
1432 # currency's if necessary.
1433 if force_frac:
1434 # TODO (3.x?): Remove this parameter
1435 warnings.warn(
1436 'The force_frac parameter to NumberPattern.apply() is deprecated.',
1437 DeprecationWarning,
1438 stacklevel=2,
1439 )
1440 frac_prec = force_frac
1441 elif currency and currency_digits:
1442 frac_prec = (get_currency_precision(currency), ) * 2
1443 else:
1444 frac_prec = self.frac_prec
1446 # Bump decimal precision to the natural precision of the number if it
1447 # exceeds the one we're about to use. This adaptative precision is only
1448 # triggered if the decimal quantization is disabled or if a scientific
1449 # notation pattern has a missing mandatory fractional part (as in the
1450 # default '#E0' pattern). This special case has been extensively
1451 # discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 .
1452 if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)):
1453 frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)]))
1455 # Render scientific notation.
1456 if self.exp_prec:
1457 number = ''.join([
1458 self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system),
1459 get_exponential_symbol(locale, numbering_system=numbering_system),
1460 exp_sign, # type: ignore # exp_sign is always defined here
1461 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
1462 ])
1464 # Is it a significant digits pattern?
1465 elif '@' in self.pattern:
1466 text = self._format_significant(value,
1467 self.int_prec[0],
1468 self.int_prec[1])
1469 a, sep, b = text.partition(".")
1470 number = self._format_int(a, 0, 1000, locale, numbering_system=numbering_system)
1471 if sep:
1472 number += get_decimal_symbol(locale, numbering_system=numbering_system) + b
1474 # A normal number pattern.
1475 else:
1476 number = self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system)
1478 retval = ''.join([
1479 self.prefix[is_negative],
1480 number if self.number_pattern != '' else '',
1481 self.suffix[is_negative]])
1483 if '¤' in retval and currency is not None:
1484 retval = retval.replace('¤¤¤', get_currency_name(currency, value, locale))
1485 retval = retval.replace('¤¤', currency.upper())
1486 retval = retval.replace('¤', get_currency_symbol(currency, locale))
1488 # remove single quotes around text, except for doubled single quotes
1489 # which are replaced with a single quote
1490 retval = re.sub(r"'([^']*)'", lambda m: m.group(1) or "'", retval)
1492 return retval
1494 #
1495 # This is one tricky piece of code. The idea is to rely as much as possible
1496 # on the decimal module to minimize the amount of code.
1497 #
1498 # Conceptually, the implementation of this method can be summarized in the
1499 # following steps:
1500 #
1501 # - Move or shift the decimal point (i.e. the exponent) so the maximum
1502 # amount of significant digits fall into the integer part (i.e. to the
1503 # left of the decimal point)
1504 #
1505 # - Round the number to the nearest integer, discarding all the fractional
1506 # part which contained extra digits to be eliminated
1507 #
1508 # - Convert the rounded integer to a string, that will contain the final
1509 # sequence of significant digits already trimmed to the maximum
1510 #
1511 # - Restore the original position of the decimal point, potentially
1512 # padding with zeroes on either side
1513 #
1514 def _format_significant(self, value: decimal.Decimal, minimum: int, maximum: int) -> str:
1515 exp = value.adjusted()
1516 scale = maximum - 1 - exp
1517 digits = str(value.scaleb(scale).quantize(decimal.Decimal(1)))
1518 if scale <= 0:
1519 result = digits + '0' * -scale
1520 else:
1521 intpart = digits[:-scale]
1522 i = len(intpart)
1523 j = i + max(minimum - i, 0)
1524 result = "{intpart}.{pad:0<{fill}}{fracpart}{fracextra}".format(
1525 intpart=intpart or '0',
1526 pad='',
1527 fill=-min(exp + 1, 0),
1528 fracpart=digits[i:j],
1529 fracextra=digits[j:].rstrip('0'),
1530 ).rstrip('.')
1531 return result
1533 def _format_int(
1534 self,
1535 value: str,
1536 min: int,
1537 max: int,
1538 locale: Locale | str | None,
1539 *,
1540 numbering_system: Literal["default"] | str,
1541 ) -> str:
1542 width = len(value)
1543 if width < min:
1544 value = '0' * (min - width) + value
1545 gsize = self.grouping[0]
1546 ret = ''
1547 symbol = get_group_symbol(locale, numbering_system=numbering_system)
1548 while len(value) > gsize:
1549 ret = symbol + value[-gsize:] + ret
1550 value = value[:-gsize]
1551 gsize = self.grouping[1]
1552 return value + ret
1554 def _quantize_value(
1555 self,
1556 value: decimal.Decimal,
1557 locale: Locale | str | None,
1558 frac_prec: tuple[int, int],
1559 group_separator: bool,
1560 *,
1561 numbering_system: Literal["default"] | str,
1562 ) -> str:
1563 # If the number is +/-Infinity, we can't quantize it
1564 if value.is_infinite():
1565 return get_infinity_symbol(locale, numbering_system=numbering_system)
1566 quantum = get_decimal_quantum(frac_prec[1])
1567 rounded = value.quantize(quantum)
1568 a, sep, b = f"{rounded:f}".partition(".")
1569 integer_part = a
1570 if group_separator:
1571 integer_part = self._format_int(a, self.int_prec[0], self.int_prec[1], locale, numbering_system=numbering_system)
1572 number = integer_part + self._format_frac(b or '0', locale=locale, force_frac=frac_prec, numbering_system=numbering_system)
1573 return number
1575 def _format_frac(
1576 self,
1577 value: str,
1578 locale: Locale | str | None,
1579 force_frac: tuple[int, int] | None = None,
1580 *,
1581 numbering_system: Literal["default"] | str,
1582 ) -> str:
1583 min, max = force_frac or self.frac_prec
1584 if len(value) < min:
1585 value += ('0' * (min - len(value)))
1586 if max == 0 or (min == 0 and int(value) == 0):
1587 return ''
1588 while len(value) > min and value[-1] == '0':
1589 value = value[:-1]
1590 return get_decimal_symbol(locale, numbering_system=numbering_system) + value