Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/babel/numbers.py: 8%
413 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
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_NUMERIC``,
11 * ``LC_ALL``, and
12 * ``LANG``
14 :copyright: (c) 2013-2023 by the Babel Team.
15 :license: BSD, see LICENSE for more details.
16"""
17# TODO:
18# Padding and rounding increments in pattern:
19# - https://www.unicode.org/reports/tr35/ (Appendix G.6)
20from __future__ import annotations
22import datetime
23import decimal
24import re
25import warnings
26from typing import TYPE_CHECKING, Any, cast, overload
28from babel.core import Locale, default_locale, get_global
29from babel.localedata import LocaleDataDict
31if TYPE_CHECKING:
32 from typing_extensions import Literal
34LC_NUMERIC = default_locale('LC_NUMERIC')
37class UnknownCurrencyError(Exception):
38 """Exception thrown when a currency is requested for which no data is available.
39 """
41 def __init__(self, identifier: str) -> None:
42 """Create the exception.
43 :param identifier: the identifier string of the unsupported currency
44 """
45 Exception.__init__(self, f"Unknown currency {identifier!r}.")
47 #: The identifier of the locale that could not be found.
48 self.identifier = identifier
51def list_currencies(locale: Locale | str | None = None) -> set[str]:
52 """ Return a `set` of normalized currency codes.
54 .. versionadded:: 2.5.0
56 :param locale: filters returned currency codes by the provided locale.
57 Expected to be a locale instance or code. If no locale is
58 provided, returns the list of all currencies from all
59 locales.
60 """
61 # Get locale-scoped currencies.
62 if locale:
63 currencies = Locale.parse(locale).currencies.keys()
64 else:
65 currencies = get_global('all_currencies')
66 return set(currencies)
69def validate_currency(currency: str, locale: Locale | str | None = None) -> None:
70 """ Check the currency code is recognized by Babel.
72 Accepts a ``locale`` parameter for fined-grained validation, working as
73 the one defined above in ``list_currencies()`` method.
75 Raises a `UnknownCurrencyError` exception if the currency is unknown to Babel.
76 """
77 if currency not in list_currencies(locale):
78 raise UnknownCurrencyError(currency)
81def is_currency(currency: str, locale: Locale | str | None = None) -> bool:
82 """ Returns `True` only if a currency is recognized by Babel.
84 This method always return a Boolean and never raise.
85 """
86 if not currency or not isinstance(currency, str):
87 return False
88 try:
89 validate_currency(currency, locale)
90 except UnknownCurrencyError:
91 return False
92 return True
95def normalize_currency(currency: str, locale: Locale | str | None = None) -> str | None:
96 """Returns the normalized identifier of any currency code.
98 Accepts a ``locale`` parameter for fined-grained validation, working as
99 the one defined above in ``list_currencies()`` method.
101 Returns None if the currency is unknown to Babel.
102 """
103 if isinstance(currency, str):
104 currency = currency.upper()
105 if not is_currency(currency, locale):
106 return
107 return currency
110def get_currency_name(
111 currency: str,
112 count: float | decimal.Decimal | None = None,
113 locale: Locale | str | None = LC_NUMERIC,
114) -> str:
115 """Return the name used by the locale for the specified currency.
117 >>> get_currency_name('USD', locale='en_US')
118 u'US Dollar'
120 .. versionadded:: 0.9.4
122 :param currency: the currency code.
123 :param count: the optional count. If provided the currency name
124 will be pluralized to that number if possible.
125 :param locale: the `Locale` object or locale identifier.
126 """
127 loc = Locale.parse(locale)
128 if count is not None:
129 try:
130 plural_form = loc.plural_form(count)
131 except (OverflowError, ValueError):
132 plural_form = 'other'
133 plural_names = loc._data['currency_names_plural']
134 if currency in plural_names:
135 currency_plural_names = plural_names[currency]
136 if plural_form in currency_plural_names:
137 return currency_plural_names[plural_form]
138 if 'other' in currency_plural_names:
139 return currency_plural_names['other']
140 return loc.currencies.get(currency, currency)
143def get_currency_symbol(currency: str, locale: Locale | str | None = LC_NUMERIC) -> str:
144 """Return the symbol used by the locale for the specified currency.
146 >>> get_currency_symbol('USD', locale='en_US')
147 u'$'
149 :param currency: the currency code.
150 :param locale: the `Locale` object or locale identifier.
151 """
152 return Locale.parse(locale).currency_symbols.get(currency, currency)
155def get_currency_precision(currency: str) -> int:
156 """Return currency's precision.
158 Precision is the number of decimals found after the decimal point in the
159 currency's format pattern.
161 .. versionadded:: 2.5.0
163 :param currency: the currency code.
164 """
165 precisions = get_global('currency_fractions')
166 return precisions.get(currency, precisions['DEFAULT'])[0]
169def get_currency_unit_pattern(
170 currency: str,
171 count: float | decimal.Decimal | None = None,
172 locale: Locale | str | None = LC_NUMERIC,
173) -> str:
174 """
175 Return the unit pattern used for long display of a currency value
176 for a given locale.
177 This is a string containing ``{0}`` where the numeric part
178 should be substituted and ``{1}`` where the currency long display
179 name should be substituted.
181 >>> get_currency_unit_pattern('USD', locale='en_US', count=10)
182 u'{0} {1}'
184 .. versionadded:: 2.7.0
186 :param currency: the currency code.
187 :param count: the optional count. If provided the unit
188 pattern for that number will be returned.
189 :param locale: the `Locale` object or locale identifier.
190 """
191 loc = Locale.parse(locale)
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_decimal_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
323 """Return the symbol used by the locale to separate decimal fractions.
325 >>> get_decimal_symbol('en_US')
326 u'.'
328 :param locale: the `Locale` object or locale identifier
329 """
330 return Locale.parse(locale).number_symbols.get('decimal', '.')
333def get_plus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
334 """Return the plus sign symbol used by the current locale.
336 >>> get_plus_sign_symbol('en_US')
337 u'+'
339 :param locale: the `Locale` object or locale identifier
340 """
341 return Locale.parse(locale).number_symbols.get('plusSign', '+')
344def get_minus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
345 """Return the plus sign symbol used by the current locale.
347 >>> get_minus_sign_symbol('en_US')
348 u'-'
350 :param locale: the `Locale` object or locale identifier
351 """
352 return Locale.parse(locale).number_symbols.get('minusSign', '-')
355def get_exponential_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
356 """Return the symbol used by the locale to separate mantissa and exponent.
358 >>> get_exponential_symbol('en_US')
359 u'E'
361 :param locale: the `Locale` object or locale identifier
362 """
363 return Locale.parse(locale).number_symbols.get('exponential', 'E')
366def get_group_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
367 """Return the symbol used by the locale to separate groups of thousands.
369 >>> get_group_symbol('en_US')
370 u','
372 :param locale: the `Locale` object or locale identifier
373 """
374 return Locale.parse(locale).number_symbols.get('group', ',')
377def get_infinity_symbol(locale: Locale | str | None = LC_NUMERIC) -> str:
378 """Return the symbol used by the locale to represent infinity.
380 >>> get_infinity_symbol('en_US')
381 u'∞'
383 :param locale: the `Locale` object or locale identifier
384 """
385 return Locale.parse(locale).number_symbols.get('infinity', '∞')
388def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = LC_NUMERIC) -> str:
389 """Return the given number formatted for a specific locale.
391 >>> format_number(1099, locale='en_US') # doctest: +SKIP
392 u'1,099'
393 >>> format_number(1099, locale='de_DE') # doctest: +SKIP
394 u'1.099'
396 .. deprecated:: 2.6.0
398 Use babel.numbers.format_decimal() instead.
400 :param number: the number to format
401 :param locale: the `Locale` object or locale identifier
404 """
405 warnings.warn('Use babel.numbers.format_decimal() instead.', DeprecationWarning)
406 return format_decimal(number, locale=locale)
409def get_decimal_precision(number: decimal.Decimal) -> int:
410 """Return maximum precision of a decimal instance's fractional part.
412 Precision is extracted from the fractional part only.
413 """
414 # Copied from: https://github.com/mahmoud/boltons/pull/59
415 assert isinstance(number, decimal.Decimal)
416 decimal_tuple = number.normalize().as_tuple()
417 # Note: DecimalTuple.exponent can be 'n' (qNaN), 'N' (sNaN), or 'F' (Infinity)
418 if not isinstance(decimal_tuple.exponent, int) or decimal_tuple.exponent >= 0:
419 return 0
420 return abs(decimal_tuple.exponent)
423def get_decimal_quantum(precision: int | decimal.Decimal) -> decimal.Decimal:
424 """Return minimal quantum of a number, as defined by precision."""
425 assert isinstance(precision, (int, decimal.Decimal))
426 return decimal.Decimal(10) ** (-precision)
429def format_decimal(
430 number: float | decimal.Decimal | str,
431 format: str | NumberPattern | None = None,
432 locale: Locale | str | None = LC_NUMERIC,
433 decimal_quantization: bool = True,
434 group_separator: bool = True,
435) -> str:
436 """Return the given decimal number formatted for a specific locale.
438 >>> format_decimal(1.2345, locale='en_US')
439 u'1.234'
440 >>> format_decimal(1.2346, locale='en_US')
441 u'1.235'
442 >>> format_decimal(-1.2346, locale='en_US')
443 u'-1.235'
444 >>> format_decimal(1.2345, locale='sv_SE')
445 u'1,234'
446 >>> format_decimal(1.2345, locale='de')
447 u'1,234'
449 The appropriate thousands grouping and the decimal separator are used for
450 each locale:
452 >>> format_decimal(12345.5, locale='en_US')
453 u'12,345.5'
455 By default the locale is allowed to truncate and round a high-precision
456 number by forcing its format pattern onto the decimal part. You can bypass
457 this behavior with the `decimal_quantization` parameter:
459 >>> format_decimal(1.2346, locale='en_US')
460 u'1.235'
461 >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False)
462 u'1.2346'
463 >>> format_decimal(12345.67, locale='fr_CA', group_separator=False)
464 u'12345,67'
465 >>> format_decimal(12345.67, locale='en_US', group_separator=True)
466 u'12,345.67'
468 :param number: the number to format
469 :param format:
470 :param locale: the `Locale` object or locale identifier
471 :param decimal_quantization: Truncate and round high-precision numbers to
472 the format pattern. Defaults to `True`.
473 :param group_separator: Boolean to switch group separator on/off in a locale's
474 number format.
475 """
476 locale = Locale.parse(locale)
477 if format is None:
478 format = locale.decimal_formats[format]
479 pattern = parse_pattern(format)
480 return pattern.apply(
481 number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)
484def format_compact_decimal(
485 number: float | decimal.Decimal | str,
486 *,
487 format_type: Literal["short", "long"] = "short",
488 locale: Locale | str | None = LC_NUMERIC,
489 fraction_digits: int = 0,
490) -> str:
491 """Return the given decimal number formatted for a specific locale in compact form.
493 >>> format_compact_decimal(12345, format_type="short", locale='en_US')
494 u'12K'
495 >>> format_compact_decimal(12345, format_type="long", locale='en_US')
496 u'12 thousand'
497 >>> format_compact_decimal(12345, format_type="short", locale='en_US', fraction_digits=2)
498 u'12.34K'
499 >>> format_compact_decimal(1234567, format_type="short", locale="ja_JP")
500 u'123万'
501 >>> format_compact_decimal(2345678, format_type="long", locale="mk")
502 u'2 милиони'
503 >>> format_compact_decimal(21000000, format_type="long", locale="mk")
504 u'21 милион'
506 :param number: the number to format
507 :param format_type: Compact format to use ("short" or "long")
508 :param locale: the `Locale` object or locale identifier
509 :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
510 """
511 locale = Locale.parse(locale)
512 compact_format = locale.compact_decimal_formats[format_type]
513 number, format = _get_compact_format(number, compact_format, locale, fraction_digits)
514 # Did not find a format, fall back.
515 if format is None:
516 format = locale.decimal_formats[None]
517 pattern = parse_pattern(format)
518 return pattern.apply(number, locale, decimal_quantization=False)
521def _get_compact_format(
522 number: float | decimal.Decimal | str,
523 compact_format: LocaleDataDict,
524 locale: Locale,
525 fraction_digits: int,
526) -> tuple[decimal.Decimal, NumberPattern | None]:
527 """Returns the number after dividing by the unit and the format pattern to use.
528 The algorithm is described here:
529 https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats.
530 """
531 if not isinstance(number, decimal.Decimal):
532 number = decimal.Decimal(str(number))
533 if number.is_nan() or number.is_infinite():
534 return number, None
535 format = None
536 for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True):
537 if abs(number) >= magnitude:
538 # check the pattern using "other" as the amount
539 format = compact_format["other"][str(magnitude)]
540 pattern = parse_pattern(format).pattern
541 # if the pattern is "0", we do not divide the number
542 if pattern == "0":
543 break
544 # otherwise, we need to divide the number by the magnitude but remove zeros
545 # equal to the number of 0's in the pattern minus 1
546 number = cast(decimal.Decimal, number / (magnitude // (10 ** (pattern.count("0") - 1))))
547 # round to the number of fraction digits requested
548 rounded = round(number, fraction_digits)
549 # if the remaining number is singular, use the singular format
550 plural_form = locale.plural_form(abs(number))
551 if plural_form not in compact_format:
552 plural_form = "other"
553 if number == 1 and "1" in compact_format:
554 plural_form = "1"
555 format = compact_format[plural_form][str(magnitude)]
556 number = rounded
557 break
558 return number, format
561class UnknownCurrencyFormatError(KeyError):
562 """Exception raised when an unknown currency format is requested."""
565def format_currency(
566 number: float | decimal.Decimal | str,
567 currency: str,
568 format: str | NumberPattern | None = None,
569 locale: Locale | str | None = LC_NUMERIC,
570 currency_digits: bool = True,
571 format_type: Literal["name", "standard", "accounting"] = "standard",
572 decimal_quantization: bool = True,
573 group_separator: bool = True,
574) -> str:
575 """Return formatted currency value.
577 >>> format_currency(1099.98, 'USD', locale='en_US')
578 '$1,099.98'
579 >>> format_currency(1099.98, 'USD', locale='es_CO')
580 u'US$1.099,98'
581 >>> format_currency(1099.98, 'EUR', locale='de_DE')
582 u'1.099,98\\xa0\\u20ac'
584 The format can also be specified explicitly. The currency is
585 placed with the '¤' sign. As the sign gets repeated the format
586 expands (¤ being the symbol, ¤¤ is the currency abbreviation and
587 ¤¤¤ is the full name of the currency):
589 >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US')
590 u'EUR 1,099.98'
591 >>> format_currency(1099.98, 'EUR', u'#,##0.00 \xa4\xa4\xa4', locale='en_US')
592 u'1,099.98 euros'
594 Currencies usually have a specific number of decimal digits. This function
595 favours that information over the given format:
597 >>> format_currency(1099.98, 'JPY', locale='en_US')
598 u'\\xa51,100'
599 >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES')
600 u'1.099,98'
602 However, the number of decimal digits can be overridden from the currency
603 information, by setting the last parameter to ``False``:
605 >>> format_currency(1099.98, 'JPY', locale='en_US', currency_digits=False)
606 u'\\xa51,099.98'
607 >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES', currency_digits=False)
608 u'1.099,98'
610 If a format is not specified the type of currency format to use
611 from the locale can be specified:
613 >>> format_currency(1099.98, 'EUR', locale='en_US', format_type='standard')
614 u'\\u20ac1,099.98'
616 When the given currency format type is not available, an exception is
617 raised:
619 >>> format_currency('1099.98', 'EUR', locale='root', format_type='unknown')
620 Traceback (most recent call last):
621 ...
622 UnknownCurrencyFormatError: "'unknown' is not a known currency format type"
624 >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False)
625 u'$101299.98'
627 >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True)
628 u'$101,299.98'
630 You can also pass format_type='name' to use long display names. The order of
631 the number and currency name, along with the correct localized plural form
632 of the currency name, is chosen according to locale:
634 >>> format_currency(1, 'USD', locale='en_US', format_type='name')
635 u'1.00 US dollar'
636 >>> format_currency(1099.98, 'USD', locale='en_US', format_type='name')
637 u'1,099.98 US dollars'
638 >>> format_currency(1099.98, 'USD', locale='ee', format_type='name')
639 u'us ga dollar 1,099.98'
641 By default the locale is allowed to truncate and round a high-precision
642 number by forcing its format pattern onto the decimal part. You can bypass
643 this behavior with the `decimal_quantization` parameter:
645 >>> format_currency(1099.9876, 'USD', locale='en_US')
646 u'$1,099.99'
647 >>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False)
648 u'$1,099.9876'
650 :param number: the number to format
651 :param currency: the currency code
652 :param format: the format string to use
653 :param locale: the `Locale` object or locale identifier
654 :param currency_digits: use the currency's natural number of decimal digits
655 :param format_type: the currency format type to use
656 :param decimal_quantization: Truncate and round high-precision numbers to
657 the format pattern. Defaults to `True`.
658 :param group_separator: Boolean to switch group separator on/off in a locale's
659 number format.
661 """
662 if format_type == 'name':
663 return _format_currency_long_name(number, currency, format=format,
664 locale=locale, currency_digits=currency_digits,
665 decimal_quantization=decimal_quantization, group_separator=group_separator)
666 locale = Locale.parse(locale)
667 if format:
668 pattern = parse_pattern(format)
669 else:
670 try:
671 pattern = locale.currency_formats[format_type]
672 except KeyError:
673 raise UnknownCurrencyFormatError(f"{format_type!r} is not a known currency format type") from None
675 return pattern.apply(
676 number, locale, currency=currency, currency_digits=currency_digits,
677 decimal_quantization=decimal_quantization, group_separator=group_separator)
680def _format_currency_long_name(
681 number: float | decimal.Decimal | str,
682 currency: str,
683 format: str | NumberPattern | None = None,
684 locale: Locale | str | None = LC_NUMERIC,
685 currency_digits: bool = True,
686 format_type: Literal["name", "standard", "accounting"] = "standard",
687 decimal_quantization: bool = True,
688 group_separator: bool = True,
689) -> str:
690 # Algorithm described here:
691 # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies
692 locale = Locale.parse(locale)
693 # Step 1.
694 # There are no examples of items with explicit count (0 or 1) in current
695 # locale data. So there is no point implementing that.
696 # Step 2.
698 # Correct number to numeric type, important for looking up plural rules:
699 number_n = float(number) if isinstance(number, str) else number
701 # Step 3.
702 unit_pattern = get_currency_unit_pattern(currency, count=number_n, locale=locale)
704 # Step 4.
705 display_name = get_currency_name(currency, count=number_n, locale=locale)
707 # Step 5.
708 if not format:
709 format = locale.decimal_formats[format]
711 pattern = parse_pattern(format)
713 number_part = pattern.apply(
714 number, locale, currency=currency, currency_digits=currency_digits,
715 decimal_quantization=decimal_quantization, group_separator=group_separator)
717 return unit_pattern.format(number_part, display_name)
720def format_compact_currency(
721 number: float | decimal.Decimal | str,
722 currency: str,
723 *,
724 format_type: Literal["short"] = "short",
725 locale: Locale | str | None = LC_NUMERIC,
726 fraction_digits: int = 0
727) -> str:
728 """Format a number as a currency value in compact form.
730 >>> format_compact_currency(12345, 'USD', locale='en_US')
731 u'$12K'
732 >>> format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2)
733 u'$123.46M'
734 >>> format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1)
735 '123,5\xa0Mio.\xa0€'
737 :param number: the number to format
738 :param currency: the currency code
739 :param format_type: the compact format type to use. Defaults to "short".
740 :param locale: the `Locale` object or locale identifier
741 :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
742 """
743 locale = Locale.parse(locale)
744 try:
745 compact_format = locale.compact_currency_formats[format_type]
746 except KeyError as error:
747 raise UnknownCurrencyFormatError(f"{format_type!r} is not a known compact currency format type") from error
748 number, format = _get_compact_format(number, compact_format, locale, fraction_digits)
749 # Did not find a format, fall back.
750 if format is None or "¤" not in str(format):
751 # find first format that has a currency symbol
752 for magnitude in compact_format['other']:
753 format = compact_format['other'][magnitude].pattern
754 if '¤' not in format:
755 continue
756 # remove characters that are not the currency symbol, 0's or spaces
757 format = re.sub(r'[^0\s\¤]', '', format)
758 # compress adjacent spaces into one
759 format = re.sub(r'(\s)\s+', r'\1', format).strip()
760 break
761 if format is None:
762 raise ValueError('No compact currency format found for the given number and locale.')
763 pattern = parse_pattern(format)
764 return pattern.apply(number, locale, currency=currency, currency_digits=False, decimal_quantization=False)
767def format_percent(
768 number: float | decimal.Decimal | str,
769 format: str | NumberPattern | None = None,
770 locale: Locale | str | None = LC_NUMERIC,
771 decimal_quantization: bool = True,
772 group_separator: bool = True,
773) -> str:
774 """Return formatted percent value for a specific locale.
776 >>> format_percent(0.34, locale='en_US')
777 u'34%'
778 >>> format_percent(25.1234, locale='en_US')
779 u'2,512%'
780 >>> format_percent(25.1234, locale='sv_SE')
781 u'2\\xa0512\\xa0%'
783 The format pattern can also be specified explicitly:
785 >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US')
786 u'25,123\u2030'
788 By default the locale is allowed to truncate and round a high-precision
789 number by forcing its format pattern onto the decimal part. You can bypass
790 this behavior with the `decimal_quantization` parameter:
792 >>> format_percent(23.9876, locale='en_US')
793 u'2,399%'
794 >>> format_percent(23.9876, locale='en_US', decimal_quantization=False)
795 u'2,398.76%'
797 >>> format_percent(229291.1234, locale='pt_BR', group_separator=False)
798 u'22929112%'
800 >>> format_percent(229291.1234, locale='pt_BR', group_separator=True)
801 u'22.929.112%'
803 :param number: the percent number to format
804 :param format:
805 :param locale: the `Locale` object or locale identifier
806 :param decimal_quantization: Truncate and round high-precision numbers to
807 the format pattern. Defaults to `True`.
808 :param group_separator: Boolean to switch group separator on/off in a locale's
809 number format.
810 """
811 locale = Locale.parse(locale)
812 if not format:
813 format = locale.percent_formats[format]
814 pattern = parse_pattern(format)
815 return pattern.apply(
816 number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator)
819def format_scientific(
820 number: float | decimal.Decimal | str,
821 format: str | NumberPattern | None = None,
822 locale: Locale | str | None = LC_NUMERIC,
823 decimal_quantization: bool = True,
824) -> str:
825 """Return value formatted in scientific notation for a specific locale.
827 >>> format_scientific(10000, locale='en_US')
828 u'1E4'
830 The format pattern can also be specified explicitly:
832 >>> format_scientific(1234567, u'##0.##E00', locale='en_US')
833 u'1.23E06'
835 By default the locale is allowed to truncate and round a high-precision
836 number by forcing its format pattern onto the decimal part. You can bypass
837 this behavior with the `decimal_quantization` parameter:
839 >>> format_scientific(1234.9876, u'#.##E0', locale='en_US')
840 u'1.23E3'
841 >>> format_scientific(1234.9876, u'#.##E0', locale='en_US', decimal_quantization=False)
842 u'1.2349876E3'
844 :param number: the number to format
845 :param format:
846 :param locale: the `Locale` object or locale identifier
847 :param decimal_quantization: Truncate and round high-precision numbers to
848 the format pattern. Defaults to `True`.
849 """
850 locale = Locale.parse(locale)
851 if not format:
852 format = locale.scientific_formats[format]
853 pattern = parse_pattern(format)
854 return pattern.apply(
855 number, locale, decimal_quantization=decimal_quantization)
858class NumberFormatError(ValueError):
859 """Exception raised when a string cannot be parsed into a number."""
861 def __init__(self, message: str, suggestions: list[str] | None = None) -> None:
862 super().__init__(message)
863 #: a list of properly formatted numbers derived from the invalid input
864 self.suggestions = suggestions
867def parse_number(string: str, locale: Locale | str | None = LC_NUMERIC) -> int:
868 """Parse localized number string into an integer.
870 >>> parse_number('1,099', locale='en_US')
871 1099
872 >>> parse_number('1.099', locale='de_DE')
873 1099
875 When the given string cannot be parsed, an exception is raised:
877 >>> parse_number('1.099,98', locale='de')
878 Traceback (most recent call last):
879 ...
880 NumberFormatError: '1.099,98' is not a valid number
882 :param string: the string to parse
883 :param locale: the `Locale` object or locale identifier
884 :return: the parsed number
885 :raise `NumberFormatError`: if the string can not be converted to a number
886 """
887 try:
888 return int(string.replace(get_group_symbol(locale), ''))
889 except ValueError as ve:
890 raise NumberFormatError(f"{string!r} is not a valid number") from ve
893def parse_decimal(string: str, locale: Locale | str | None = LC_NUMERIC, strict: bool = False) -> decimal.Decimal:
894 """Parse localized decimal string into a decimal.
896 >>> parse_decimal('1,099.98', locale='en_US')
897 Decimal('1099.98')
898 >>> parse_decimal('1.099,98', locale='de')
899 Decimal('1099.98')
900 >>> parse_decimal('12 345,123', locale='ru')
901 Decimal('12345.123')
903 When the given string cannot be parsed, an exception is raised:
905 >>> parse_decimal('2,109,998', locale='de')
906 Traceback (most recent call last):
907 ...
908 NumberFormatError: '2,109,998' is not a valid decimal number
910 If `strict` is set to `True` and the given string contains a number
911 formatted in an irregular way, an exception is raised:
913 >>> parse_decimal('30.00', locale='de', strict=True)
914 Traceback (most recent call last):
915 ...
916 NumberFormatError: '30.00' is not a properly formatted decimal number. Did you mean '3.000'? Or maybe '30,00'?
918 >>> parse_decimal('0.00', locale='de', strict=True)
919 Traceback (most recent call last):
920 ...
921 NumberFormatError: '0.00' is not a properly formatted decimal number. Did you mean '0'?
923 :param string: the string to parse
924 :param locale: the `Locale` object or locale identifier
925 :param strict: controls whether numbers formatted in a weird way are
926 accepted or rejected
927 :raise NumberFormatError: if the string can not be converted to a
928 decimal number
929 """
930 locale = Locale.parse(locale)
931 group_symbol = get_group_symbol(locale)
932 decimal_symbol = get_decimal_symbol(locale)
934 if not strict and (
935 group_symbol == '\xa0' and # if the grouping symbol is U+00A0 NO-BREAK SPACE,
936 group_symbol not in string and # and the string to be parsed does not contain it,
937 ' ' in string # but it does contain a space instead,
938 ):
939 # ... it's reasonable to assume it is taking the place of the grouping symbol.
940 string = string.replace(' ', group_symbol)
942 try:
943 parsed = decimal.Decimal(string.replace(group_symbol, '')
944 .replace(decimal_symbol, '.'))
945 except decimal.InvalidOperation as exc:
946 raise NumberFormatError(f"{string!r} is not a valid decimal number") from exc
947 if strict and group_symbol in string:
948 proper = format_decimal(parsed, locale=locale, decimal_quantization=False)
949 if string != proper and string.rstrip('0') != (proper + decimal_symbol):
950 try:
951 parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '')
952 .replace(group_symbol, '.'))
953 except decimal.InvalidOperation as exc:
954 raise NumberFormatError(
955 f"{string!r} is not a properly formatted decimal number. "
956 f"Did you mean {proper!r}?",
957 suggestions=[proper],
958 ) from exc
959 else:
960 proper_alt = format_decimal(parsed_alt, locale=locale, decimal_quantization=False)
961 if proper_alt == proper:
962 raise NumberFormatError(
963 f"{string!r} is not a properly formatted decimal number. "
964 f"Did you mean {proper!r}?",
965 suggestions=[proper],
966 )
967 else:
968 raise NumberFormatError(
969 f"{string!r} is not a properly formatted decimal number. "
970 f"Did you mean {proper!r}? Or maybe {proper_alt!r}?",
971 suggestions=[proper, proper_alt],
972 )
973 return parsed
976PREFIX_END = r'[^0-9@#.,]'
977NUMBER_TOKEN = r'[0-9@#.,E+]'
979PREFIX_PATTERN = r"(?P<prefix>(?:'[^']*'|%s)*)" % PREFIX_END
980NUMBER_PATTERN = r"(?P<number>%s*)" % NUMBER_TOKEN
981SUFFIX_PATTERN = r"(?P<suffix>.*)"
983number_re = re.compile(f"{PREFIX_PATTERN}{NUMBER_PATTERN}{SUFFIX_PATTERN}")
986def parse_grouping(p: str) -> tuple[int, int]:
987 """Parse primary and secondary digit grouping
989 >>> parse_grouping('##')
990 (1000, 1000)
991 >>> parse_grouping('#,###')
992 (3, 3)
993 >>> parse_grouping('#,####,###')
994 (3, 4)
995 """
996 width = len(p)
997 g1 = p.rfind(',')
998 if g1 == -1:
999 return 1000, 1000
1000 g1 = width - g1 - 1
1001 g2 = p[:-g1 - 1].rfind(',')
1002 if g2 == -1:
1003 return g1, g1
1004 g2 = width - g1 - g2 - 2
1005 return g1, g2
1008def parse_pattern(pattern: NumberPattern | str) -> NumberPattern:
1009 """Parse number format patterns"""
1010 if isinstance(pattern, NumberPattern):
1011 return pattern
1013 def _match_number(pattern):
1014 rv = number_re.search(pattern)
1015 if rv is None:
1016 raise ValueError(f"Invalid number pattern {pattern!r}")
1017 return rv.groups()
1019 pos_pattern = pattern
1021 # Do we have a negative subpattern?
1022 if ';' in pattern:
1023 pos_pattern, neg_pattern = pattern.split(';', 1)
1024 pos_prefix, number, pos_suffix = _match_number(pos_pattern)
1025 neg_prefix, _, neg_suffix = _match_number(neg_pattern)
1026 else:
1027 pos_prefix, number, pos_suffix = _match_number(pos_pattern)
1028 neg_prefix = f"-{pos_prefix}"
1029 neg_suffix = pos_suffix
1030 if 'E' in number:
1031 number, exp = number.split('E', 1)
1032 else:
1033 exp = None
1034 if '@' in number and '.' in number and '0' in number:
1035 raise ValueError('Significant digit patterns can not contain "@" or "0"')
1036 if '.' in number:
1037 integer, fraction = number.rsplit('.', 1)
1038 else:
1039 integer = number
1040 fraction = ''
1042 def parse_precision(p):
1043 """Calculate the min and max allowed digits"""
1044 min = max = 0
1045 for c in p:
1046 if c in '@0':
1047 min += 1
1048 max += 1
1049 elif c == '#':
1050 max += 1
1051 elif c == ',':
1052 continue
1053 else:
1054 break
1055 return min, max
1057 int_prec = parse_precision(integer)
1058 frac_prec = parse_precision(fraction)
1059 if exp:
1060 exp_plus = exp.startswith('+')
1061 exp = exp.lstrip('+')
1062 exp_prec = parse_precision(exp)
1063 else:
1064 exp_plus = None
1065 exp_prec = None
1066 grouping = parse_grouping(integer)
1067 return NumberPattern(pattern, (pos_prefix, neg_prefix),
1068 (pos_suffix, neg_suffix), grouping,
1069 int_prec, frac_prec,
1070 exp_prec, exp_plus, number)
1073class NumberPattern:
1075 def __init__(
1076 self,
1077 pattern: str,
1078 prefix: tuple[str, str],
1079 suffix: tuple[str, str],
1080 grouping: tuple[int, int],
1081 int_prec: tuple[int, int],
1082 frac_prec: tuple[int, int],
1083 exp_prec: tuple[int, int] | None,
1084 exp_plus: bool | None,
1085 number_pattern: str | None = None,
1086 ) -> None:
1087 # Metadata of the decomposed parsed pattern.
1088 self.pattern = pattern
1089 self.prefix = prefix
1090 self.suffix = suffix
1091 self.number_pattern = number_pattern
1092 self.grouping = grouping
1093 self.int_prec = int_prec
1094 self.frac_prec = frac_prec
1095 self.exp_prec = exp_prec
1096 self.exp_plus = exp_plus
1097 self.scale = self.compute_scale()
1099 def __repr__(self) -> str:
1100 return f"<{type(self).__name__} {self.pattern!r}>"
1102 def compute_scale(self) -> Literal[0, 2, 3]:
1103 """Return the scaling factor to apply to the number before rendering.
1105 Auto-set to a factor of 2 or 3 if presence of a ``%`` or ``‰`` sign is
1106 detected in the prefix or suffix of the pattern. Default is to not mess
1107 with the scale at all and keep it to 0.
1108 """
1109 scale = 0
1110 if '%' in ''.join(self.prefix + self.suffix):
1111 scale = 2
1112 elif '‰' in ''.join(self.prefix + self.suffix):
1113 scale = 3
1114 return scale
1116 def scientific_notation_elements(self, value: decimal.Decimal, locale: Locale | str | None) -> tuple[decimal.Decimal, int, str]:
1117 """ Returns normalized scientific notation components of a value.
1118 """
1119 # Normalize value to only have one lead digit.
1120 exp = value.adjusted()
1121 value = value * get_decimal_quantum(exp)
1122 assert value.adjusted() == 0
1124 # Shift exponent and value by the minimum number of leading digits
1125 # imposed by the rendering pattern. And always make that number
1126 # greater or equal to 1.
1127 lead_shift = max([1, min(self.int_prec)]) - 1
1128 exp = exp - lead_shift
1129 value = value * get_decimal_quantum(-lead_shift)
1131 # Get exponent sign symbol.
1132 exp_sign = ''
1133 if exp < 0:
1134 exp_sign = get_minus_sign_symbol(locale)
1135 elif self.exp_plus:
1136 exp_sign = get_plus_sign_symbol(locale)
1138 # Normalize exponent value now that we have the sign.
1139 exp = abs(exp)
1141 return value, exp, exp_sign
1143 def apply(
1144 self,
1145 value: float | decimal.Decimal | str,
1146 locale: Locale | str | None,
1147 currency: str | None = None,
1148 currency_digits: bool = True,
1149 decimal_quantization: bool = True,
1150 force_frac: tuple[int, int] | None = None,
1151 group_separator: bool = True,
1152 ):
1153 """Renders into a string a number following the defined pattern.
1155 Forced decimal quantization is active by default so we'll produce a
1156 number string that is strictly following CLDR pattern definitions.
1158 :param value: The value to format. If this is not a Decimal object,
1159 it will be cast to one.
1160 :type value: decimal.Decimal|float|int
1161 :param locale: The locale to use for formatting.
1162 :type locale: str|babel.core.Locale
1163 :param currency: Which currency, if any, to format as.
1164 :type currency: str|None
1165 :param currency_digits: Whether or not to use the currency's precision.
1166 If false, the pattern's precision is used.
1167 :type currency_digits: bool
1168 :param decimal_quantization: Whether decimal numbers should be forcibly
1169 quantized to produce a formatted output
1170 strictly matching the CLDR definition for
1171 the locale.
1172 :type decimal_quantization: bool
1173 :param force_frac: DEPRECATED - a forced override for `self.frac_prec`
1174 for a single formatting invocation.
1175 :return: Formatted decimal string.
1176 :rtype: str
1177 """
1178 if not isinstance(value, decimal.Decimal):
1179 value = decimal.Decimal(str(value))
1181 value = value.scaleb(self.scale)
1183 # Separate the absolute value from its sign.
1184 is_negative = int(value.is_signed())
1185 value = abs(value).normalize()
1187 # Prepare scientific notation metadata.
1188 if self.exp_prec:
1189 value, exp, exp_sign = self.scientific_notation_elements(value, locale)
1191 # Adjust the precision of the fractional part and force it to the
1192 # currency's if necessary.
1193 if force_frac:
1194 # TODO (3.x?): Remove this parameter
1195 warnings.warn('The force_frac parameter to NumberPattern.apply() is deprecated.', DeprecationWarning)
1196 frac_prec = force_frac
1197 elif currency and currency_digits:
1198 frac_prec = (get_currency_precision(currency), ) * 2
1199 else:
1200 frac_prec = self.frac_prec
1202 # Bump decimal precision to the natural precision of the number if it
1203 # exceeds the one we're about to use. This adaptative precision is only
1204 # triggered if the decimal quantization is disabled or if a scientific
1205 # notation pattern has a missing mandatory fractional part (as in the
1206 # default '#E0' pattern). This special case has been extensively
1207 # discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 .
1208 if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)):
1209 frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)]))
1211 # Render scientific notation.
1212 if self.exp_prec:
1213 number = ''.join([
1214 self._quantize_value(value, locale, frac_prec, group_separator),
1215 get_exponential_symbol(locale),
1216 exp_sign, # type: ignore # exp_sign is always defined here
1217 self._format_int(str(exp), self.exp_prec[0], self.exp_prec[1], locale) # type: ignore # exp is always defined here
1218 ])
1220 # Is it a significant digits pattern?
1221 elif '@' in self.pattern:
1222 text = self._format_significant(value,
1223 self.int_prec[0],
1224 self.int_prec[1])
1225 a, sep, b = text.partition(".")
1226 number = self._format_int(a, 0, 1000, locale)
1227 if sep:
1228 number += get_decimal_symbol(locale) + b
1230 # A normal number pattern.
1231 else:
1232 number = self._quantize_value(value, locale, frac_prec, group_separator)
1234 retval = ''.join([
1235 self.prefix[is_negative],
1236 number if self.number_pattern != '' else '',
1237 self.suffix[is_negative]])
1239 if '¤' in retval and currency is not None:
1240 retval = retval.replace('¤¤¤', get_currency_name(currency, value, locale))
1241 retval = retval.replace('¤¤', currency.upper())
1242 retval = retval.replace('¤', get_currency_symbol(currency, locale))
1244 # remove single quotes around text, except for doubled single quotes
1245 # which are replaced with a single quote
1246 retval = re.sub(r"'([^']*)'", lambda m: m.group(1) or "'", retval)
1248 return retval
1250 #
1251 # This is one tricky piece of code. The idea is to rely as much as possible
1252 # on the decimal module to minimize the amount of code.
1253 #
1254 # Conceptually, the implementation of this method can be summarized in the
1255 # following steps:
1256 #
1257 # - Move or shift the decimal point (i.e. the exponent) so the maximum
1258 # amount of significant digits fall into the integer part (i.e. to the
1259 # left of the decimal point)
1260 #
1261 # - Round the number to the nearest integer, discarding all the fractional
1262 # part which contained extra digits to be eliminated
1263 #
1264 # - Convert the rounded integer to a string, that will contain the final
1265 # sequence of significant digits already trimmed to the maximum
1266 #
1267 # - Restore the original position of the decimal point, potentially
1268 # padding with zeroes on either side
1269 #
1270 def _format_significant(self, value: decimal.Decimal, minimum: int, maximum: int) -> str:
1271 exp = value.adjusted()
1272 scale = maximum - 1 - exp
1273 digits = str(value.scaleb(scale).quantize(decimal.Decimal(1)))
1274 if scale <= 0:
1275 result = digits + '0' * -scale
1276 else:
1277 intpart = digits[:-scale]
1278 i = len(intpart)
1279 j = i + max(minimum - i, 0)
1280 result = "{intpart}.{pad:0<{fill}}{fracpart}{fracextra}".format(
1281 intpart=intpart or '0',
1282 pad='',
1283 fill=-min(exp + 1, 0),
1284 fracpart=digits[i:j],
1285 fracextra=digits[j:].rstrip('0'),
1286 ).rstrip('.')
1287 return result
1289 def _format_int(self, value: str, min: int, max: int, locale: Locale | str | None) -> str:
1290 width = len(value)
1291 if width < min:
1292 value = '0' * (min - width) + value
1293 gsize = self.grouping[0]
1294 ret = ''
1295 symbol = get_group_symbol(locale)
1296 while len(value) > gsize:
1297 ret = symbol + value[-gsize:] + ret
1298 value = value[:-gsize]
1299 gsize = self.grouping[1]
1300 return value + ret
1302 def _quantize_value(self, value: decimal.Decimal, locale: Locale | str | None, frac_prec: tuple[int, int], group_separator: bool) -> str:
1303 # If the number is +/-Infinity, we can't quantize it
1304 if value.is_infinite():
1305 return get_infinity_symbol(locale)
1306 quantum = get_decimal_quantum(frac_prec[1])
1307 rounded = value.quantize(quantum)
1308 a, sep, b = f"{rounded:f}".partition(".")
1309 integer_part = a
1310 if group_separator:
1311 integer_part = self._format_int(a, self.int_prec[0], self.int_prec[1], locale)
1312 number = integer_part + self._format_frac(b or '0', locale, frac_prec)
1313 return number
1315 def _format_frac(self, value: str, locale: Locale | str | None, force_frac: tuple[int, int] | None = None) -> str:
1316 min, max = force_frac or self.frac_prec
1317 if len(value) < min:
1318 value += ('0' * (min - len(value)))
1319 if max == 0 or (min == 0 and int(value) == 0):
1320 return ''
1321 while len(value) > min and value[-1] == '0':
1322 value = value[:-1]
1323 return get_decimal_symbol(locale) + value