Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/babel/numbers.py: 17%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

442 statements  

1""" 

2 babel.numbers 

3 ~~~~~~~~~~~~~ 

4 

5 Locale dependent formatting and parsing of numeric data. 

6 

7 The default locale for the functions in this module is determined by the 

8 following environment variables, in that order: 

9 

10 * ``LC_MONETARY`` for currency related functions, 

11 * ``LC_NUMERIC``, and 

12 * ``LC_ALL``, and 

13 * ``LANG`` 

14 

15 :copyright: (c) 2013-2025 by the Babel Team. 

16 :license: BSD, see LICENSE for more details. 

17""" 

18# TODO: 

19# Padding and rounding increments in pattern: 

20# - https://www.unicode.org/reports/tr35/ (Appendix G.6) 

21from __future__ import annotations 

22 

23import datetime 

24import decimal 

25import re 

26import warnings 

27from typing import Any, Literal, cast, overload 

28 

29from babel.core import Locale, default_locale, get_global 

30from babel.localedata import LocaleDataDict 

31 

32LC_MONETARY = default_locale(('LC_MONETARY', 'LC_NUMERIC')) 

33LC_NUMERIC = default_locale('LC_NUMERIC') 

34 

35 

36class UnknownCurrencyError(Exception): 

37 """Exception thrown when a currency is requested for which no data is available. 

38 """ 

39 

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}.") 

45 

46 #: The identifier of the locale that could not be found. 

47 self.identifier = identifier 

48 

49 

50def list_currencies(locale: Locale | str | None = None) -> set[str]: 

51 """ Return a `set` of normalized currency codes. 

52 

53 .. versionadded:: 2.5.0 

54 

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')) 

64 

65 

66def validate_currency(currency: str, locale: Locale | str | None = None) -> None: 

67 """ Check the currency code is recognized by Babel. 

68 

69 Accepts a ``locale`` parameter for fined-grained validation, working as 

70 the one defined above in ``list_currencies()`` method. 

71 

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) 

76 

77 

78def is_currency(currency: str, locale: Locale | str | None = None) -> bool: 

79 """ Returns `True` only if a currency is recognized by Babel. 

80 

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 

90 

91 

92def normalize_currency(currency: str, locale: Locale | str | None = None) -> str | None: 

93 """Returns the normalized identifier of any currency code. 

94 

95 Accepts a ``locale`` parameter for fined-grained validation, working as 

96 the one defined above in ``list_currencies()`` method. 

97 

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 

105 

106 

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. 

113 

114 >>> get_currency_name('USD', locale='en_US') 

115 u'US Dollar' 

116 

117 .. versionadded:: 0.9.4 

118 

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) 

139 

140 

141def get_currency_symbol(currency: str, locale: Locale | str | None = None) -> str: 

142 """Return the symbol used by the locale for the specified currency. 

143 

144 >>> get_currency_symbol('USD', locale='en_US') 

145 u'$' 

146 

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) 

152 

153 

154def get_currency_precision(currency: str) -> int: 

155 """Return currency's precision. 

156 

157 Precision is the number of decimals found after the decimal point in the 

158 currency's format pattern. 

159 

160 .. versionadded:: 2.5.0 

161 

162 :param currency: the currency code. 

163 """ 

164 precisions = get_global('currency_fractions') 

165 return precisions.get(currency, precisions['DEFAULT'])[0] 

166 

167 

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. 

179 

180 >>> get_currency_unit_pattern('USD', locale='en_US', count=10) 

181 u'{0} {1}' 

182 

183 .. versionadded:: 2.7.0 

184 

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 

199 

200 return loc._data['currency_unit_patterns']['other'] 

201 

202 

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 

213 

214 

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 

225 

226 

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. 

239 

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. 

243 

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: 

248 

249 >>> from datetime import date 

250 >>> get_territory_currencies('AT', date(1995, 1, 1), date(2011, 1, 1)) 

251 ['ATS', 'EUR'] 

252 

253 Likewise it's also possible to find all the currencies in use on a 

254 single date: 

255 

256 >>> get_territory_currencies('AT', date(1995, 1, 1)) 

257 ['ATS'] 

258 >>> get_territory_currencies('AT', date(2011, 1, 1)) 

259 ['EUR'] 

260 

261 By default the return value only includes tender currencies. This 

262 however can be changed: 

263 

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'] 

269 

270 .. versionadded:: 2.0 

271 

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() 

293 

294 curs = currencies.get(territory.upper(), ()) 

295 # TODO: validate that the territory exists 

296 

297 def _is_active(start, end): 

298 return (start is None or start <= end_date) and \ 

299 (end is None or end >= start_date) 

300 

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) 

318 

319 return result 

320 

321 

322def _get_numbering_system(locale: Locale, numbering_system: Literal["default"] | str = "latn") -> str: 

323 if numbering_system == "default": 

324 return locale.default_numbering_system 

325 else: 

326 return numbering_system 

327 

328 

329def _get_number_symbols( 

330 locale: Locale, 

331 *, 

332 numbering_system: Literal["default"] | str = "latn", 

333) -> LocaleDataDict: 

334 numbering_system = _get_numbering_system(locale, numbering_system) 

335 try: 

336 return locale.number_symbols[numbering_system] 

337 except KeyError as error: 

338 raise UnsupportedNumberingSystemError(f"Unknown numbering system {numbering_system} for Locale {locale}.") from error 

339 

340 

341class UnsupportedNumberingSystemError(Exception): 

342 """Exception thrown when an unsupported numbering system is requested for the given Locale.""" 

343 pass 

344 

345 

346def get_decimal_symbol( 

347 locale: Locale | str | None = None, 

348 *, 

349 numbering_system: Literal["default"] | str = "latn", 

350) -> str: 

351 """Return the symbol used by the locale to separate decimal fractions. 

352 

353 >>> get_decimal_symbol('en_US') 

354 u'.' 

355 >>> get_decimal_symbol('ar_EG', numbering_system='default') 

356 u'٫' 

357 >>> get_decimal_symbol('ar_EG', numbering_system='latn') 

358 u'.' 

359 

360 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

361 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". 

362 The special value "default" will use the default numbering system of the locale. 

363 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. 

364 """ 

365 locale = Locale.parse(locale or LC_NUMERIC) 

366 return _get_number_symbols(locale, numbering_system=numbering_system).get('decimal', '.') 

367 

368 

369def get_plus_sign_symbol( 

370 locale: Locale | str | None = None, 

371 *, 

372 numbering_system: Literal["default"] | str = "latn", 

373) -> str: 

374 """Return the plus sign symbol used by the current locale. 

375 

376 >>> get_plus_sign_symbol('en_US') 

377 u'+' 

378 >>> get_plus_sign_symbol('ar_EG', numbering_system='default') 

379 u'\u061c+' 

380 >>> get_plus_sign_symbol('ar_EG', numbering_system='latn') 

381 u'\u200e+' 

382 

383 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

384 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". 

385 The special value "default" will use the default numbering system of the locale. 

386 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. 

387 """ 

388 locale = Locale.parse(locale or LC_NUMERIC) 

389 return _get_number_symbols(locale, numbering_system=numbering_system).get('plusSign', '+') 

390 

391 

392def get_minus_sign_symbol( 

393 locale: Locale | str | None = None, 

394 *, 

395 numbering_system: Literal["default"] | str = "latn", 

396) -> str: 

397 """Return the plus sign symbol used by the current locale. 

398 

399 >>> get_minus_sign_symbol('en_US') 

400 u'-' 

401 >>> get_minus_sign_symbol('ar_EG', numbering_system='default') 

402 u'\u061c-' 

403 >>> get_minus_sign_symbol('ar_EG', numbering_system='latn') 

404 u'\u200e-' 

405 

406 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

407 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". 

408 The special value "default" will use the default numbering system of the locale. 

409 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. 

410 """ 

411 locale = Locale.parse(locale or LC_NUMERIC) 

412 return _get_number_symbols(locale, numbering_system=numbering_system).get('minusSign', '-') 

413 

414 

415def get_exponential_symbol( 

416 locale: Locale | str | None = None, 

417 *, 

418 numbering_system: Literal["default"] | str = "latn", 

419) -> str: 

420 """Return the symbol used by the locale to separate mantissa and exponent. 

421 

422 >>> get_exponential_symbol('en_US') 

423 u'E' 

424 >>> get_exponential_symbol('ar_EG', numbering_system='default') 

425 u'أس' 

426 >>> get_exponential_symbol('ar_EG', numbering_system='latn') 

427 u'E' 

428 

429 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

430 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". 

431 The special value "default" will use the default numbering system of the locale. 

432 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. 

433 """ 

434 locale = Locale.parse(locale or LC_NUMERIC) 

435 return _get_number_symbols(locale, numbering_system=numbering_system).get('exponential', 'E') 

436 

437 

438def get_group_symbol( 

439 locale: Locale | str | None = None, 

440 *, 

441 numbering_system: Literal["default"] | str = "latn", 

442) -> str: 

443 """Return the symbol used by the locale to separate groups of thousands. 

444 

445 >>> get_group_symbol('en_US') 

446 u',' 

447 >>> get_group_symbol('ar_EG', numbering_system='default') 

448 u'٬' 

449 >>> get_group_symbol('ar_EG', numbering_system='latn') 

450 u',' 

451 

452 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

453 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". 

454 The special value "default" will use the default numbering system of the locale. 

455 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. 

456 """ 

457 locale = Locale.parse(locale or LC_NUMERIC) 

458 return _get_number_symbols(locale, numbering_system=numbering_system).get('group', ',') 

459 

460 

461def get_infinity_symbol( 

462 locale: Locale | str | None = None, 

463 *, 

464 numbering_system: Literal["default"] | str = "latn", 

465) -> str: 

466 """Return the symbol used by the locale to represent infinity. 

467 

468 >>> get_infinity_symbol('en_US') 

469 u'∞' 

470 >>> get_infinity_symbol('ar_EG', numbering_system='default') 

471 u'∞' 

472 >>> get_infinity_symbol('ar_EG', numbering_system='latn') 

473 u'∞' 

474 

475 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

476 :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn". 

477 The special value "default" will use the default numbering system of the locale. 

478 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. 

479 """ 

480 locale = Locale.parse(locale or LC_NUMERIC) 

481 return _get_number_symbols(locale, numbering_system=numbering_system).get('infinity', '∞') 

482 

483 

484def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = None) -> str: 

485 """Return the given number formatted for a specific locale. 

486 

487 >>> format_number(1099, locale='en_US') # doctest: +SKIP 

488 u'1,099' 

489 >>> format_number(1099, locale='de_DE') # doctest: +SKIP 

490 u'1.099' 

491 

492 .. deprecated:: 2.6.0 

493 

494 Use babel.numbers.format_decimal() instead. 

495 

496 :param number: the number to format 

497 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

498 

499 

500 """ 

501 warnings.warn('Use babel.numbers.format_decimal() instead.', DeprecationWarning, stacklevel=2) 

502 return format_decimal(number, locale=locale) 

503 

504 

505def get_decimal_precision(number: decimal.Decimal) -> int: 

506 """Return maximum precision of a decimal instance's fractional part. 

507 

508 Precision is extracted from the fractional part only. 

509 """ 

510 # Copied from: https://github.com/mahmoud/boltons/pull/59 

511 assert isinstance(number, decimal.Decimal) 

512 decimal_tuple = number.normalize().as_tuple() 

513 # Note: DecimalTuple.exponent can be 'n' (qNaN), 'N' (sNaN), or 'F' (Infinity) 

514 if not isinstance(decimal_tuple.exponent, int) or decimal_tuple.exponent >= 0: 

515 return 0 

516 return abs(decimal_tuple.exponent) 

517 

518 

519def get_decimal_quantum(precision: int | decimal.Decimal) -> decimal.Decimal: 

520 """Return minimal quantum of a number, as defined by precision.""" 

521 assert isinstance(precision, (int, decimal.Decimal)) 

522 return decimal.Decimal(10) ** (-precision) 

523 

524 

525def format_decimal( 

526 number: float | decimal.Decimal | str, 

527 format: str | NumberPattern | None = None, 

528 locale: Locale | str | None = None, 

529 decimal_quantization: bool = True, 

530 group_separator: bool = True, 

531 *, 

532 numbering_system: Literal["default"] | str = "latn", 

533) -> str: 

534 """Return the given decimal number formatted for a specific locale. 

535 

536 >>> format_decimal(1.2345, locale='en_US') 

537 u'1.234' 

538 >>> format_decimal(1.2346, locale='en_US') 

539 u'1.235' 

540 >>> format_decimal(-1.2346, locale='en_US') 

541 u'-1.235' 

542 >>> format_decimal(1.2345, locale='sv_SE') 

543 u'1,234' 

544 >>> format_decimal(1.2345, locale='de') 

545 u'1,234' 

546 >>> format_decimal(1.2345, locale='ar_EG', numbering_system='default') 

547 u'1٫234' 

548 >>> format_decimal(1.2345, locale='ar_EG', numbering_system='latn') 

549 u'1.234' 

550 

551 The appropriate thousands grouping and the decimal separator are used for 

552 each locale: 

553 

554 >>> format_decimal(12345.5, locale='en_US') 

555 u'12,345.5' 

556 

557 By default the locale is allowed to truncate and round a high-precision 

558 number by forcing its format pattern onto the decimal part. You can bypass 

559 this behavior with the `decimal_quantization` parameter: 

560 

561 >>> format_decimal(1.2346, locale='en_US') 

562 u'1.235' 

563 >>> format_decimal(1.2346, locale='en_US', decimal_quantization=False) 

564 u'1.2346' 

565 >>> format_decimal(12345.67, locale='fr_CA', group_separator=False) 

566 u'12345,67' 

567 >>> format_decimal(12345.67, locale='en_US', group_separator=True) 

568 u'12,345.67' 

569 

570 :param number: the number to format 

571 :param format: 

572 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

573 :param decimal_quantization: Truncate and round high-precision numbers to 

574 the format pattern. Defaults to `True`. 

575 :param group_separator: Boolean to switch group separator on/off in a locale's 

576 number format. 

577 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

578 The special value "default" will use the default numbering system of the locale. 

579 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. 

580 """ 

581 locale = Locale.parse(locale or LC_NUMERIC) 

582 if format is None: 

583 format = locale.decimal_formats[format] 

584 pattern = parse_pattern(format) 

585 return pattern.apply( 

586 number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) 

587 

588 

589def format_compact_decimal( 

590 number: float | decimal.Decimal | str, 

591 *, 

592 format_type: Literal["short", "long"] = "short", 

593 locale: Locale | str | None = None, 

594 fraction_digits: int = 0, 

595 numbering_system: Literal["default"] | str = "latn", 

596) -> str: 

597 """Return the given decimal number formatted for a specific locale in compact form. 

598 

599 >>> format_compact_decimal(12345, format_type="short", locale='en_US') 

600 u'12K' 

601 >>> format_compact_decimal(12345, format_type="long", locale='en_US') 

602 u'12 thousand' 

603 >>> format_compact_decimal(12345, format_type="short", locale='en_US', fraction_digits=2) 

604 u'12.34K' 

605 >>> format_compact_decimal(1234567, format_type="short", locale="ja_JP") 

606 u'123万' 

607 >>> format_compact_decimal(2345678, format_type="long", locale="mk") 

608 u'2 милиони' 

609 >>> format_compact_decimal(21000000, format_type="long", locale="mk") 

610 u'21 милион' 

611 >>> format_compact_decimal(12345, format_type="short", locale='ar_EG', fraction_digits=2, numbering_system='default') 

612 u'12٫34\xa0ألف' 

613 

614 :param number: the number to format 

615 :param format_type: Compact format to use ("short" or "long") 

616 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

617 :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`. 

618 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

619 The special value "default" will use the default numbering system of the locale. 

620 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. 

621 """ 

622 locale = Locale.parse(locale or LC_NUMERIC) 

623 compact_format = locale.compact_decimal_formats[format_type] 

624 number, format = _get_compact_format(number, compact_format, locale, fraction_digits) 

625 # Did not find a format, fall back. 

626 if format is None: 

627 format = locale.decimal_formats[None] 

628 pattern = parse_pattern(format) 

629 return pattern.apply(number, locale, decimal_quantization=False, numbering_system=numbering_system) 

630 

631 

632def _get_compact_format( 

633 number: float | decimal.Decimal | str, 

634 compact_format: LocaleDataDict, 

635 locale: Locale, 

636 fraction_digits: int, 

637) -> tuple[decimal.Decimal, NumberPattern | None]: 

638 """Returns the number after dividing by the unit and the format pattern to use. 

639 The algorithm is described here: 

640 https://www.unicode.org/reports/tr35/tr35-45/tr35-numbers.html#Compact_Number_Formats. 

641 """ 

642 if not isinstance(number, decimal.Decimal): 

643 number = decimal.Decimal(str(number)) 

644 if number.is_nan() or number.is_infinite(): 

645 return number, None 

646 format = None 

647 for magnitude in sorted([int(m) for m in compact_format["other"]], reverse=True): 

648 if abs(number) >= magnitude: 

649 # check the pattern using "other" as the amount 

650 format = compact_format["other"][str(magnitude)] 

651 pattern = parse_pattern(format).pattern 

652 # if the pattern is "0", we do not divide the number 

653 if pattern == "0": 

654 break 

655 # otherwise, we need to divide the number by the magnitude but remove zeros 

656 # equal to the number of 0's in the pattern minus 1 

657 number = cast(decimal.Decimal, number / (magnitude // (10 ** (pattern.count("0") - 1)))) 

658 # round to the number of fraction digits requested 

659 rounded = round(number, fraction_digits) 

660 # if the remaining number is singular, use the singular format 

661 plural_form = locale.plural_form(abs(number)) 

662 if plural_form not in compact_format: 

663 plural_form = "other" 

664 if number == 1 and "1" in compact_format: 

665 plural_form = "1" 

666 format = compact_format[plural_form][str(magnitude)] 

667 number = rounded 

668 break 

669 return number, format 

670 

671 

672class UnknownCurrencyFormatError(KeyError): 

673 """Exception raised when an unknown currency format is requested.""" 

674 

675 

676def format_currency( 

677 number: float | decimal.Decimal | str, 

678 currency: str, 

679 format: str | NumberPattern | None = None, 

680 locale: Locale | str | None = None, 

681 currency_digits: bool = True, 

682 format_type: Literal["name", "standard", "accounting"] = "standard", 

683 decimal_quantization: bool = True, 

684 group_separator: bool = True, 

685 *, 

686 numbering_system: Literal["default"] | str = "latn", 

687) -> str: 

688 """Return formatted currency value. 

689 

690 >>> format_currency(1099.98, 'USD', locale='en_US') 

691 '$1,099.98' 

692 >>> format_currency(1099.98, 'USD', locale='es_CO') 

693 u'US$1.099,98' 

694 >>> format_currency(1099.98, 'EUR', locale='de_DE') 

695 u'1.099,98\\xa0\\u20ac' 

696 >>> format_currency(1099.98, 'EGP', locale='ar_EG', numbering_system='default') 

697 u'\u200f1٬099٫98\xa0ج.م.\u200f' 

698 

699 The format can also be specified explicitly. The currency is 

700 placed with the '¤' sign. As the sign gets repeated the format 

701 expands (¤ being the symbol, ¤¤ is the currency abbreviation and 

702 ¤¤¤ is the full name of the currency): 

703 

704 >>> format_currency(1099.98, 'EUR', u'\xa4\xa4 #,##0.00', locale='en_US') 

705 u'EUR 1,099.98' 

706 >>> format_currency(1099.98, 'EUR', u'#,##0.00 \xa4\xa4\xa4', locale='en_US') 

707 u'1,099.98 euros' 

708 

709 Currencies usually have a specific number of decimal digits. This function 

710 favours that information over the given format: 

711 

712 >>> format_currency(1099.98, 'JPY', locale='en_US') 

713 u'\\xa51,100' 

714 >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES') 

715 u'1.099,98' 

716 

717 However, the number of decimal digits can be overridden from the currency 

718 information, by setting the last parameter to ``False``: 

719 

720 >>> format_currency(1099.98, 'JPY', locale='en_US', currency_digits=False) 

721 u'\\xa51,099.98' 

722 >>> format_currency(1099.98, 'COP', u'#,##0.00', locale='es_ES', currency_digits=False) 

723 u'1.099,98' 

724 

725 If a format is not specified the type of currency format to use 

726 from the locale can be specified: 

727 

728 >>> format_currency(1099.98, 'EUR', locale='en_US', format_type='standard') 

729 u'\\u20ac1,099.98' 

730 

731 When the given currency format type is not available, an exception is 

732 raised: 

733 

734 >>> format_currency('1099.98', 'EUR', locale='root', format_type='unknown') 

735 Traceback (most recent call last): 

736 ... 

737 UnknownCurrencyFormatError: "'unknown' is not a known currency format type" 

738 

739 >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=False) 

740 u'$101299.98' 

741 

742 >>> format_currency(101299.98, 'USD', locale='en_US', group_separator=True) 

743 u'$101,299.98' 

744 

745 You can also pass format_type='name' to use long display names. The order of 

746 the number and currency name, along with the correct localized plural form 

747 of the currency name, is chosen according to locale: 

748 

749 >>> format_currency(1, 'USD', locale='en_US', format_type='name') 

750 u'1.00 US dollar' 

751 >>> format_currency(1099.98, 'USD', locale='en_US', format_type='name') 

752 u'1,099.98 US dollars' 

753 >>> format_currency(1099.98, 'USD', locale='ee', format_type='name') 

754 u'us ga dollar 1,099.98' 

755 

756 By default the locale is allowed to truncate and round a high-precision 

757 number by forcing its format pattern onto the decimal part. You can bypass 

758 this behavior with the `decimal_quantization` parameter: 

759 

760 >>> format_currency(1099.9876, 'USD', locale='en_US') 

761 u'$1,099.99' 

762 >>> format_currency(1099.9876, 'USD', locale='en_US', decimal_quantization=False) 

763 u'$1,099.9876' 

764 

765 :param number: the number to format 

766 :param currency: the currency code 

767 :param format: the format string to use 

768 :param locale: the `Locale` object or locale identifier. 

769 Defaults to the system currency locale or numeric locale. 

770 :param currency_digits: use the currency's natural number of decimal digits 

771 :param format_type: the currency format type to use 

772 :param decimal_quantization: Truncate and round high-precision numbers to 

773 the format pattern. Defaults to `True`. 

774 :param group_separator: Boolean to switch group separator on/off in a locale's 

775 number format. 

776 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

777 The special value "default" will use the default numbering system of the locale. 

778 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. 

779 """ 

780 locale = Locale.parse(locale or LC_MONETARY) 

781 

782 if format_type == 'name': 

783 return _format_currency_long_name( 

784 number, 

785 currency, 

786 locale=locale, 

787 format=format, 

788 currency_digits=currency_digits, 

789 decimal_quantization=decimal_quantization, 

790 group_separator=group_separator, 

791 numbering_system=numbering_system, 

792 ) 

793 

794 if format: 

795 pattern = parse_pattern(format) 

796 else: 

797 try: 

798 pattern = locale.currency_formats[format_type] 

799 except KeyError: 

800 raise UnknownCurrencyFormatError(f"{format_type!r} is not a known currency format type") from None 

801 

802 return pattern.apply( 

803 number, locale, currency=currency, currency_digits=currency_digits, 

804 decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) 

805 

806 

807def _format_currency_long_name( 

808 number: float | decimal.Decimal | str, 

809 currency: str, 

810 *, 

811 locale: Locale, 

812 format: str | NumberPattern | None, 

813 currency_digits: bool, 

814 decimal_quantization: bool, 

815 group_separator: bool, 

816 numbering_system: Literal["default"] | str, 

817) -> str: 

818 # Algorithm described here: 

819 # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies 

820 

821 # Step 1. 

822 # There are no examples of items with explicit count (0 or 1) in current 

823 # locale data. So there is no point implementing that. 

824 # Step 2. 

825 

826 # Correct number to numeric type, important for looking up plural rules: 

827 number_n = float(number) if isinstance(number, str) else number 

828 

829 # Step 3. 

830 unit_pattern = get_currency_unit_pattern(currency, count=number_n, locale=locale) 

831 

832 # Step 4. 

833 display_name = get_currency_name(currency, count=number_n, locale=locale) 

834 

835 # Step 5. 

836 if not format: 

837 format = locale.decimal_formats[None] 

838 

839 pattern = parse_pattern(format) 

840 

841 number_part = pattern.apply( 

842 number, locale, currency=currency, currency_digits=currency_digits, 

843 decimal_quantization=decimal_quantization, group_separator=group_separator, numbering_system=numbering_system) 

844 

845 return unit_pattern.format(number_part, display_name) 

846 

847 

848def format_compact_currency( 

849 number: float | decimal.Decimal | str, 

850 currency: str, 

851 *, 

852 format_type: Literal["short"] = "short", 

853 locale: Locale | str | None = None, 

854 fraction_digits: int = 0, 

855 numbering_system: Literal["default"] | str = "latn", 

856) -> str: 

857 """Format a number as a currency value in compact form. 

858 

859 >>> format_compact_currency(12345, 'USD', locale='en_US') 

860 u'$12K' 

861 >>> format_compact_currency(123456789, 'USD', locale='en_US', fraction_digits=2) 

862 u'$123.46M' 

863 >>> format_compact_currency(123456789, 'EUR', locale='de_DE', fraction_digits=1) 

864 '123,5\xa0Mio.\xa0€' 

865 

866 :param number: the number to format 

867 :param currency: the currency code 

868 :param format_type: the compact format type to use. Defaults to "short". 

869 :param locale: the `Locale` object or locale identifier. 

870 Defaults to the system currency locale or numeric locale. 

871 :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`. 

872 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

873 The special value "default" will use the default numbering system of the locale. 

874 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. 

875 """ 

876 locale = Locale.parse(locale or LC_MONETARY) 

877 try: 

878 compact_format = locale.compact_currency_formats[format_type] 

879 except KeyError as error: 

880 raise UnknownCurrencyFormatError(f"{format_type!r} is not a known compact currency format type") from error 

881 number, format = _get_compact_format(number, compact_format, locale, fraction_digits) 

882 # Did not find a format, fall back. 

883 if format is None or "¤" not in str(format): 

884 # find first format that has a currency symbol 

885 for magnitude in compact_format['other']: 

886 format = compact_format['other'][magnitude].pattern 

887 if '¤' not in format: 

888 continue 

889 # remove characters that are not the currency symbol, 0's or spaces 

890 format = re.sub(r'[^0\s\¤]', '', format) 

891 # compress adjacent spaces into one 

892 format = re.sub(r'(\s)\s+', r'\1', format).strip() 

893 break 

894 if format is None: 

895 raise ValueError('No compact currency format found for the given number and locale.') 

896 pattern = parse_pattern(format) 

897 return pattern.apply(number, locale, currency=currency, currency_digits=False, decimal_quantization=False, 

898 numbering_system=numbering_system) 

899 

900 

901def format_percent( 

902 number: float | decimal.Decimal | str, 

903 format: str | NumberPattern | None = None, 

904 locale: Locale | str | None = None, 

905 decimal_quantization: bool = True, 

906 group_separator: bool = True, 

907 *, 

908 numbering_system: Literal["default"] | str = "latn", 

909) -> str: 

910 """Return formatted percent value for a specific locale. 

911 

912 >>> format_percent(0.34, locale='en_US') 

913 u'34%' 

914 >>> format_percent(25.1234, locale='en_US') 

915 u'2,512%' 

916 >>> format_percent(25.1234, locale='sv_SE') 

917 u'2\\xa0512\\xa0%' 

918 >>> format_percent(25.1234, locale='ar_EG', numbering_system='default') 

919 u'2٬512%' 

920 

921 The format pattern can also be specified explicitly: 

922 

923 >>> format_percent(25.1234, u'#,##0\u2030', locale='en_US') 

924 u'25,123\u2030' 

925 

926 By default the locale is allowed to truncate and round a high-precision 

927 number by forcing its format pattern onto the decimal part. You can bypass 

928 this behavior with the `decimal_quantization` parameter: 

929 

930 >>> format_percent(23.9876, locale='en_US') 

931 u'2,399%' 

932 >>> format_percent(23.9876, locale='en_US', decimal_quantization=False) 

933 u'2,398.76%' 

934 

935 >>> format_percent(229291.1234, locale='pt_BR', group_separator=False) 

936 u'22929112%' 

937 

938 >>> format_percent(229291.1234, locale='pt_BR', group_separator=True) 

939 u'22.929.112%' 

940 

941 :param number: the percent number to format 

942 :param format: 

943 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

944 :param decimal_quantization: Truncate and round high-precision numbers to 

945 the format pattern. Defaults to `True`. 

946 :param group_separator: Boolean to switch group separator on/off in a locale's 

947 number format. 

948 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

949 The special value "default" will use the default numbering system of the locale. 

950 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. 

951 """ 

952 locale = Locale.parse(locale or LC_NUMERIC) 

953 if not format: 

954 format = locale.percent_formats[None] 

955 pattern = parse_pattern(format) 

956 return pattern.apply( 

957 number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator, 

958 numbering_system=numbering_system, 

959 ) 

960 

961 

962def format_scientific( 

963 number: float | decimal.Decimal | str, 

964 format: str | NumberPattern | None = None, 

965 locale: Locale | str | None = None, 

966 decimal_quantization: bool = True, 

967 *, 

968 numbering_system: Literal["default"] | str = "latn", 

969) -> str: 

970 """Return value formatted in scientific notation for a specific locale. 

971 

972 >>> format_scientific(10000, locale='en_US') 

973 u'1E4' 

974 >>> format_scientific(10000, locale='ar_EG', numbering_system='default') 

975 u'1أس4' 

976 

977 The format pattern can also be specified explicitly: 

978 

979 >>> format_scientific(1234567, u'##0.##E00', locale='en_US') 

980 u'1.23E06' 

981 

982 By default the locale is allowed to truncate and round a high-precision 

983 number by forcing its format pattern onto the decimal part. You can bypass 

984 this behavior with the `decimal_quantization` parameter: 

985 

986 >>> format_scientific(1234.9876, u'#.##E0', locale='en_US') 

987 u'1.23E3' 

988 >>> format_scientific(1234.9876, u'#.##E0', locale='en_US', decimal_quantization=False) 

989 u'1.2349876E3' 

990 

991 :param number: the number to format 

992 :param format: 

993 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

994 :param decimal_quantization: Truncate and round high-precision numbers to 

995 the format pattern. Defaults to `True`. 

996 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

997 The special value "default" will use the default numbering system of the locale. 

998 :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale. 

999 """ 

1000 locale = Locale.parse(locale or LC_NUMERIC) 

1001 if not format: 

1002 format = locale.scientific_formats[None] 

1003 pattern = parse_pattern(format) 

1004 return pattern.apply( 

1005 number, locale, decimal_quantization=decimal_quantization, numbering_system=numbering_system) 

1006 

1007 

1008class NumberFormatError(ValueError): 

1009 """Exception raised when a string cannot be parsed into a number.""" 

1010 

1011 def __init__(self, message: str, suggestions: list[str] | None = None) -> None: 

1012 super().__init__(message) 

1013 #: a list of properly formatted numbers derived from the invalid input 

1014 self.suggestions = suggestions 

1015 

1016 

1017SPACE_CHARS = { 

1018 ' ', # space 

1019 '\xa0', # no-break space 

1020 '\u202f', # narrow no-break space 

1021} 

1022 

1023SPACE_CHARS_RE = re.compile('|'.join(SPACE_CHARS)) 

1024 

1025 

1026def parse_number( 

1027 string: str, 

1028 locale: Locale | str | None = None, 

1029 *, 

1030 numbering_system: Literal["default"] | str = "latn", 

1031) -> int: 

1032 """Parse localized number string into an integer. 

1033 

1034 >>> parse_number('1,099', locale='en_US') 

1035 1099 

1036 >>> parse_number('1.099', locale='de_DE') 

1037 1099 

1038 

1039 When the given string cannot be parsed, an exception is raised: 

1040 

1041 >>> parse_number('1.099,98', locale='de') 

1042 Traceback (most recent call last): 

1043 ... 

1044 NumberFormatError: '1.099,98' is not a valid number 

1045 

1046 :param string: the string to parse 

1047 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

1048 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

1049 The special value "default" will use the default numbering system of the locale. 

1050 :return: the parsed number 

1051 :raise `NumberFormatError`: if the string can not be converted to a number 

1052 :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale. 

1053 """ 

1054 group_symbol = get_group_symbol(locale, numbering_system=numbering_system) 

1055 

1056 if ( 

1057 group_symbol in SPACE_CHARS and # if the grouping symbol is a kind of space, 

1058 group_symbol not in string and # and the string to be parsed does not contain it, 

1059 SPACE_CHARS_RE.search(string) # but it does contain any other kind of space instead, 

1060 ): 

1061 # ... it's reasonable to assume it is taking the place of the grouping symbol. 

1062 string = SPACE_CHARS_RE.sub(group_symbol, string) 

1063 

1064 try: 

1065 return int(string.replace(group_symbol, '')) 

1066 except ValueError as ve: 

1067 raise NumberFormatError(f"{string!r} is not a valid number") from ve 

1068 

1069 

1070def parse_decimal( 

1071 string: str, 

1072 locale: Locale | str | None = None, 

1073 strict: bool = False, 

1074 *, 

1075 numbering_system: Literal["default"] | str = "latn", 

1076) -> decimal.Decimal: 

1077 """Parse localized decimal string into a decimal. 

1078 

1079 >>> parse_decimal('1,099.98', locale='en_US') 

1080 Decimal('1099.98') 

1081 >>> parse_decimal('1.099,98', locale='de') 

1082 Decimal('1099.98') 

1083 >>> parse_decimal('12 345,123', locale='ru') 

1084 Decimal('12345.123') 

1085 >>> parse_decimal('1٬099٫98', locale='ar_EG', numbering_system='default') 

1086 Decimal('1099.98') 

1087 

1088 When the given string cannot be parsed, an exception is raised: 

1089 

1090 >>> parse_decimal('2,109,998', locale='de') 

1091 Traceback (most recent call last): 

1092 ... 

1093 NumberFormatError: '2,109,998' is not a valid decimal number 

1094 

1095 If `strict` is set to `True` and the given string contains a number 

1096 formatted in an irregular way, an exception is raised: 

1097 

1098 >>> parse_decimal('30.00', locale='de', strict=True) 

1099 Traceback (most recent call last): 

1100 ... 

1101 NumberFormatError: '30.00' is not a properly formatted decimal number. Did you mean '3.000'? Or maybe '30,00'? 

1102 

1103 >>> parse_decimal('0.00', locale='de', strict=True) 

1104 Traceback (most recent call last): 

1105 ... 

1106 NumberFormatError: '0.00' is not a properly formatted decimal number. Did you mean '0'? 

1107 

1108 :param string: the string to parse 

1109 :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale. 

1110 :param strict: controls whether numbers formatted in a weird way are 

1111 accepted or rejected 

1112 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

1113 The special value "default" will use the default numbering system of the locale. 

1114 :raise NumberFormatError: if the string can not be converted to a 

1115 decimal number 

1116 :raise UnsupportedNumberingSystemError: if the numbering system is not supported by the locale. 

1117 """ 

1118 locale = Locale.parse(locale or LC_NUMERIC) 

1119 group_symbol = get_group_symbol(locale, numbering_system=numbering_system) 

1120 decimal_symbol = get_decimal_symbol(locale, numbering_system=numbering_system) 

1121 

1122 if not strict and ( 

1123 group_symbol in SPACE_CHARS and # if the grouping symbol is a kind of space, 

1124 group_symbol not in string and # and the string to be parsed does not contain it, 

1125 SPACE_CHARS_RE.search(string) # but it does contain any other kind of space instead, 

1126 ): 

1127 # ... it's reasonable to assume it is taking the place of the grouping symbol. 

1128 string = SPACE_CHARS_RE.sub(group_symbol, string) 

1129 

1130 try: 

1131 parsed = decimal.Decimal(string.replace(group_symbol, '') 

1132 .replace(decimal_symbol, '.')) 

1133 except decimal.InvalidOperation as exc: 

1134 raise NumberFormatError(f"{string!r} is not a valid decimal number") from exc 

1135 if strict and group_symbol in string: 

1136 proper = format_decimal(parsed, locale=locale, decimal_quantization=False, numbering_system=numbering_system) 

1137 if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol): 

1138 try: 

1139 parsed_alt = decimal.Decimal(string.replace(decimal_symbol, '') 

1140 .replace(group_symbol, '.')) 

1141 except decimal.InvalidOperation as exc: 

1142 raise NumberFormatError( 

1143 f"{string!r} is not a properly formatted decimal number. " 

1144 f"Did you mean {proper!r}?", 

1145 suggestions=[proper], 

1146 ) from exc 

1147 else: 

1148 proper_alt = format_decimal( 

1149 parsed_alt, 

1150 locale=locale, 

1151 decimal_quantization=False, 

1152 numbering_system=numbering_system, 

1153 ) 

1154 if proper_alt == proper: 

1155 raise NumberFormatError( 

1156 f"{string!r} is not a properly formatted decimal number. " 

1157 f"Did you mean {proper!r}?", 

1158 suggestions=[proper], 

1159 ) 

1160 else: 

1161 raise NumberFormatError( 

1162 f"{string!r} is not a properly formatted decimal number. " 

1163 f"Did you mean {proper!r}? Or maybe {proper_alt!r}?", 

1164 suggestions=[proper, proper_alt], 

1165 ) 

1166 return parsed 

1167 

1168 

1169def _remove_trailing_zeros_after_decimal(string: str, decimal_symbol: str) -> str: 

1170 """ 

1171 Remove trailing zeros from the decimal part of a numeric string. 

1172 

1173 This function takes a string representing a numeric value and a decimal symbol. 

1174 It removes any trailing zeros that appear after the decimal symbol in the number. 

1175 If the decimal part becomes empty after removing trailing zeros, the decimal symbol 

1176 is also removed. If the string does not contain the decimal symbol, it is returned unchanged. 

1177 

1178 :param string: The numeric string from which to remove trailing zeros. 

1179 :type string: str 

1180 :param decimal_symbol: The symbol used to denote the decimal point. 

1181 :type decimal_symbol: str 

1182 :return: The numeric string with trailing zeros removed from its decimal part. 

1183 :rtype: str 

1184 

1185 Example: 

1186 >>> _remove_trailing_zeros_after_decimal("123.4500", ".") 

1187 '123.45' 

1188 >>> _remove_trailing_zeros_after_decimal("100.000", ".") 

1189 '100' 

1190 >>> _remove_trailing_zeros_after_decimal("100", ".") 

1191 '100' 

1192 """ 

1193 integer_part, _, decimal_part = string.partition(decimal_symbol) 

1194 

1195 if decimal_part: 

1196 decimal_part = decimal_part.rstrip("0") 

1197 if decimal_part: 

1198 return integer_part + decimal_symbol + decimal_part 

1199 return integer_part 

1200 

1201 return string 

1202 

1203 

1204PREFIX_END = r'[^0-9@#.,]' 

1205NUMBER_TOKEN = r'[0-9@#.,E+]' 

1206 

1207PREFIX_PATTERN = r"(?P<prefix>(?:'[^']*'|%s)*)" % PREFIX_END 

1208NUMBER_PATTERN = r"(?P<number>%s*)" % NUMBER_TOKEN 

1209SUFFIX_PATTERN = r"(?P<suffix>.*)" 

1210 

1211number_re = re.compile(f"{PREFIX_PATTERN}{NUMBER_PATTERN}{SUFFIX_PATTERN}") 

1212 

1213 

1214def parse_grouping(p: str) -> tuple[int, int]: 

1215 """Parse primary and secondary digit grouping 

1216 

1217 >>> parse_grouping('##') 

1218 (1000, 1000) 

1219 >>> parse_grouping('#,###') 

1220 (3, 3) 

1221 >>> parse_grouping('#,####,###') 

1222 (3, 4) 

1223 """ 

1224 width = len(p) 

1225 g1 = p.rfind(',') 

1226 if g1 == -1: 

1227 return 1000, 1000 

1228 g1 = width - g1 - 1 

1229 g2 = p[:-g1 - 1].rfind(',') 

1230 if g2 == -1: 

1231 return g1, g1 

1232 g2 = width - g1 - g2 - 2 

1233 return g1, g2 

1234 

1235 

1236def parse_pattern(pattern: NumberPattern | str) -> NumberPattern: 

1237 """Parse number format patterns""" 

1238 if isinstance(pattern, NumberPattern): 

1239 return pattern 

1240 

1241 def _match_number(pattern): 

1242 rv = number_re.search(pattern) 

1243 if rv is None: 

1244 raise ValueError(f"Invalid number pattern {pattern!r}") 

1245 return rv.groups() 

1246 

1247 pos_pattern = pattern 

1248 

1249 # Do we have a negative subpattern? 

1250 if ';' in pattern: 

1251 pos_pattern, neg_pattern = pattern.split(';', 1) 

1252 pos_prefix, number, pos_suffix = _match_number(pos_pattern) 

1253 neg_prefix, _, neg_suffix = _match_number(neg_pattern) 

1254 else: 

1255 pos_prefix, number, pos_suffix = _match_number(pos_pattern) 

1256 neg_prefix = f"-{pos_prefix}" 

1257 neg_suffix = pos_suffix 

1258 if 'E' in number: 

1259 number, exp = number.split('E', 1) 

1260 else: 

1261 exp = None 

1262 if '@' in number and '.' in number and '0' in number: 

1263 raise ValueError('Significant digit patterns can not contain "@" or "0"') 

1264 if '.' in number: 

1265 integer, fraction = number.rsplit('.', 1) 

1266 else: 

1267 integer = number 

1268 fraction = '' 

1269 

1270 def parse_precision(p): 

1271 """Calculate the min and max allowed digits""" 

1272 min = max = 0 

1273 for c in p: 

1274 if c in '@0': 

1275 min += 1 

1276 max += 1 

1277 elif c == '#': 

1278 max += 1 

1279 elif c == ',': 

1280 continue 

1281 else: 

1282 break 

1283 return min, max 

1284 

1285 int_prec = parse_precision(integer) 

1286 frac_prec = parse_precision(fraction) 

1287 if exp: 

1288 exp_plus = exp.startswith('+') 

1289 exp = exp.lstrip('+') 

1290 exp_prec = parse_precision(exp) 

1291 else: 

1292 exp_plus = None 

1293 exp_prec = None 

1294 grouping = parse_grouping(integer) 

1295 return NumberPattern(pattern, (pos_prefix, neg_prefix), 

1296 (pos_suffix, neg_suffix), grouping, 

1297 int_prec, frac_prec, 

1298 exp_prec, exp_plus, number) 

1299 

1300 

1301class NumberPattern: 

1302 

1303 def __init__( 

1304 self, 

1305 pattern: str, 

1306 prefix: tuple[str, str], 

1307 suffix: tuple[str, str], 

1308 grouping: tuple[int, int], 

1309 int_prec: tuple[int, int], 

1310 frac_prec: tuple[int, int], 

1311 exp_prec: tuple[int, int] | None, 

1312 exp_plus: bool | None, 

1313 number_pattern: str | None = None, 

1314 ) -> None: 

1315 # Metadata of the decomposed parsed pattern. 

1316 self.pattern = pattern 

1317 self.prefix = prefix 

1318 self.suffix = suffix 

1319 self.number_pattern = number_pattern 

1320 self.grouping = grouping 

1321 self.int_prec = int_prec 

1322 self.frac_prec = frac_prec 

1323 self.exp_prec = exp_prec 

1324 self.exp_plus = exp_plus 

1325 self.scale = self.compute_scale() 

1326 

1327 def __repr__(self) -> str: 

1328 return f"<{type(self).__name__} {self.pattern!r}>" 

1329 

1330 def compute_scale(self) -> Literal[0, 2, 3]: 

1331 """Return the scaling factor to apply to the number before rendering. 

1332 

1333 Auto-set to a factor of 2 or 3 if presence of a ``%`` or ``‰`` sign is 

1334 detected in the prefix or suffix of the pattern. Default is to not mess 

1335 with the scale at all and keep it to 0. 

1336 """ 

1337 scale = 0 

1338 if '%' in ''.join(self.prefix + self.suffix): 

1339 scale = 2 

1340 elif '‰' in ''.join(self.prefix + self.suffix): 

1341 scale = 3 

1342 return scale 

1343 

1344 def scientific_notation_elements( 

1345 self, 

1346 value: decimal.Decimal, 

1347 locale: Locale | str | None, 

1348 *, 

1349 numbering_system: Literal["default"] | str = "latn", 

1350 ) -> tuple[decimal.Decimal, int, str]: 

1351 """ Returns normalized scientific notation components of a value. 

1352 """ 

1353 # Normalize value to only have one lead digit. 

1354 exp = value.adjusted() 

1355 value = value * get_decimal_quantum(exp) 

1356 assert value.adjusted() == 0 

1357 

1358 # Shift exponent and value by the minimum number of leading digits 

1359 # imposed by the rendering pattern. And always make that number 

1360 # greater or equal to 1. 

1361 lead_shift = max([1, min(self.int_prec)]) - 1 

1362 exp = exp - lead_shift 

1363 value = value * get_decimal_quantum(-lead_shift) 

1364 

1365 # Get exponent sign symbol. 

1366 exp_sign = '' 

1367 if exp < 0: 

1368 exp_sign = get_minus_sign_symbol(locale, numbering_system=numbering_system) 

1369 elif self.exp_plus: 

1370 exp_sign = get_plus_sign_symbol(locale, numbering_system=numbering_system) 

1371 

1372 # Normalize exponent value now that we have the sign. 

1373 exp = abs(exp) 

1374 

1375 return value, exp, exp_sign 

1376 

1377 def apply( 

1378 self, 

1379 value: float | decimal.Decimal | str, 

1380 locale: Locale | str | None, 

1381 currency: str | None = None, 

1382 currency_digits: bool = True, 

1383 decimal_quantization: bool = True, 

1384 force_frac: tuple[int, int] | None = None, 

1385 group_separator: bool = True, 

1386 *, 

1387 numbering_system: Literal["default"] | str = "latn", 

1388 ): 

1389 """Renders into a string a number following the defined pattern. 

1390 

1391 Forced decimal quantization is active by default so we'll produce a 

1392 number string that is strictly following CLDR pattern definitions. 

1393 

1394 :param value: The value to format. If this is not a Decimal object, 

1395 it will be cast to one. 

1396 :type value: decimal.Decimal|float|int 

1397 :param locale: The locale to use for formatting. 

1398 :type locale: str|babel.core.Locale 

1399 :param currency: Which currency, if any, to format as. 

1400 :type currency: str|None 

1401 :param currency_digits: Whether or not to use the currency's precision. 

1402 If false, the pattern's precision is used. 

1403 :type currency_digits: bool 

1404 :param decimal_quantization: Whether decimal numbers should be forcibly 

1405 quantized to produce a formatted output 

1406 strictly matching the CLDR definition for 

1407 the locale. 

1408 :type decimal_quantization: bool 

1409 :param force_frac: DEPRECATED - a forced override for `self.frac_prec` 

1410 for a single formatting invocation. 

1411 :param group_separator: Whether to use the locale's number group separator. 

1412 :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn". 

1413 The special value "default" will use the default numbering system of the locale. 

1414 :return: Formatted decimal string. 

1415 :rtype: str 

1416 :raise UnsupportedNumberingSystemError: If the numbering system is not supported by the locale. 

1417 """ 

1418 if not isinstance(value, decimal.Decimal): 

1419 value = decimal.Decimal(str(value)) 

1420 

1421 value = value.scaleb(self.scale) 

1422 

1423 # Separate the absolute value from its sign. 

1424 is_negative = int(value.is_signed()) 

1425 value = abs(value).normalize() 

1426 

1427 # Prepare scientific notation metadata. 

1428 if self.exp_prec: 

1429 value, exp, exp_sign = self.scientific_notation_elements(value, locale, numbering_system=numbering_system) 

1430 

1431 # Adjust the precision of the fractional part and force it to the 

1432 # currency's if necessary. 

1433 if force_frac: 

1434 # TODO (3.x?): Remove this parameter 

1435 warnings.warn( 

1436 'The force_frac parameter to NumberPattern.apply() is deprecated.', 

1437 DeprecationWarning, 

1438 stacklevel=2, 

1439 ) 

1440 frac_prec = force_frac 

1441 elif currency and currency_digits: 

1442 frac_prec = (get_currency_precision(currency), ) * 2 

1443 else: 

1444 frac_prec = self.frac_prec 

1445 

1446 # Bump decimal precision to the natural precision of the number if it 

1447 # exceeds the one we're about to use. This adaptative precision is only 

1448 # triggered if the decimal quantization is disabled or if a scientific 

1449 # notation pattern has a missing mandatory fractional part (as in the 

1450 # default '#E0' pattern). This special case has been extensively 

1451 # discussed at https://github.com/python-babel/babel/pull/494#issuecomment-307649969 . 

1452 if not decimal_quantization or (self.exp_prec and frac_prec == (0, 0)): 

1453 frac_prec = (frac_prec[0], max([frac_prec[1], get_decimal_precision(value)])) 

1454 

1455 # Render scientific notation. 

1456 if self.exp_prec: 

1457 number = ''.join([ 

1458 self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system), 

1459 get_exponential_symbol(locale, numbering_system=numbering_system), 

1460 exp_sign, # type: ignore # exp_sign is always defined here 

1461 self._format_int(str(exp), self.exp_prec[0], self.exp_prec[1], locale, numbering_system=numbering_system), # type: ignore # exp is always defined here 

1462 ]) 

1463 

1464 # Is it a significant digits pattern? 

1465 elif '@' in self.pattern: 

1466 text = self._format_significant(value, 

1467 self.int_prec[0], 

1468 self.int_prec[1]) 

1469 a, sep, b = text.partition(".") 

1470 number = self._format_int(a, 0, 1000, locale, numbering_system=numbering_system) 

1471 if sep: 

1472 number += get_decimal_symbol(locale, numbering_system=numbering_system) + b 

1473 

1474 # A normal number pattern. 

1475 else: 

1476 number = self._quantize_value(value, locale, frac_prec, group_separator, numbering_system=numbering_system) 

1477 

1478 retval = ''.join([ 

1479 self.prefix[is_negative], 

1480 number if self.number_pattern != '' else '', 

1481 self.suffix[is_negative]]) 

1482 

1483 if '¤' in retval and currency is not None: 

1484 retval = retval.replace('¤¤¤', get_currency_name(currency, value, locale)) 

1485 retval = retval.replace('¤¤', currency.upper()) 

1486 retval = retval.replace('¤', get_currency_symbol(currency, locale)) 

1487 

1488 # remove single quotes around text, except for doubled single quotes 

1489 # which are replaced with a single quote 

1490 retval = re.sub(r"'([^']*)'", lambda m: m.group(1) or "'", retval) 

1491 

1492 return retval 

1493 

1494 # 

1495 # This is one tricky piece of code. The idea is to rely as much as possible 

1496 # on the decimal module to minimize the amount of code. 

1497 # 

1498 # Conceptually, the implementation of this method can be summarized in the 

1499 # following steps: 

1500 # 

1501 # - Move or shift the decimal point (i.e. the exponent) so the maximum 

1502 # amount of significant digits fall into the integer part (i.e. to the 

1503 # left of the decimal point) 

1504 # 

1505 # - Round the number to the nearest integer, discarding all the fractional 

1506 # part which contained extra digits to be eliminated 

1507 # 

1508 # - Convert the rounded integer to a string, that will contain the final 

1509 # sequence of significant digits already trimmed to the maximum 

1510 # 

1511 # - Restore the original position of the decimal point, potentially 

1512 # padding with zeroes on either side 

1513 # 

1514 def _format_significant(self, value: decimal.Decimal, minimum: int, maximum: int) -> str: 

1515 exp = value.adjusted() 

1516 scale = maximum - 1 - exp 

1517 digits = str(value.scaleb(scale).quantize(decimal.Decimal(1))) 

1518 if scale <= 0: 

1519 result = digits + '0' * -scale 

1520 else: 

1521 intpart = digits[:-scale] 

1522 i = len(intpart) 

1523 j = i + max(minimum - i, 0) 

1524 result = "{intpart}.{pad:0<{fill}}{fracpart}{fracextra}".format( 

1525 intpart=intpart or '0', 

1526 pad='', 

1527 fill=-min(exp + 1, 0), 

1528 fracpart=digits[i:j], 

1529 fracextra=digits[j:].rstrip('0'), 

1530 ).rstrip('.') 

1531 return result 

1532 

1533 def _format_int( 

1534 self, 

1535 value: str, 

1536 min: int, 

1537 max: int, 

1538 locale: Locale | str | None, 

1539 *, 

1540 numbering_system: Literal["default"] | str, 

1541 ) -> str: 

1542 width = len(value) 

1543 if width < min: 

1544 value = '0' * (min - width) + value 

1545 gsize = self.grouping[0] 

1546 ret = '' 

1547 symbol = get_group_symbol(locale, numbering_system=numbering_system) 

1548 while len(value) > gsize: 

1549 ret = symbol + value[-gsize:] + ret 

1550 value = value[:-gsize] 

1551 gsize = self.grouping[1] 

1552 return value + ret 

1553 

1554 def _quantize_value( 

1555 self, 

1556 value: decimal.Decimal, 

1557 locale: Locale | str | None, 

1558 frac_prec: tuple[int, int], 

1559 group_separator: bool, 

1560 *, 

1561 numbering_system: Literal["default"] | str, 

1562 ) -> str: 

1563 # If the number is +/-Infinity, we can't quantize it 

1564 if value.is_infinite(): 

1565 return get_infinity_symbol(locale, numbering_system=numbering_system) 

1566 quantum = get_decimal_quantum(frac_prec[1]) 

1567 rounded = value.quantize(quantum) 

1568 a, sep, b = f"{rounded:f}".partition(".") 

1569 integer_part = a 

1570 if group_separator: 

1571 integer_part = self._format_int(a, self.int_prec[0], self.int_prec[1], locale, numbering_system=numbering_system) 

1572 number = integer_part + self._format_frac(b or '0', locale=locale, force_frac=frac_prec, numbering_system=numbering_system) 

1573 return number 

1574 

1575 def _format_frac( 

1576 self, 

1577 value: str, 

1578 locale: Locale | str | None, 

1579 force_frac: tuple[int, int] | None = None, 

1580 *, 

1581 numbering_system: Literal["default"] | str, 

1582 ) -> str: 

1583 min, max = force_frac or self.frac_prec 

1584 if len(value) < min: 

1585 value += ('0' * (min - len(value))) 

1586 if max == 0 or (min == 0 and int(value) == 0): 

1587 return '' 

1588 while len(value) > min and value[-1] == '0': 

1589 value = value[:-1] 

1590 return get_decimal_symbol(locale, numbering_system=numbering_system) + value