Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/babel/numbers.py: 15%
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"""
2babel.numbers
3~~~~~~~~~~~~~
5Locale dependent formatting and parsing of numeric data.
7The default locale for the functions in this module is determined by the
8following 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-2026 by the Babel Team.
16:license: BSD, see LICENSE for more details.
17"""
19# TODO:
20# Padding and rounding increments in pattern:
21# - https://www.unicode.org/reports/tr35/ (Appendix G.6)
22from __future__ import annotations
24import datetime
25import decimal
26import re
27import warnings
28from typing import Any, Literal, cast, overload
30from babel.core import Locale, default_locale, get_global
31from babel.localedata import LocaleDataDict
33LC_MONETARY = default_locale(('LC_MONETARY', 'LC_NUMERIC'))
34LC_NUMERIC = default_locale('LC_NUMERIC')
37class UnknownCurrencyError(Exception):
38 """Exception thrown when a currency is requested for which no data is available."""
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 '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 '$'
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 '{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]: ... # pragma: no cover
214@overload
215def get_territory_currencies(
216 territory: str,
217 start_date: datetime.date | None = ...,
218 end_date: datetime.date | None = ...,
219 tender: bool = ...,
220 non_tender: bool = ...,
221 include_details: Literal[True] = ...,
222) -> list[dict[str, Any]]: ... # pragma: no cover
225def get_territory_currencies(
226 territory: str,
227 start_date: datetime.date | None = None,
228 end_date: datetime.date | None = None,
229 tender: bool = True,
230 non_tender: bool = False,
231 include_details: bool = False,
232) -> list[str] | list[dict[str, Any]]:
233 """Returns the list of currencies for the given territory that are valid for
234 the given date range. In addition to that the currency database
235 distinguishes between tender and non-tender currencies. By default only
236 tender currencies are returned.
238 The return value is a list of all currencies roughly ordered by the time
239 of when the currency became active. The longer the currency is being in
240 use the more to the left of the list it will be.
242 The start date defaults to today. If no end date is given it will be the
243 same as the start date. Otherwise a range can be defined. For instance
244 this can be used to find the currencies in use in Austria between 1995 and
245 2011:
247 >>> from datetime import date
248 >>> get_territory_currencies('AT', date(1995, 1, 1), date(2011, 1, 1))
249 ['ATS', 'EUR']
251 Likewise it's also possible to find all the currencies in use on a
252 single date:
254 >>> get_territory_currencies('AT', date(1995, 1, 1))
255 ['ATS']
256 >>> get_territory_currencies('AT', date(2011, 1, 1))
257 ['EUR']
259 By default the return value only includes tender currencies. This
260 however can be changed:
262 >>> get_territory_currencies('US')
263 ['USD']
264 >>> get_territory_currencies('US', tender=False, non_tender=True,
265 ... start_date=date(2014, 1, 1))
266 ['USN', 'USS']
268 .. versionadded:: 2.0
270 :param territory: the name of the territory to find the currency for.
271 :param start_date: the start date. If not given today is assumed.
272 :param end_date: the end date. If not given the start date is assumed.
273 :param tender: controls whether tender currencies should be included.
274 :param non_tender: controls whether non-tender currencies should be
275 included.
276 :param include_details: if set to `True`, instead of returning currency
277 codes the return value will be dictionaries
278 with detail information. In that case each
279 dictionary will have the keys ``'currency'``,
280 ``'from'``, ``'to'``, and ``'tender'``.
281 """
282 currencies = get_global('territory_currencies')
283 if start_date is None:
284 start_date = datetime.date.today()
285 elif isinstance(start_date, datetime.datetime):
286 start_date = start_date.date()
287 if end_date is None:
288 end_date = start_date
289 elif isinstance(end_date, datetime.datetime):
290 end_date = end_date.date()
292 curs = currencies.get(territory.upper(), ())
293 # TODO: validate that the territory exists
295 def _is_active(start, end):
296 return (start is None or start <= end_date) and (end is None or end >= start_date)
298 result = []
299 for currency_code, start, end, is_tender in curs:
300 if start:
301 start = datetime.date(*start)
302 if end:
303 end = datetime.date(*end)
304 if ((is_tender and tender) or (not is_tender and non_tender)) and _is_active(
305 start,
306 end,
307 ):
308 if include_details:
309 result.append(
310 {
311 'currency': currency_code,
312 'from': start,
313 'to': end,
314 'tender': is_tender,
315 },
316 )
317 else:
318 result.append(currency_code)
320 return result
323def _get_numbering_system(
324 locale: Locale,
325 numbering_system: Literal["default"] | str = "latn",
326) -> str:
327 if numbering_system == "default":
328 return locale.default_numbering_system
329 else:
330 return numbering_system
333def _get_number_symbols(
334 locale: Locale,
335 *,
336 numbering_system: Literal["default"] | str = "latn",
337) -> LocaleDataDict:
338 numbering_system = _get_numbering_system(locale, numbering_system)
339 try:
340 return locale.number_symbols[numbering_system]
341 except KeyError as error:
342 raise UnsupportedNumberingSystemError(
343 f"Unknown numbering system {numbering_system} for Locale {locale}.",
344 ) from error
347class UnsupportedNumberingSystemError(Exception):
348 """Exception thrown when an unsupported numbering system is requested for the given Locale."""
350 pass
353def get_decimal_symbol(
354 locale: Locale | str | None = None,
355 *,
356 numbering_system: Literal["default"] | str = "latn",
357) -> str:
358 """Return the symbol used by the locale to separate decimal fractions.
360 >>> get_decimal_symbol('en_US')
361 '.'
362 >>> get_decimal_symbol('ar_EG', numbering_system='default')
363 '٫'
364 >>> get_decimal_symbol('ar_EG', numbering_system='latn')
365 '.'
367 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
368 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
369 The special value "default" will use the default numbering system of the locale.
370 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
371 """
372 locale = Locale.parse(locale or LC_NUMERIC)
373 return _get_number_symbols(locale, numbering_system=numbering_system).get('decimal', '.')
376def get_plus_sign_symbol(
377 locale: Locale | str | None = None,
378 *,
379 numbering_system: Literal["default"] | str = "latn",
380) -> str:
381 """Return the plus sign symbol used by the current locale.
383 >>> get_plus_sign_symbol('en_US')
384 '+'
385 >>> get_plus_sign_symbol('ar_EG', numbering_system='default')
386 '\\u061c+'
387 >>> get_plus_sign_symbol('ar_EG', numbering_system='latn')
388 '\\u200e+'
390 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
391 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
392 The special value "default" will use the default numbering system of the locale.
393 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
394 """
395 locale = Locale.parse(locale or LC_NUMERIC)
396 return _get_number_symbols(locale, numbering_system=numbering_system).get('plusSign', '+')
399def get_minus_sign_symbol(
400 locale: Locale | str | None = None,
401 *,
402 numbering_system: Literal["default"] | str = "latn",
403) -> str:
404 """Return the plus sign symbol used by the current locale.
406 >>> get_minus_sign_symbol('en_US')
407 '-'
408 >>> get_minus_sign_symbol('ar_EG', numbering_system='default')
409 '\\u061c-'
410 >>> get_minus_sign_symbol('ar_EG', numbering_system='latn')
411 '\\u200e-'
413 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
414 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
415 The special value "default" will use the default numbering system of the locale.
416 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
417 """
418 locale = Locale.parse(locale or LC_NUMERIC)
419 return _get_number_symbols(locale, numbering_system=numbering_system).get('minusSign', '-')
422def get_exponential_symbol(
423 locale: Locale | str | None = None,
424 *,
425 numbering_system: Literal["default"] | str = "latn",
426) -> str:
427 """Return the symbol used by the locale to separate mantissa and exponent.
429 >>> get_exponential_symbol('en_US')
430 'E'
431 >>> get_exponential_symbol('ar_EG', numbering_system='default')
432 'أس'
433 >>> get_exponential_symbol('ar_EG', numbering_system='latn')
434 'E'
436 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
437 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
438 The special value "default" will use the default numbering system of the locale.
439 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
440 """
441 locale = Locale.parse(locale or LC_NUMERIC)
442 return _get_number_symbols(locale, numbering_system=numbering_system).get('exponential', 'E') # fmt: skip
445def get_group_symbol(
446 locale: Locale | str | None = None,
447 *,
448 numbering_system: Literal["default"] | str = "latn",
449) -> str:
450 """Return the symbol used by the locale to separate groups of thousands.
452 >>> get_group_symbol('en_US')
453 ','
454 >>> get_group_symbol('ar_EG', numbering_system='default')
455 '٬'
456 >>> get_group_symbol('ar_EG', numbering_system='latn')
457 ','
459 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
460 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
461 The special value "default" will use the default numbering system of the locale.
462 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
463 """
464 locale = Locale.parse(locale or LC_NUMERIC)
465 return _get_number_symbols(locale, numbering_system=numbering_system).get('group', ',')
468def get_infinity_symbol(
469 locale: Locale | str | None = None,
470 *,
471 numbering_system: Literal["default"] | str = "latn",
472) -> str:
473 """Return the symbol used by the locale to represent infinity.
475 >>> get_infinity_symbol('en_US')
476 '∞'
477 >>> get_infinity_symbol('ar_EG', numbering_system='default')
478 '∞'
479 >>> get_infinity_symbol('ar_EG', numbering_system='latn')
480 '∞'
482 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
483 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
484 The special value "default" will use the default numbering system of the locale.
485 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
486 """
487 locale = Locale.parse(locale or LC_NUMERIC)
488 return _get_number_symbols(locale, numbering_system=numbering_system).get('infinity', '∞')
491def format_number(
492 number: float | decimal.Decimal | str,
493 locale: Locale | str | None = None,
494) -> str:
495 """Return the given number formatted for a specific locale.
497 >>> format_number(1099, locale='en_US') # doctest: +SKIP
498 '1,099'
499 >>> format_number(1099, locale='de_DE') # doctest: +SKIP
500 '1.099'
502 .. deprecated:: 2.6.0
504 Use babel.numbers.format_decimal() instead.
506 :param number: the number to format
507 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
510 """
511 warnings.warn(
512 'Use babel.numbers.format_decimal() instead.',
513 DeprecationWarning,
514 stacklevel=2,
515 )
516 return format_decimal(number, locale=locale)
519def get_decimal_precision(number: decimal.Decimal) -> int:
520 """Return maximum precision of a decimal instance's fractional part.
522 Precision is extracted from the fractional part only.
523 """
524 # Copied from: https://github.com/mahmoud/boltons/pull/59
525 assert isinstance(number, decimal.Decimal)
526 decimal_tuple = number.normalize().as_tuple()
527 # Note: DecimalTuple.exponent can be 'n' (qNaN), 'N' (sNaN), or 'F' (Infinity)
528 if not isinstance(decimal_tuple.exponent, int) or decimal_tuple.exponent >= 0:
529 return 0
530 return abs(decimal_tuple.exponent)
533def get_decimal_quantum(precision: int | decimal.Decimal) -> decimal.Decimal:
534 """Return minimal quantum of a number, as defined by precision."""
535 assert isinstance(precision, (int, decimal.Decimal))
536 return decimal.Decimal(10) ** (-precision)
539def format_decimal(
540 number: float | decimal.Decimal | str,
541 format: str | NumberPattern | None = None,
542 locale: Locale | str | None = None,
543 decimal_quantization: bool = True,
544 group_separator: bool = True,
545 *,
546 numbering_system: Literal["default"] | str = "latn",
547) -> str:
548 """Return the given decimal number formatted for a specific locale.
550 >>> format_decimal(1.2345, locale='en_US')
551 '1.234'
552 >>> format_decimal(1.2346, locale='en_US')
553 '1.235'
554 >>> format_decimal(-1.2346, locale='en_US')
555 '-1.235'
556 >>> format_decimal(1.2345, locale='sv_SE')
557 '1,234'
558 >>> format_decimal(1.2345, locale='de')
559 '1,234'
560 >>> format_decimal(1.2345, locale='ar_EG', numbering_system='default')
561 '1٫234'
562 >>> format_decimal(1.2345, locale='ar_EG', numbering_system='latn')
563 '1.234'
565 The appropriate thousands grouping and the decimal separator are used for
566 each locale:
568 >>> format_decimal(12345.5, locale='en_US')
569 '12,345.5'
571 By default the locale is allowed to truncate and round a high-precision
572 number by forcing its format pattern onto the decimal part. You can bypass
573 this behavior with the `decimal_quantization` parameter:
575 >>> format_decimal(1.2346, locale='en_US')
576 '1.235'
577 >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False)
578 '1.2346'
579 >>> format_decimal(12345.67, locale='fr_CA', group_separator=False)
580 '12345,67'
581 >>> format_decimal(12345.67, locale='en_US', group_separator=True)
582 '12,345.67'
584 :param number: the number to format
585 :param format:
586 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
587 :param decimal_quantization: Truncate and round high-precision numbers to
588 the format pattern. Defaults to `True`.
589 :param group_separator: Boolean to switch group separator on/off in a locale's
590 number format.
591 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
592 The special value "default" will use the default numbering system of the locale.
593 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
594 """
595 locale = Locale.parse(locale or LC_NUMERIC)
596 if format is None:
597 format = locale.decimal_formats[format]
598 pattern = parse_pattern(format)
599 return pattern.apply(
600 number,
601 locale,
602 decimal_quantization=decimal_quantization,
603 group_separator=group_separator,
604 numbering_system=numbering_system,
605 )
608def format_compact_decimal(
609 number: float | decimal.Decimal | str,
610 *,
611 format_type: Literal["short", "long"] = "short",
612 locale: Locale | str | None = None,
613 fraction_digits: int = 0,
614 numbering_system: Literal["default"] | str = "latn",
615) -> str:
616 """Return the given decimal number formatted for a specific locale in compact form.
618 >>> format_compact_decimal(12345, format_type="short", locale='en_US')
619 '12K'
620 >>> format_compact_decimal(12345, format_type="long", locale='en_US')
621 '12 thousand'
622 >>> format_compact_decimal(12345, format_type="short", locale='en_US', fraction_digits=2)
623 '12.34K'
624 >>> format_compact_decimal(1234567, format_type="short", locale="ja_JP")
625 '123万'
626 >>> format_compact_decimal(2345678, format_type="long", locale="mk")
627 '2 милиони'
628 >>> format_compact_decimal(21000000, format_type="long", locale="mk")
629 '21 милион'
630 >>> format_compact_decimal(12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='default')
631 '12٫34\\xa0ألف'
633 :param number: the number to format
634 :param format_type: Compact format to use ("short" or "long")
635 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
636 :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
637 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
638 The special value "default" will use the default numbering system of the locale.
639 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
640 """
641 locale = Locale.parse(locale or LC_NUMERIC)
642 compact_format = locale.compact_decimal_formats[format_type]
643 number, format = _get_compact_format(number, compact_format, locale, fraction_digits)
644 # Did not find a format, fall back.
645 if format is None:
646 format = locale.decimal_formats[None]
647 pattern = parse_pattern(format)
648 return pattern.apply(
649 number,
650 locale,
651 decimal_quantization=False,
652 numbering_system=numbering_system,
653 )
656def _get_compact_format(
657 number: float | decimal.Decimal | str,
658 compact_format: LocaleDataDict,
659 locale: Locale,
660 fraction_digits: int,
661) -> tuple[decimal.Decimal, NumberPattern | None]:
662 """Returns the number after dividing by the unit and the format pattern to use.
663 The algorithm is described here:
664 https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats.
665 """
666 if not isinstance(number, decimal.Decimal):
667 number = decimal.Decimal(str(number))
668 if number.is_nan() or number.is_infinite():
669 return number, None
670 format = None
671 for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True):
672 if abs(number) >= magnitude:
673 # check the pattern using "other" as the amount
674 format = compact_format["other"][str(magnitude)]
675 pattern = parse_pattern(format).pattern
676 # if the pattern is "0", we do not divide the number
677 if pattern == "0":
678 break
679 # otherwise, we need to divide the number by the magnitude but remove zeros
680 # equal to the number of 0's in the pattern minus 1
681 number = cast(
682 decimal.Decimal,
683 number / (magnitude // (10 ** (pattern.count("0") - 1))),
684 )
685 # round to the number of fraction digits requested
686 rounded = round(number, fraction_digits)
687 # if the remaining number is singular, use the singular format
688 plural_form = locale.plural_form(abs(number))
689 if plural_form not in compact_format:
690 plural_form = "other"
691 if number == 1 and "1" in compact_format:
692 plural_form = "1"
693 if str(magnitude) not in compact_format[plural_form]:
694 plural_form = "other" # fall back to other as the implicit default
695 format = compact_format[plural_form][str(magnitude)]
696 number = rounded
697 break
698 return number, format
701class UnknownCurrencyFormatError(KeyError):
702 """Exception raised when an unknown currency format is requested."""
705def format_currency(
706 number: float | decimal.Decimal | str,
707 currency: str,
708 format: str | NumberPattern | None = None,
709 locale: Locale | str | None = None,
710 currency_digits: bool = True,
711 format_type: Literal["name", "standard", "accounting"] = "standard",
712 decimal_quantization: bool = True,
713 group_separator: bool = True,
714 *,
715 numbering_system: Literal["default"] | str = "latn",
716) -> str:
717 """Return formatted currency value.
719 >>> format_currency(1099.98, 'USD', locale='en_US')
720 '$1,099.98'
721 >>> format_currency(1099.98, 'USD', locale='es_CO')
722 'US$1.099,98'
723 >>> format_currency(1099.98, 'EUR', locale='de_DE')
724 '1.099,98\\xa0\\u20ac'
725 >>> format_currency(1099.98, 'EGP', locale='ar_EG', numbering_system='default')
726 '\\u200f1٬099٫98\\xa0ج.م.\\u200f'
728 The format can also be specified explicitly. The currency is
729 placed with the '¤' sign. As the sign gets repeated the format
730 expands (¤ being the symbol, ¤¤ is the currency abbreviation and
731 ¤¤¤ is the full name of the currency):
733 >>> format_currency(1099.98, 'EUR', '\\xa4\\xa4 #,##0.00', locale='en_US')
734 'EUR 1,099.98'
735 >>> format_currency(1099.98, 'EUR', '#,##0.00 \\xa4\\xa4\\xa4', locale='en_US')
736 '1,099.98 euros'
738 Currencies usually have a specific number of decimal digits. This function
739 favours that information over the given format:
741 >>> format_currency(1099.98, 'JPY', locale='en_US')
742 '\\xa51,100'
743 >>> format_currency(1099.98, 'COP', '#,##0.00', locale='es_ES')
744 '1.099,98'
746 However, the number of decimal digits can be overridden from the currency
747 information, by setting the last parameter to ``False``:
749 >>> format_currency(1099.98, 'JPY', locale='en_US', currency_digits=False)
750 '\\xa51,099.98'
751 >>> format_currency(1099.98, 'COP', '#,##0.00', locale='es_ES', currency_digits=False)
752 '1.099,98'
754 If a format is not specified the type of currency format to use
755 from the locale can be specified:
757 >>> format_currency(1099.98, 'EUR', locale='en_US', format_type='standard')
758 '\\u20ac1,099.98'
760 When the given currency format type is not available, an exception is
761 raised:
763 >>> format_currency('1099.98', 'EUR', locale='root', format_type='unknown')
764 Traceback (most recent call last):
765 ...
766 UnknownCurrencyFormatError: "'unknown' is not a known currency format type"
768 >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False)
769 '$101299.98'
771 >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True)
772 '$101,299.98'
774 You can also pass format_type='name' to use long display names. The order of
775 the number and currency name, along with the correct localized plural form
776 of the currency name, is chosen according to locale:
778 >>> format_currency(1, 'USD', locale='en_US', format_type='name')
779 '1.00 US dollar'
780 >>> format_currency(1099.98, 'USD', locale='en_US', format_type='name')
781 '1,099.98 US dollars'
782 >>> format_currency(1099.98, 'USD', locale='ee', format_type='name')
783 'us ga dollar 1,099.98'
785 By default the locale is allowed to truncate and round a high-precision
786 number by forcing its format pattern onto the decimal part. You can bypass
787 this behavior with the `decimal_quantization` parameter:
789 >>> format_currency(1099.9876, 'USD', locale='en_US')
790 '$1,099.99'
791 >>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False)
792 '$1,099.9876'
794 :param number: the number to format
795 :param currency: the currency code
796 :param format: the format string to use
797 :param locale: the `Locale` object or locale identifier.
798 Defaults to the system currency locale or numeric locale.
799 :param currency_digits: use the currency's natural number of decimal digits
800 :param format_type: the currency format type to use
801 :param decimal_quantization: Truncate and round high-precision numbers to
802 the format pattern. Defaults to `True`.
803 :param group_separator: Boolean to switch group separator on/off in a locale's
804 number format.
805 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
806 The special value "default" will use the default numbering system of the locale.
807 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
808 """
809 locale = Locale.parse(locale or LC_MONETARY)
811 if format_type == 'name':
812 return _format_currency_long_name(
813 number,
814 currency,
815 locale=locale,
816 format=format,
817 currency_digits=currency_digits,
818 decimal_quantization=decimal_quantization,
819 group_separator=group_separator,
820 numbering_system=numbering_system,
821 )
823 if format:
824 pattern = parse_pattern(format)
825 else:
826 try:
827 pattern = locale.currency_formats[format_type]
828 except KeyError:
829 raise UnknownCurrencyFormatError(
830 f"{format_type!r} is not a known currency format type",
831 ) from None
833 return pattern.apply(
834 number,
835 locale,
836 currency=currency,
837 currency_digits=currency_digits,
838 decimal_quantization=decimal_quantization,
839 group_separator=group_separator,
840 numbering_system=numbering_system,
841 )
844def _format_currency_long_name(
845 number: float | decimal.Decimal | str,
846 currency: str,
847 *,
848 locale: Locale,
849 format: str | NumberPattern | None,
850 currency_digits: bool,
851 decimal_quantization: bool,
852 group_separator: bool,
853 numbering_system: Literal["default"] | str,
854) -> str:
855 # Algorithm described here:
856 # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies
858 # Step 1.
859 # There are no examples of items with explicit count (0 or 1) in current
860 # locale data. So there is no point implementing that.
861 # Step 2.
863 # Correct number to numeric type, important for looking up plural rules:
864 number_n = float(number) if isinstance(number, str) else number
866 # Step 3.
867 unit_pattern = get_currency_unit_pattern(currency, count=number_n, locale=locale)
869 # Step 4.
870 display_name = get_currency_name(currency, count=number_n, locale=locale)
872 # Step 5.
873 if not format:
874 format = locale.decimal_formats[None]
876 pattern = parse_pattern(format)
878 number_part = pattern.apply(
879 number,
880 locale,
881 currency=currency,
882 currency_digits=currency_digits,
883 decimal_quantization=decimal_quantization,
884 group_separator=group_separator,
885 numbering_system=numbering_system,
886 )
888 return unit_pattern.format(number_part, display_name)
891def format_compact_currency(
892 number: float | decimal.Decimal | str,
893 currency: str,
894 *,
895 format_type: Literal["short"] = "short",
896 locale: Locale | str | None = None,
897 fraction_digits: int = 0,
898 numbering_system: Literal["default"] | str = "latn",
899) -> str:
900 """Format a number as a currency value in compact form.
902 >>> format_compact_currency(12345, 'USD', locale='en_US')
903 '$12K'
904 >>> format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2)
905 '$123.46M'
906 >>> format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1)
907 '123,5\\xa0Mio.\\xa0€'
909 :param number: the number to format
910 :param currency: the currency code
911 :param format_type: the compact format type to use. Defaults to "short".
912 :param locale: the `Locale` object or locale identifier.
913 Defaults to the system currency locale or numeric locale.
914 :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
915 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
916 The special value "default" will use the default numbering system of the locale.
917 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
918 """
919 locale = Locale.parse(locale or LC_MONETARY)
920 try:
921 compact_format = locale.compact_currency_formats[format_type]
922 except KeyError as error:
923 raise UnknownCurrencyFormatError(
924 f"{format_type!r} is not a known compact currency format type",
925 ) from error
926 number, format = _get_compact_format(number, compact_format, locale, fraction_digits)
927 # Did not find a format, fall back.
928 if format is None or "¤" not in str(format):
929 # find first format that has a currency symbol
930 for magnitude in compact_format['other']:
931 format = compact_format['other'][magnitude].pattern
932 if '¤' not in format:
933 continue
934 # remove characters that are not the currency symbol, 0's or spaces
935 format = re.sub(r'[^0\s\¤]', '', format)
936 # compress adjacent spaces into one
937 format = re.sub(r'(\s)\s+', r'\1', format).strip()
938 break
939 if format is None:
940 raise ValueError('No compact currency format found for the given number and locale.')
941 pattern = parse_pattern(format)
942 return pattern.apply(
943 number,
944 locale,
945 currency=currency,
946 currency_digits=False,
947 decimal_quantization=False,
948 numbering_system=numbering_system,
949 )
952def format_percent(
953 number: float | decimal.Decimal | str,
954 format: str | NumberPattern | None = None,
955 locale: Locale | str | None = None,
956 decimal_quantization: bool = True,
957 group_separator: bool = True,
958 *,
959 numbering_system: Literal["default"] | str = "latn",
960) -> str:
961 """Return formatted percent value for a specific locale.
963 >>> format_percent(0.34, locale='en_US')
964 '34%'
965 >>> format_percent(25.1234, locale='en_US')
966 '2,512%'
967 >>> format_percent(25.1234, locale='sv_SE')
968 '2\\xa0512\\xa0%'
969 >>> format_percent(25.1234, locale='ar_EG', numbering_system='default')
970 '2٬512%'
972 The format pattern can also be specified explicitly:
974 >>> format_percent(25.1234, '#,##0\\u2030', locale='en_US')
975 '25,123‰'
977 By default the locale is allowed to truncate and round a high-precision
978 number by forcing its format pattern onto the decimal part. You can bypass
979 this behavior with the `decimal_quantization` parameter:
981 >>> format_percent(23.9876, locale='en_US')
982 '2,399%'
983 >>> format_percent(23.9876, locale='en_US', decimal_quantization=False)
984 '2,398.76%'
986 >>> format_percent(229291.1234, locale='pt_BR', group_separator=False)
987 '22929112%'
989 >>> format_percent(229291.1234, locale='pt_BR', group_separator=True)
990 '22.929.112%'
992 :param number: the percent number to format
993 :param format:
994 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
995 :param decimal_quantization: Truncate and round high-precision numbers to
996 the format pattern. Defaults to `True`.
997 :param group_separator: Boolean to switch group separator on/off in a locale's
998 number format.
999 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
1000 The special value "default" will use the default numbering system of the locale.
1001 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
1002 """
1003 locale = Locale.parse(locale or LC_NUMERIC)
1004 if not format:
1005 format = locale.percent_formats[None]
1006 pattern = parse_pattern(format)
1007 return pattern.apply(
1008 number,
1009 locale,
1010 decimal_quantization=decimal_quantization,
1011 group_separator=group_separator,
1012 numbering_system=numbering_system,
1013 )
1016def format_scientific(
1017 number: float | decimal.Decimal | str,
1018 format: str | NumberPattern | None = None,
1019 locale: Locale | str | None = None,
1020 decimal_quantization: bool = True,
1021 *,
1022 numbering_system: Literal["default"] | str = "latn",
1023) -> str:
1024 """Return value formatted in scientific notation for a specific locale.
1026 >>> format_scientific(10000, locale='en_US')
1027 '1E4'
1028 >>> format_scientific(10000, locale='ar_EG', numbering_system='default')
1029 '1أس4'
1031 The format pattern can also be specified explicitly:
1033 >>> format_scientific(1234567, '##0.##E00', locale='en_US')
1034 '1.23E06'
1036 By default the locale is allowed to truncate and round a high-precision
1037 number by forcing its format pattern onto the decimal part. You can bypass
1038 this behavior with the `decimal_quantization` parameter:
1040 >>> format_scientific(1234.9876, '#.##E0', locale='en_US')
1041 '1.23E3'
1042 >>> format_scientific(1234.9876, '#.##E0', locale='en_US', decimal_quantization=False)
1043 '1.2349876E3'
1045 :param number: the number to format
1046 :param format:
1047 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
1048 :param decimal_quantization: Truncate and round high-precision numbers to
1049 the format pattern. Defaults to `True`.
1050 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
1051 The special value "default" will use the default numbering system of the locale.
1052 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
1053 """
1054 locale = Locale.parse(locale or LC_NUMERIC)
1055 if not format:
1056 format = locale.scientific_formats[None]
1057 pattern = parse_pattern(format)
1058 return pattern.apply(
1059 number,
1060 locale,
1061 decimal_quantization=decimal_quantization,
1062 numbering_system=numbering_system,
1063 )
1066class NumberFormatError(ValueError):
1067 """Exception raised when a string cannot be parsed into a number."""
1069 def __init__(self, message: str, suggestions: list[str] | None = None) -> None:
1070 super().__init__(message)
1071 #: a list of properly formatted numbers derived from the invalid input
1072 self.suggestions = suggestions
1075SPACE_CHARS = {
1076 ' ', # space
1077 '\xa0', # no-break space
1078 '\u202f', # narrow no-break space
1079}
1081SPACE_CHARS_RE = re.compile('|'.join(SPACE_CHARS))
1084def parse_number(
1085 string: str,
1086 locale: Locale | str | None = None,
1087 *,
1088 numbering_system: Literal["default"] | str = "latn",
1089) -> int:
1090 """Parse localized number string into an integer.
1092 >>> parse_number('1,099', locale='en_US')
1093 1099
1094 >>> parse_number('1.099', locale='de_DE')
1095 1099
1097 When the given string cannot be parsed, an exception is raised:
1099 >>> parse_number('1.099,98', locale='de')
1100 Traceback (most recent call last):
1101 ...
1102 NumberFormatError: '1.099,98' is not a valid number
1104 :param string: the string to parse
1105 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
1106 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
1107 The special value "default" will use the default numbering system of the locale.
1108 :return: the parsed number
1109 :raise `NumberFormatError`: if the string can not be converted to a number
1110 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
1111 """
1112 group_symbol = get_group_symbol(locale, numbering_system=numbering_system)
1114 if (
1115 # if the grouping symbol is a kind of space,
1116 group_symbol in SPACE_CHARS
1117 # and the string to be parsed does not contain it,
1118 and group_symbol not in string
1119 # but it does contain any other kind of space instead,
1120 and SPACE_CHARS_RE.search(string)
1121 ):
1122 # ... it's reasonable to assume it is taking the place of the grouping symbol.
1123 string = SPACE_CHARS_RE.sub(group_symbol, string)
1125 try:
1126 return int(string.replace(group_symbol, ''))
1127 except ValueError as ve:
1128 raise NumberFormatError(f"{string!r} is not a valid number") from ve
1131def parse_decimal(
1132 string: str,
1133 locale: Locale | str | None = None,
1134 strict: bool = False,
1135 *,
1136 numbering_system: Literal["default"] | str = "latn",
1137) -> decimal.Decimal:
1138 """Parse localized decimal string into a decimal.
1140 >>> parse_decimal('1,099.98', locale='en_US')
1141 Decimal('1099.98')
1142 >>> parse_decimal('1.099,98', locale='de')
1143 Decimal('1099.98')
1144 >>> parse_decimal('12 345,123', locale='ru')
1145 Decimal('12345.123')
1146 >>> parse_decimal('1٬099٫98', locale='ar_EG', numbering_system='default')
1147 Decimal('1099.98')
1149 When the given string cannot be parsed, an exception is raised:
1151 >>> parse_decimal('2,109,998', locale='de')
1152 Traceback (most recent call last):
1153 ...
1154 NumberFormatError: '2,109,998' is not a valid decimal number
1156 If `strict` is set to `True` and the given string contains a number
1157 formatted in an irregular way, an exception is raised:
1159 >>> parse_decimal('30.00', locale='de', strict=True)
1160 Traceback (most recent call last):
1161 ...
1162 NumberFormatError: '30.00' is not a properly formatted decimal number. Did you mean '3.000'? Or maybe '30,00'?
1164 >>> parse_decimal('0.00', locale='de', strict=True)
1165 Traceback (most recent call last):
1166 ...
1167 NumberFormatError: '0.00' is not a properly formatted decimal number. Did you mean '0'?
1169 :param string: the string to parse
1170 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
1171 :param strict: controls whether numbers formatted in a weird way are
1172 accepted or rejected
1173 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
1174 The special value "default" will use the default numbering system of the locale.
1175 :raise NumberFormatError: if the string can not be converted to a
1176 decimal number
1177 :raise UnsupportedNumberingSystemError: if the numbering system is not supported by the locale.
1178 """
1179 locale = Locale.parse(locale or LC_NUMERIC)
1180 group_symbol = get_group_symbol(locale, numbering_system=numbering_system)
1181 decimal_symbol = get_decimal_symbol(locale, numbering_system=numbering_system)
1183 if not strict and (
1184 group_symbol in SPACE_CHARS # if the grouping symbol is a kind of space,
1185 and group_symbol not in string # and the string to be parsed does not contain it,
1186 # but it does contain any other kind of space instead,
1187 and SPACE_CHARS_RE.search(string)
1188 ):
1189 # ... it's reasonable to assume it is taking the place of the grouping symbol.
1190 string = SPACE_CHARS_RE.sub(group_symbol, string)
1192 try:
1193 parsed = decimal.Decimal(string.replace(group_symbol, '').replace(decimal_symbol, '.'))
1194 except decimal.InvalidOperation as exc:
1195 raise NumberFormatError(f"{string!r} is not a valid decimal number") from exc
1196 if strict and group_symbol in string:
1197 proper = format_decimal(
1198 parsed,
1199 locale=locale,
1200 decimal_quantization=False,
1201 numbering_system=numbering_system,
1202 )
1203 if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol): # fmt: skip
1204 try:
1205 parsed_alt = decimal.Decimal(
1206 string.replace(decimal_symbol, '').replace(group_symbol, '.'),
1207 )
1208 except decimal.InvalidOperation as exc:
1209 raise NumberFormatError(
1210 f"{string!r} is not a properly formatted decimal number. "
1211 f"Did you mean {proper!r}?",
1212 suggestions=[proper],
1213 ) from exc
1214 else:
1215 proper_alt = format_decimal(
1216 parsed_alt,
1217 locale=locale,
1218 decimal_quantization=False,
1219 numbering_system=numbering_system,
1220 )
1221 if proper_alt == proper:
1222 raise NumberFormatError(
1223 f"{string!r} is not a properly formatted decimal number. "
1224 f"Did you mean {proper!r}?",
1225 suggestions=[proper],
1226 )
1227 else:
1228 raise NumberFormatError(
1229 f"{string!r} is not a properly formatted decimal number. "
1230 f"Did you mean {proper!r}? Or maybe {proper_alt!r}?",
1231 suggestions=[proper, proper_alt],
1232 )
1233 return parsed
1236def _remove_trailing_zeros_after_decimal(string: str, decimal_symbol: str) -> str:
1237 """
1238 Remove trailing zeros from the decimal part of a numeric string.
1240 This function takes a string representing a numeric value and a decimal symbol.
1241 It removes any trailing zeros that appear after the decimal symbol in the number.
1242 If the decimal part becomes empty after removing trailing zeros, the decimal symbol
1243 is also removed. If the string does not contain the decimal symbol, it is returned unchanged.
1245 :param string: The numeric string from which to remove trailing zeros.
1246 :type string: str
1247 :param decimal_symbol: The symbol used to denote the decimal point.
1248 :type decimal_symbol: str
1249 :return: The numeric string with trailing zeros removed from its decimal part.
1250 :rtype: str
1252 Example:
1253 >>> _remove_trailing_zeros_after_decimal("123.4500", ".")
1254 '123.45'
1255 >>> _remove_trailing_zeros_after_decimal("100.000", ".")
1256 '100'
1257 >>> _remove_trailing_zeros_after_decimal("100", ".")
1258 '100'
1259 """
1260 integer_part, _, decimal_part = string.partition(decimal_symbol)
1262 if decimal_part:
1263 decimal_part = decimal_part.rstrip("0")
1264 if decimal_part:
1265 return integer_part + decimal_symbol + decimal_part
1266 return integer_part
1268 return string
1271_number_pattern_re = re.compile(
1272 r"(?P<prefix>(?:[^'0-9@#.,]|'[^']*')*)"
1273 r"(?P<number>[0-9@#.,E+]*)"
1274 r"(?P<suffix>.*)",
1275)
1278def parse_grouping(p: str) -> tuple[int, int]:
1279 """Parse primary and secondary digit grouping
1281 >>> parse_grouping('##')
1282 (1000, 1000)
1283 >>> parse_grouping('#,###')
1284 (3, 3)
1285 >>> parse_grouping('#,####,###')
1286 (3, 4)
1287 """
1288 width = len(p)
1289 g1 = p.rfind(',')
1290 if g1 == -1:
1291 return 1000, 1000
1292 g1 = width - g1 - 1
1293 g2 = p[: -g1 - 1].rfind(',')
1294 if g2 == -1:
1295 return g1, g1
1296 g2 = width - g1 - g2 - 2
1297 return g1, g2
1300def parse_pattern(pattern: NumberPattern | str) -> NumberPattern:
1301 """Parse number format patterns"""
1302 if isinstance(pattern, NumberPattern):
1303 return pattern
1305 def _match_number(pattern):
1306 rv = _number_pattern_re.search(pattern)
1307 if rv is None:
1308 raise ValueError(f"Invalid number pattern {pattern!r}")
1309 return rv.groups()
1311 pos_pattern = pattern
1313 # Do we have a negative subpattern?
1314 if ';' in pattern:
1315 pos_pattern, neg_pattern = pattern.split(';', 1)
1316 pos_prefix, number, pos_suffix = _match_number(pos_pattern)
1317 neg_prefix, _, neg_suffix = _match_number(neg_pattern)
1318 else:
1319 pos_prefix, number, pos_suffix = _match_number(pos_pattern)
1320 neg_prefix = f"-{pos_prefix}"
1321 neg_suffix = pos_suffix
1322 if 'E' in number:
1323 number, exp = number.split('E', 1)
1324 else:
1325 exp = None
1326 if '@' in number and '.' in number and '0' in number:
1327 raise ValueError('Significant digit patterns can not contain "@" or "0"')
1328 if '.' in number:
1329 integer, fraction = number.rsplit('.', 1)
1330 else:
1331 integer = number
1332 fraction = ''
1334 def parse_precision(p):
1335 """Calculate the min and max allowed digits"""
1336 min = max = 0
1337 for c in p:
1338 if c in '@0':
1339 min += 1
1340 max += 1
1341 elif c == '#':
1342 max += 1
1343 elif c == ',':
1344 continue
1345 else:
1346 break
1347 return min, max
1349 int_prec = parse_precision(integer)
1350 frac_prec = parse_precision(fraction)
1351 if exp:
1352 exp_plus = exp.startswith('+')
1353 exp = exp.lstrip('+')
1354 exp_prec = parse_precision(exp)
1355 else:
1356 exp_plus = None
1357 exp_prec = None
1358 grouping = parse_grouping(integer)
1359 return NumberPattern(
1360 pattern,
1361 (pos_prefix, neg_prefix),
1362 (pos_suffix, neg_suffix),
1363 grouping,
1364 int_prec,
1365 frac_prec,
1366 exp_prec,
1367 exp_plus,
1368 number,
1369 )
1372class NumberPattern:
1373 def __init__(
1374 self,
1375 pattern: str,
1376 prefix: tuple[str, str],
1377 suffix: tuple[str, str],
1378 grouping: tuple[int, int],
1379 int_prec: tuple[int, int],
1380 frac_prec: tuple[int, int],
1381 exp_prec: tuple[int, int] | None,
1382 exp_plus: bool | None,
1383 number_pattern: str | None = None,
1384 ) -> None:
1385 # Metadata of the decomposed parsed pattern.
1386 self.pattern = pattern
1387 self.prefix = prefix
1388 self.suffix = suffix
1389 self.number_pattern = number_pattern
1390 self.grouping = grouping
1391 self.int_prec = int_prec
1392 self.frac_prec = frac_prec
1393 self.exp_prec = exp_prec
1394 self.exp_plus = exp_plus
1395 self.scale = self.compute_scale()
1397 def __repr__(self) -> str:
1398 return f"<{type(self).__name__} {self.pattern!r}>"
1400 def compute_scale(self) -> Literal[0, 2, 3]:
1401 """Return the scaling factor to apply to the number before rendering.
1403 Auto-set to a factor of 2 or 3 if presence of a ``%`` or ``‰`` sign is
1404 detected in the prefix or suffix of the pattern. Default is to not mess
1405 with the scale at all and keep it to 0.
1406 """
1407 scale = 0
1408 if '%' in ''.join(self.prefix + self.suffix):
1409 scale = 2
1410 elif '‰' in ''.join(self.prefix + self.suffix):
1411 scale = 3
1412 return scale
1414 def scientific_notation_elements(
1415 self,
1416 value: decimal.Decimal,
1417 locale: Locale | str | None,
1418 *,
1419 numbering_system: Literal["default"] | str = "latn",
1420 ) -> tuple[decimal.Decimal, int, str]:
1421 """Returns normalized scientific notation components of a value."""
1422 # Normalize value to only have one lead digit.
1423 exp = value.adjusted()
1424 value = value * get_decimal_quantum(exp)
1425 assert value.adjusted() == 0
1427 # Shift exponent and value by the minimum number of leading digits
1428 # imposed by the rendering pattern. And always make that number
1429 # greater or equal to 1.
1430 lead_shift = max([1, min(self.int_prec)]) - 1
1431 exp = exp - lead_shift
1432 value = value * get_decimal_quantum(-lead_shift)
1434 # Get exponent sign symbol.
1435 exp_sign = ''
1436 if exp < 0:
1437 exp_sign = get_minus_sign_symbol(locale, numbering_system=numbering_system)
1438 elif self.exp_plus:
1439 exp_sign = get_plus_sign_symbol(locale, numbering_system=numbering_system)
1441 # Normalize exponent value now that we have the sign.
1442 exp = abs(exp)
1444 return value, exp, exp_sign
1446 def apply(
1447 self,
1448 value: float | decimal.Decimal | str,
1449 locale: Locale | str | None,
1450 currency: str | None = None,
1451 currency_digits: bool = True,
1452 decimal_quantization: bool = True,
1453 force_frac: tuple[int, int] | None = None,
1454 group_separator: bool = True,
1455 *,
1456 numbering_system: Literal["default"] | str = "latn",
1457 ):
1458 """Renders into a string a number following the defined pattern.
1460 Forced decimal quantization is active by default so we'll produce a
1461 number string that is strictly following CLDR pattern definitions.
1463 :param value: The value to format. If this is not a Decimal object,
1464 it will be cast to one.
1465 :type value: decimal.Decimal|float|int
1466 :param locale: The locale to use for formatting.
1467 :type locale: str|babel.core.Locale
1468 :param currency: Which currency, if any, to format as.
1469 :type currency: str|None
1470 :param currency_digits: Whether or not to use the currency's precision.
1471 If false, the pattern's precision is used.
1472 :type currency_digits: bool
1473 :param decimal_quantization: Whether decimal numbers should be forcibly
1474 quantized to produce a formatted output
1475 strictly matching the CLDR definition for
1476 the locale.
1477 :type decimal_quantization: bool
1478 :param force_frac: DEPRECATED - a forced override for `self.frac_prec`
1479 for a single formatting invocation.
1480 :param group_separator: Whether to use the locale's number group separator.
1481 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
1482 The special value "default" will use the default numbering system of the locale.
1483 :return: Formatted decimal string.
1484 :rtype: str
1485 :raise UnsupportedNumberingSystemError: If the numbering system is not supported by the locale.
1486 """
1487 if not isinstance(value, decimal.Decimal):
1488 value = decimal.Decimal(str(value))
1490 value = value.scaleb(self.scale)
1492 # Separate the absolute value from its sign.
1493 is_negative = int(value.is_signed())
1494 value = abs(value).normalize()
1496 # Prepare scientific notation metadata.
1497 if self.exp_prec:
1498 value, exp, exp_sign = self.scientific_notation_elements(
1499 value,
1500 locale,
1501 numbering_system=numbering_system,
1502 )
1504 # Adjust the precision of the fractional part and force it to the
1505 # currency's if necessary.
1506 if force_frac:
1507 # TODO (3.x?): Remove this parameter
1508 warnings.warn(
1509 'The force_frac parameter to NumberPattern.apply() is deprecated.',
1510 DeprecationWarning,
1511 stacklevel=2,
1512 )
1513 frac_prec = force_frac
1514 elif currency and currency_digits:
1515 frac_prec = (get_currency_precision(currency),) * 2
1516 else:
1517 frac_prec = self.frac_prec
1519 # Bump decimal precision to the natural precision of the number if it
1520 # exceeds the one we're about to use. This adaptative precision is only
1521 # triggered if the decimal quantization is disabled or if a scientific
1522 # notation pattern has a missing mandatory fractional part (as in the
1523 # default '#E0' pattern). This special case has been extensively
1524 # discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 .
1525 if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)):
1526 frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)]))
1528 # Render scientific notation.
1529 if self.exp_prec:
1530 number = ''.join([
1531 self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system),
1532 get_exponential_symbol(locale, numbering_system=numbering_system),
1533 exp_sign, # type: ignore # exp_sign is always defined here
1534 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
1535 ]) # fmt: skip
1537 # Is it a significant digits pattern?
1538 elif '@' in self.pattern:
1539 text = self._format_significant(value, self.int_prec[0], self.int_prec[1])
1540 a, sep, b = text.partition(".")
1541 number = self._format_int(a, 0, 1000, locale, numbering_system=numbering_system)
1542 if sep:
1543 number += get_decimal_symbol(locale, numbering_system=numbering_system) + b
1545 # A normal number pattern.
1546 else:
1547 number = self._quantize_value(
1548 value,
1549 locale,
1550 frac_prec,
1551 group_separator,
1552 numbering_system=numbering_system,
1553 )
1555 retval = ''.join(
1556 (
1557 self.prefix[is_negative],
1558 number if self.number_pattern != '' else '',
1559 self.suffix[is_negative],
1560 ),
1561 )
1563 if '¤' in retval and currency is not None:
1564 retval = retval.replace('¤¤¤', get_currency_name(currency, value, locale))
1565 retval = retval.replace('¤¤', currency.upper())
1566 retval = retval.replace('¤', get_currency_symbol(currency, locale))
1568 # remove single quotes around text, except for doubled single quotes
1569 # which are replaced with a single quote
1570 retval = re.sub(r"'([^']*)'", lambda m: m.group(1) or "'", retval)
1572 return retval
1574 #
1575 # This is one tricky piece of code. The idea is to rely as much as possible
1576 # on the decimal module to minimize the amount of code.
1577 #
1578 # Conceptually, the implementation of this method can be summarized in the
1579 # following steps:
1580 #
1581 # - Move or shift the decimal point (i.e. the exponent) so the maximum
1582 # amount of significant digits fall into the integer part (i.e. to the
1583 # left of the decimal point)
1584 #
1585 # - Round the number to the nearest integer, discarding all the fractional
1586 # part which contained extra digits to be eliminated
1587 #
1588 # - Convert the rounded integer to a string, that will contain the final
1589 # sequence of significant digits already trimmed to the maximum
1590 #
1591 # - Restore the original position of the decimal point, potentially
1592 # padding with zeroes on either side
1593 #
1594 def _format_significant(self, value: decimal.Decimal, minimum: int, maximum: int) -> str:
1595 exp = value.adjusted()
1596 scale = maximum - 1 - exp
1597 digits = str(value.scaleb(scale).quantize(decimal.Decimal(1)))
1598 if scale <= 0:
1599 result = digits + '0' * -scale
1600 else:
1601 intpart = digits[:-scale]
1602 i = len(intpart)
1603 j = i + max(minimum - i, 0)
1604 result = "{intpart}.{pad:0<{fill}}{fracpart}{fracextra}".format(
1605 intpart=intpart or '0',
1606 pad='',
1607 fill=-min(exp + 1, 0),
1608 fracpart=digits[i:j],
1609 fracextra=digits[j:].rstrip('0'),
1610 ).rstrip('.')
1611 return result
1613 def _format_int(
1614 self,
1615 value: str,
1616 min: int,
1617 max: int,
1618 locale: Locale | str | None,
1619 *,
1620 numbering_system: Literal["default"] | str,
1621 ) -> str:
1622 width = len(value)
1623 if width < min:
1624 value = '0' * (min - width) + value
1625 gsize = self.grouping[0]
1626 ret = ''
1627 symbol = get_group_symbol(locale, numbering_system=numbering_system)
1628 while len(value) > gsize:
1629 ret = symbol + value[-gsize:] + ret
1630 value = value[:-gsize]
1631 gsize = self.grouping[1]
1632 return value + ret
1634 def _quantize_value(
1635 self,
1636 value: decimal.Decimal,
1637 locale: Locale | str | None,
1638 frac_prec: tuple[int, int],
1639 group_separator: bool,
1640 *,
1641 numbering_system: Literal["default"] | str,
1642 ) -> str:
1643 # If the number is +/-Infinity, we can't quantize it
1644 if value.is_infinite():
1645 return get_infinity_symbol(locale, numbering_system=numbering_system)
1646 quantum = get_decimal_quantum(frac_prec[1])
1647 rounded = value.quantize(quantum)
1648 a, sep, b = f"{rounded:f}".partition(".")
1649 integer_part = a
1650 if group_separator:
1651 integer_part = self._format_int(
1652 a,
1653 self.int_prec[0],
1654 self.int_prec[1],
1655 locale,
1656 numbering_system=numbering_system,
1657 )
1658 number = integer_part + self._format_frac(
1659 b or '0',
1660 locale=locale,
1661 force_frac=frac_prec,
1662 numbering_system=numbering_system,
1663 )
1664 return number
1666 def _format_frac(
1667 self,
1668 value: str,
1669 locale: Locale | str | None,
1670 force_frac: tuple[int, int] | None = None,
1671 *,
1672 numbering_system: Literal["default"] | str,
1673 ) -> str:
1674 min, max = force_frac or self.frac_prec
1675 if len(value) < min:
1676 value += '0' * (min - len(value))
1677 if max == 0 or (min == 0 and int(value) == 0):
1678 return ''
1679 while len(value) > min and value[-1] == '0':
1680 value = value[:-1]
1681 return get_decimal_symbol(locale, numbering_system=numbering_system) + value