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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

437 statements  

1""" 

2babel.numbers 

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

4 

5Locale dependent formatting and parsing of numeric data. 

6 

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

8following 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-2026 by the Babel Team. 

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

17""" 

18 

19# TODO: 

20# Padding and rounding increments in pattern: 

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

22from __future__ import annotations 

23 

24import datetime 

25import decimal 

26import re 

27import warnings 

28from typing import Any, Literal, cast, overload 

29 

30from babel.core import Locale, default_locale, get_global 

31from babel.localedata import LocaleDataDict 

32 

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

34LC_NUMERIC = default_locale('LC_NUMERIC') 

35 

36 

37class UnknownCurrencyError(Exception): 

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

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

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 '{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]: ... # pragma: no cover 

212 

213 

214@overload 

215def get_territory_currencies( 

216 territory: str, 

217 start_date: datetime.date | None = ..., 

218 end_date: datetime.date | None = ..., 

219 tender: bool = ..., 

220 non_tender: bool = ..., 

221 include_details: Literal[True] = ..., 

222) -> list[dict[str, Any]]: ... # pragma: no cover 

223 

224 

225def get_territory_currencies( 

226 territory: str, 

227 start_date: datetime.date | None = None, 

228 end_date: datetime.date | None = None, 

229 tender: bool = True, 

230 non_tender: bool = False, 

231 include_details: bool = False, 

232) -> list[str] | list[dict[str, Any]]: 

233 """Returns the list of currencies for the given territory that are valid for 

234 the given date range. In addition to that the currency database 

235 distinguishes between tender and non-tender currencies. By default only 

236 tender currencies are returned. 

237 

238 The return value is a list of all currencies roughly ordered by the time 

239 of when the currency became active. The longer the currency is being in 

240 use the more to the left of the list it will be. 

241 

242 The start date defaults to today. If no end date is given it will be the 

243 same as the start date. Otherwise a range can be defined. For instance 

244 this can be used to find the currencies in use in Austria between 1995 and 

245 2011: 

246 

247 >>> from datetime import date 

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

249 ['ATS', 'EUR'] 

250 

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

252 single date: 

253 

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

255 ['ATS'] 

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

257 ['EUR'] 

258 

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

260 however can be changed: 

261 

262 >>> get_territory_currencies('US') 

263 ['USD'] 

264 >>> get_territory_currencies('US', tender=False, non_tender=True, 

265 ... start_date=date(2014, 1, 1)) 

266 ['USN', 'USS'] 

267 

268 .. versionadded:: 2.0 

269 

270 :param territory: the name of the territory to find the currency for. 

271 :param start_date: the start date. If not given today is assumed. 

272 :param end_date: the end date. If not given the start date is assumed. 

273 :param tender: controls whether tender currencies should be included. 

274 :param non_tender: controls whether non-tender currencies should be 

275 included. 

276 :param include_details: if set to `True`, instead of returning currency 

277 codes the return value will be dictionaries 

278 with detail information. In that case each 

279 dictionary will have the keys ``'currency'``, 

280 ``'from'``, ``'to'``, and ``'tender'``. 

281 """ 

282 currencies = get_global('territory_currencies') 

283 if start_date is None: 

284 start_date = datetime.date.today() 

285 elif isinstance(start_date, datetime.datetime): 

286 start_date = start_date.date() 

287 if end_date is None: 

288 end_date = start_date 

289 elif isinstance(end_date, datetime.datetime): 

290 end_date = end_date.date() 

291 

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

293 # TODO: validate that the territory exists 

294 

295 def _is_active(start, end): 

296 return (start is None or start <= end_date) and (end is None or end >= start_date) 

297 

298 result = [] 

299 for currency_code, start, end, is_tender in curs: 

300 if start: 

301 start = datetime.date(*start) 

302 if end: 

303 end = datetime.date(*end) 

304 if ((is_tender and tender) or (not is_tender and non_tender)) and _is_active( 

305 start, 

306 end, 

307 ): 

308 if include_details: 

309 result.append( 

310 { 

311 'currency': currency_code, 

312 'from': start, 

313 'to': end, 

314 'tender': is_tender, 

315 }, 

316 ) 

317 else: 

318 result.append(currency_code) 

319 

320 return result 

321 

322 

323def _get_numbering_system( 

324 locale: Locale, 

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

326) -> str: 

327 if numbering_system == "default": 

328 return locale.default_numbering_system 

329 else: 

330 return numbering_system 

331 

332 

333def _get_number_symbols( 

334 locale: Locale, 

335 *, 

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

337) -> LocaleDataDict: 

338 numbering_system = _get_numbering_system(locale, numbering_system) 

339 try: 

340 return locale.number_symbols[numbering_system] 

341 except KeyError as error: 

342 raise UnsupportedNumberingSystemError( 

343 f"Unknown numbering system {numbering_system} for Locale {locale}.", 

344 ) from error 

345 

346 

347class UnsupportedNumberingSystemError(Exception): 

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

349 

350 pass 

351 

352 

353def get_decimal_symbol( 

354 locale: Locale | str | None = None, 

355 *, 

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

357) -> str: 

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

359 

360 >>> get_decimal_symbol('en_US') 

361 '.' 

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

363 '٫' 

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

365 '.' 

366 

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

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

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

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

371 """ 

372 locale = Locale.parse(locale or LC_NUMERIC) 

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

374 

375 

376def get_plus_sign_symbol( 

377 locale: Locale | str | None = None, 

378 *, 

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

380) -> str: 

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

382 

383 >>> get_plus_sign_symbol('en_US') 

384 '+' 

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

386 '\\u061c+' 

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

388 '\\u200e+' 

389 

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

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

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

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

394 """ 

395 locale = Locale.parse(locale or LC_NUMERIC) 

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

397 

398 

399def get_minus_sign_symbol( 

400 locale: Locale | str | None = None, 

401 *, 

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

403) -> str: 

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

405 

406 >>> get_minus_sign_symbol('en_US') 

407 '-' 

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

409 '\\u061c-' 

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

411 '\\u200e-' 

412 

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

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

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

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

417 """ 

418 locale = Locale.parse(locale or LC_NUMERIC) 

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

420 

421 

422def get_exponential_symbol( 

423 locale: Locale | str | None = None, 

424 *, 

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

426) -> str: 

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

428 

429 >>> get_exponential_symbol('en_US') 

430 'E' 

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

432 'أس' 

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

434 'E' 

435 

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

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

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

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

440 """ 

441 locale = Locale.parse(locale or LC_NUMERIC) 

442 return _get_number_symbols(locale, numbering_system=numbering_system).get('exponential', 'E') # fmt: skip 

443 

444 

445def get_group_symbol( 

446 locale: Locale | str | None = None, 

447 *, 

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

449) -> str: 

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

451 

452 >>> get_group_symbol('en_US') 

453 ',' 

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

455 '٬' 

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

457 ',' 

458 

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

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

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

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

463 """ 

464 locale = Locale.parse(locale or LC_NUMERIC) 

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

466 

467 

468def get_infinity_symbol( 

469 locale: Locale | str | None = None, 

470 *, 

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

472) -> str: 

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

474 

475 >>> get_infinity_symbol('en_US') 

476 '∞' 

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

478 '∞' 

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

480 '∞' 

481 

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

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

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

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

486 """ 

487 locale = Locale.parse(locale or LC_NUMERIC) 

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

489 

490 

491def format_number( 

492 number: float | decimal.Decimal | str, 

493 locale: Locale | str | None = None, 

494) -> str: 

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

496 

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

498 '1,099' 

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

500 '1.099' 

501 

502 .. deprecated:: 2.6.0 

503 

504 Use babel.numbers.format_decimal() instead. 

505 

506 :param number: the number to format 

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

508 

509 

510 """ 

511 warnings.warn( 

512 'Use babel.numbers.format_decimal() instead.', 

513 DeprecationWarning, 

514 stacklevel=2, 

515 ) 

516 return format_decimal(number, locale=locale) 

517 

518 

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

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

521 

522 Precision is extracted from the fractional part only. 

523 """ 

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

525 assert isinstance(number, decimal.Decimal) 

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

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

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

529 return 0 

530 return abs(decimal_tuple.exponent) 

531 

532 

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

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

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

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

537 

538 

539def format_decimal( 

540 number: float | decimal.Decimal | str, 

541 format: str | NumberPattern | None = None, 

542 locale: Locale | str | None = None, 

543 decimal_quantization: bool = True, 

544 group_separator: bool = True, 

545 *, 

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

547) -> str: 

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

549 

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

551 '1.234' 

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

553 '1.235' 

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

555 '-1.235' 

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

557 '1,234' 

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

559 '1,234' 

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

561 '1٫234' 

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

563 '1.234' 

564 

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

566 each locale: 

567 

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

569 '12,345.5' 

570 

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

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

573 this behavior with the `decimal_quantization` parameter: 

574 

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

576 '1.235' 

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

578 '1.2346' 

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

580 '12345,67' 

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

582 '12,345.67' 

583 

584 :param number: the number to format 

585 :param format: 

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

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

588 the format pattern. Defaults to `True`. 

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

590 number format. 

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

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

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

594 """ 

595 locale = Locale.parse(locale or LC_NUMERIC) 

596 if format is None: 

597 format = locale.decimal_formats[format] 

598 pattern = parse_pattern(format) 

599 return pattern.apply( 

600 number, 

601 locale, 

602 decimal_quantization=decimal_quantization, 

603 group_separator=group_separator, 

604 numbering_system=numbering_system, 

605 ) 

606 

607 

608def format_compact_decimal( 

609 number: float | decimal.Decimal | str, 

610 *, 

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

612 locale: Locale | str | None = None, 

613 fraction_digits: int = 0, 

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

615) -> str: 

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

617 

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

619 '12K' 

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

621 '12 thousand' 

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

623 '12.34K' 

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

625 '123万' 

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

627 '2 милиони' 

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

629 '21 милион' 

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

631 '12٫34\\xa0ألف' 

632 

633 :param number: the number to format 

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

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

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

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

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

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

640 """ 

641 locale = Locale.parse(locale or LC_NUMERIC) 

642 compact_format = locale.compact_decimal_formats[format_type] 

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

644 # Did not find a format, fall back. 

645 if format is None: 

646 format = locale.decimal_formats[None] 

647 pattern = parse_pattern(format) 

648 return pattern.apply( 

649 number, 

650 locale, 

651 decimal_quantization=False, 

652 numbering_system=numbering_system, 

653 ) 

654 

655 

656def _get_compact_format( 

657 number: float | decimal.Decimal | str, 

658 compact_format: LocaleDataDict, 

659 locale: Locale, 

660 fraction_digits: int, 

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

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

663 The algorithm is described here: 

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

665 """ 

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

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

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

669 return number, None 

670 format = None 

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

672 if abs(number) >= magnitude: 

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

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

675 pattern = parse_pattern(format).pattern 

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

677 if pattern == "0": 

678 break 

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

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

681 number = cast( 

682 decimal.Decimal, 

683 number / (magnitude // (10 ** (pattern.count("0") - 1))), 

684 ) 

685 # round to the number of fraction digits requested 

686 rounded = round(number, fraction_digits) 

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

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

689 if plural_form not in compact_format: 

690 plural_form = "other" 

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

692 plural_form = "1" 

693 if str(magnitude) not in compact_format[plural_form]: 

694 plural_form = "other" # fall back to other as the implicit default 

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

696 number = rounded 

697 break 

698 return number, format 

699 

700 

701class UnknownCurrencyFormatError(KeyError): 

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

703 

704 

705def format_currency( 

706 number: float | decimal.Decimal | str, 

707 currency: str, 

708 format: str | NumberPattern | None = None, 

709 locale: Locale | str | None = None, 

710 currency_digits: bool = True, 

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

712 decimal_quantization: bool = True, 

713 group_separator: bool = True, 

714 *, 

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

716) -> str: 

717 """Return formatted currency value. 

718 

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

720 '$1,099.98' 

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

722 'US$1.099,98' 

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

724 '1.099,98\\xa0\\u20ac' 

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

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

727 

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

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

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

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

732 

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

734 'EUR 1,099.98' 

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

736 '1,099.98 euros' 

737 

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

739 favours that information over the given format: 

740 

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

742 '\\xa51,100' 

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

744 '1.099,98' 

745 

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

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

748 

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

750 '\\xa51,099.98' 

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

752 '1.099,98' 

753 

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

755 from the locale can be specified: 

756 

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

758 '\\u20ac1,099.98' 

759 

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

761 raised: 

762 

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

764 Traceback (most recent call last): 

765 ... 

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

767 

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

769 '$101299.98' 

770 

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

772 '$101,299.98' 

773 

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

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

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

777 

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

779 '1.00 US dollar' 

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

781 '1,099.98 US dollars' 

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

783 'us ga dollar 1,099.98' 

784 

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

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

787 this behavior with the `decimal_quantization` parameter: 

788 

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

790 '$1,099.99' 

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

792 '$1,099.9876' 

793 

794 :param number: the number to format 

795 :param currency: the currency code 

796 :param format: the format string to use 

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

798 Defaults to the system currency locale or numeric locale. 

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

800 :param format_type: the currency format type to use 

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

802 the format pattern. Defaults to `True`. 

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

804 number format. 

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

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

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

808 """ 

809 locale = Locale.parse(locale or LC_MONETARY) 

810 

811 if format_type == 'name': 

812 return _format_currency_long_name( 

813 number, 

814 currency, 

815 locale=locale, 

816 format=format, 

817 currency_digits=currency_digits, 

818 decimal_quantization=decimal_quantization, 

819 group_separator=group_separator, 

820 numbering_system=numbering_system, 

821 ) 

822 

823 if format: 

824 pattern = parse_pattern(format) 

825 else: 

826 try: 

827 pattern = locale.currency_formats[format_type] 

828 except KeyError: 

829 raise UnknownCurrencyFormatError( 

830 f"{format_type!r} is not a known currency format type", 

831 ) from None 

832 

833 return pattern.apply( 

834 number, 

835 locale, 

836 currency=currency, 

837 currency_digits=currency_digits, 

838 decimal_quantization=decimal_quantization, 

839 group_separator=group_separator, 

840 numbering_system=numbering_system, 

841 ) 

842 

843 

844def _format_currency_long_name( 

845 number: float | decimal.Decimal | str, 

846 currency: str, 

847 *, 

848 locale: Locale, 

849 format: str | NumberPattern | None, 

850 currency_digits: bool, 

851 decimal_quantization: bool, 

852 group_separator: bool, 

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

854) -> str: 

855 # Algorithm described here: 

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

857 

858 # Step 1. 

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

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

861 # Step 2. 

862 

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

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

865 

866 # Step 3. 

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

868 

869 # Step 4. 

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

871 

872 # Step 5. 

873 if not format: 

874 format = locale.decimal_formats[None] 

875 

876 pattern = parse_pattern(format) 

877 

878 number_part = pattern.apply( 

879 number, 

880 locale, 

881 currency=currency, 

882 currency_digits=currency_digits, 

883 decimal_quantization=decimal_quantization, 

884 group_separator=group_separator, 

885 numbering_system=numbering_system, 

886 ) 

887 

888 return unit_pattern.format(number_part, display_name) 

889 

890 

891def format_compact_currency( 

892 number: float | decimal.Decimal | str, 

893 currency: str, 

894 *, 

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

896 locale: Locale | str | None = None, 

897 fraction_digits: int = 0, 

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

899) -> str: 

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

901 

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

903 '$12K' 

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

905 '$123.46M' 

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

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

908 

909 :param number: the number to format 

910 :param currency: the currency code 

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

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

913 Defaults to the system currency locale or numeric locale. 

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

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

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

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

918 """ 

919 locale = Locale.parse(locale or LC_MONETARY) 

920 try: 

921 compact_format = locale.compact_currency_formats[format_type] 

922 except KeyError as error: 

923 raise UnknownCurrencyFormatError( 

924 f"{format_type!r} is not a known compact currency format type", 

925 ) from error 

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

927 # Did not find a format, fall back. 

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

929 # find first format that has a currency symbol 

930 for magnitude in compact_format['other']: 

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

932 if '¤' not in format: 

933 continue 

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

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

936 # compress adjacent spaces into one 

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

938 break 

939 if format is None: 

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

941 pattern = parse_pattern(format) 

942 return pattern.apply( 

943 number, 

944 locale, 

945 currency=currency, 

946 currency_digits=False, 

947 decimal_quantization=False, 

948 numbering_system=numbering_system, 

949 ) 

950 

951 

952def format_percent( 

953 number: float | decimal.Decimal | str, 

954 format: str | NumberPattern | None = None, 

955 locale: Locale | str | None = None, 

956 decimal_quantization: bool = True, 

957 group_separator: bool = True, 

958 *, 

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

960) -> str: 

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

962 

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

964 '34%' 

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

966 '2,512%' 

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

968 '2\\xa0512\\xa0%' 

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

970 '2٬512%' 

971 

972 The format pattern can also be specified explicitly: 

973 

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

975 '25,123‰' 

976 

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

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

979 this behavior with the `decimal_quantization` parameter: 

980 

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

982 '2,399%' 

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

984 '2,398.76%' 

985 

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

987 '22929112%' 

988 

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

990 '22.929.112%' 

991 

992 :param number: the percent number to format 

993 :param format: 

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

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

996 the format pattern. Defaults to `True`. 

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

998 number format. 

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

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

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

1002 """ 

1003 locale = Locale.parse(locale or LC_NUMERIC) 

1004 if not format: 

1005 format = locale.percent_formats[None] 

1006 pattern = parse_pattern(format) 

1007 return pattern.apply( 

1008 number, 

1009 locale, 

1010 decimal_quantization=decimal_quantization, 

1011 group_separator=group_separator, 

1012 numbering_system=numbering_system, 

1013 ) 

1014 

1015 

1016def format_scientific( 

1017 number: float | decimal.Decimal | str, 

1018 format: str | NumberPattern | None = None, 

1019 locale: Locale | str | None = None, 

1020 decimal_quantization: bool = True, 

1021 *, 

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

1023) -> str: 

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

1025 

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

1027 '1E4' 

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

1029 '1أس4' 

1030 

1031 The format pattern can also be specified explicitly: 

1032 

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

1034 '1.23E06' 

1035 

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

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

1038 this behavior with the `decimal_quantization` parameter: 

1039 

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

1041 '1.23E3' 

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

1043 '1.2349876E3' 

1044 

1045 :param number: the number to format 

1046 :param format: 

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

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

1049 the format pattern. Defaults to `True`. 

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

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

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

1053 """ 

1054 locale = Locale.parse(locale or LC_NUMERIC) 

1055 if not format: 

1056 format = locale.scientific_formats[None] 

1057 pattern = parse_pattern(format) 

1058 return pattern.apply( 

1059 number, 

1060 locale, 

1061 decimal_quantization=decimal_quantization, 

1062 numbering_system=numbering_system, 

1063 ) 

1064 

1065 

1066class NumberFormatError(ValueError): 

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

1068 

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

1070 super().__init__(message) 

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

1072 self.suggestions = suggestions 

1073 

1074 

1075SPACE_CHARS = { 

1076 ' ', # space 

1077 '\xa0', # no-break space 

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

1079} 

1080 

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

1082 

1083 

1084def parse_number( 

1085 string: str, 

1086 locale: Locale | str | None = None, 

1087 *, 

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

1089) -> int: 

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

1091 

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

1093 1099 

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

1095 1099 

1096 

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

1098 

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

1100 Traceback (most recent call last): 

1101 ... 

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

1103 

1104 :param string: the string to parse 

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

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

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

1108 :return: the parsed number 

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

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

1111 """ 

1112 group_symbol = get_group_symbol(locale, numbering_system=numbering_system) 

1113 

1114 if ( 

1115 # if the grouping symbol is a kind of space, 

1116 group_symbol in SPACE_CHARS 

1117 # and the string to be parsed does not contain it, 

1118 and group_symbol not in string 

1119 # but it does contain any other kind of space instead, 

1120 and SPACE_CHARS_RE.search(string) 

1121 ): 

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

1123 string = SPACE_CHARS_RE.sub(group_symbol, string) 

1124 

1125 try: 

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

1127 except ValueError as ve: 

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

1129 

1130 

1131def parse_decimal( 

1132 string: str, 

1133 locale: Locale | str | None = None, 

1134 strict: bool = False, 

1135 *, 

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

1137) -> decimal.Decimal: 

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

1139 

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

1141 Decimal('1099.98') 

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

1143 Decimal('1099.98') 

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

1145 Decimal('12345.123') 

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

1147 Decimal('1099.98') 

1148 

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

1150 

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

1152 Traceback (most recent call last): 

1153 ... 

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

1155 

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

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

1158 

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

1160 Traceback (most recent call last): 

1161 ... 

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

1163 

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

1165 Traceback (most recent call last): 

1166 ... 

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

1168 

1169 :param string: the string to parse 

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

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

1172 accepted or rejected 

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

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

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

1176 decimal number 

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

1178 """ 

1179 locale = Locale.parse(locale or LC_NUMERIC) 

1180 group_symbol = get_group_symbol(locale, numbering_system=numbering_system) 

1181 decimal_symbol = get_decimal_symbol(locale, numbering_system=numbering_system) 

1182 

1183 if not strict and ( 

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

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

1186 # but it does contain any other kind of space instead, 

1187 and SPACE_CHARS_RE.search(string) 

1188 ): 

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

1190 string = SPACE_CHARS_RE.sub(group_symbol, string) 

1191 

1192 try: 

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

1194 except decimal.InvalidOperation as exc: 

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

1196 if strict and group_symbol in string: 

1197 proper = format_decimal( 

1198 parsed, 

1199 locale=locale, 

1200 decimal_quantization=False, 

1201 numbering_system=numbering_system, 

1202 ) 

1203 if string != proper and proper != _remove_trailing_zeros_after_decimal(string, decimal_symbol): # fmt: skip 

1204 try: 

1205 parsed_alt = decimal.Decimal( 

1206 string.replace(decimal_symbol, '').replace(group_symbol, '.'), 

1207 ) 

1208 except decimal.InvalidOperation as exc: 

1209 raise NumberFormatError( 

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

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

1212 suggestions=[proper], 

1213 ) from exc 

1214 else: 

1215 proper_alt = format_decimal( 

1216 parsed_alt, 

1217 locale=locale, 

1218 decimal_quantization=False, 

1219 numbering_system=numbering_system, 

1220 ) 

1221 if proper_alt == proper: 

1222 raise NumberFormatError( 

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

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

1225 suggestions=[proper], 

1226 ) 

1227 else: 

1228 raise NumberFormatError( 

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

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

1231 suggestions=[proper, proper_alt], 

1232 ) 

1233 return parsed 

1234 

1235 

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

1237 """ 

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

1239 

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

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

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

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

1244 

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

1246 :type string: str 

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

1248 :type decimal_symbol: str 

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

1250 :rtype: str 

1251 

1252 Example: 

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

1254 '123.45' 

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

1256 '100' 

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

1258 '100' 

1259 """ 

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

1261 

1262 if decimal_part: 

1263 decimal_part = decimal_part.rstrip("0") 

1264 if decimal_part: 

1265 return integer_part + decimal_symbol + decimal_part 

1266 return integer_part 

1267 

1268 return string 

1269 

1270 

1271_number_pattern_re = re.compile( 

1272 r"(?P<prefix>(?:[^'0-9@#.,]|'[^']*')*)" 

1273 r"(?P<number>[0-9@#.,E+]*)" 

1274 r"(?P<suffix>.*)", 

1275) 

1276 

1277 

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

1279 """Parse primary and secondary digit grouping 

1280 

1281 >>> parse_grouping('##') 

1282 (1000, 1000) 

1283 >>> parse_grouping('#,###') 

1284 (3, 3) 

1285 >>> parse_grouping('#,####,###') 

1286 (3, 4) 

1287 """ 

1288 width = len(p) 

1289 g1 = p.rfind(',') 

1290 if g1 == -1: 

1291 return 1000, 1000 

1292 g1 = width - g1 - 1 

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

1294 if g2 == -1: 

1295 return g1, g1 

1296 g2 = width - g1 - g2 - 2 

1297 return g1, g2 

1298 

1299 

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

1301 """Parse number format patterns""" 

1302 if isinstance(pattern, NumberPattern): 

1303 return pattern 

1304 

1305 def _match_number(pattern): 

1306 rv = _number_pattern_re.search(pattern) 

1307 if rv is None: 

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

1309 return rv.groups() 

1310 

1311 pos_pattern = pattern 

1312 

1313 # Do we have a negative subpattern? 

1314 if ';' in pattern: 

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

1316 pos_prefix, number, pos_suffix = _match_number(pos_pattern) 

1317 neg_prefix, _, neg_suffix = _match_number(neg_pattern) 

1318 else: 

1319 pos_prefix, number, pos_suffix = _match_number(pos_pattern) 

1320 neg_prefix = f"-{pos_prefix}" 

1321 neg_suffix = pos_suffix 

1322 if 'E' in number: 

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

1324 else: 

1325 exp = None 

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

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

1328 if '.' in number: 

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

1330 else: 

1331 integer = number 

1332 fraction = '' 

1333 

1334 def parse_precision(p): 

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

1336 min = max = 0 

1337 for c in p: 

1338 if c in '@0': 

1339 min += 1 

1340 max += 1 

1341 elif c == '#': 

1342 max += 1 

1343 elif c == ',': 

1344 continue 

1345 else: 

1346 break 

1347 return min, max 

1348 

1349 int_prec = parse_precision(integer) 

1350 frac_prec = parse_precision(fraction) 

1351 if exp: 

1352 exp_plus = exp.startswith('+') 

1353 exp = exp.lstrip('+') 

1354 exp_prec = parse_precision(exp) 

1355 else: 

1356 exp_plus = None 

1357 exp_prec = None 

1358 grouping = parse_grouping(integer) 

1359 return NumberPattern( 

1360 pattern, 

1361 (pos_prefix, neg_prefix), 

1362 (pos_suffix, neg_suffix), 

1363 grouping, 

1364 int_prec, 

1365 frac_prec, 

1366 exp_prec, 

1367 exp_plus, 

1368 number, 

1369 ) 

1370 

1371 

1372class NumberPattern: 

1373 def __init__( 

1374 self, 

1375 pattern: str, 

1376 prefix: tuple[str, str], 

1377 suffix: tuple[str, str], 

1378 grouping: tuple[int, int], 

1379 int_prec: tuple[int, int], 

1380 frac_prec: tuple[int, int], 

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

1382 exp_plus: bool | None, 

1383 number_pattern: str | None = None, 

1384 ) -> None: 

1385 # Metadata of the decomposed parsed pattern. 

1386 self.pattern = pattern 

1387 self.prefix = prefix 

1388 self.suffix = suffix 

1389 self.number_pattern = number_pattern 

1390 self.grouping = grouping 

1391 self.int_prec = int_prec 

1392 self.frac_prec = frac_prec 

1393 self.exp_prec = exp_prec 

1394 self.exp_plus = exp_plus 

1395 self.scale = self.compute_scale() 

1396 

1397 def __repr__(self) -> str: 

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

1399 

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

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

1402 

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

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

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

1406 """ 

1407 scale = 0 

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

1409 scale = 2 

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

1411 scale = 3 

1412 return scale 

1413 

1414 def scientific_notation_elements( 

1415 self, 

1416 value: decimal.Decimal, 

1417 locale: Locale | str | None, 

1418 *, 

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

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

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

1422 # Normalize value to only have one lead digit. 

1423 exp = value.adjusted() 

1424 value = value * get_decimal_quantum(exp) 

1425 assert value.adjusted() == 0 

1426 

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

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

1429 # greater or equal to 1. 

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

1431 exp = exp - lead_shift 

1432 value = value * get_decimal_quantum(-lead_shift) 

1433 

1434 # Get exponent sign symbol. 

1435 exp_sign = '' 

1436 if exp < 0: 

1437 exp_sign = get_minus_sign_symbol(locale, numbering_system=numbering_system) 

1438 elif self.exp_plus: 

1439 exp_sign = get_plus_sign_symbol(locale, numbering_system=numbering_system) 

1440 

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

1442 exp = abs(exp) 

1443 

1444 return value, exp, exp_sign 

1445 

1446 def apply( 

1447 self, 

1448 value: float | decimal.Decimal | str, 

1449 locale: Locale | str | None, 

1450 currency: str | None = None, 

1451 currency_digits: bool = True, 

1452 decimal_quantization: bool = True, 

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

1454 group_separator: bool = True, 

1455 *, 

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

1457 ): 

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

1459 

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

1461 number string that is strictly following CLDR pattern definitions. 

1462 

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

1464 it will be cast to one. 

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

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

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

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

1469 :type currency: str|None 

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

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

1472 :type currency_digits: bool 

1473 :param decimal_quantization: Whether decimal numbers should be forcibly 

1474 quantized to produce a formatted output 

1475 strictly matching the CLDR definition for 

1476 the locale. 

1477 :type decimal_quantization: bool 

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

1479 for a single formatting invocation. 

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

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

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

1483 :return: Formatted decimal string. 

1484 :rtype: str 

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

1486 """ 

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

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

1489 

1490 value = value.scaleb(self.scale) 

1491 

1492 # Separate the absolute value from its sign. 

1493 is_negative = int(value.is_signed()) 

1494 value = abs(value).normalize() 

1495 

1496 # Prepare scientific notation metadata. 

1497 if self.exp_prec: 

1498 value, exp, exp_sign = self.scientific_notation_elements( 

1499 value, 

1500 locale, 

1501 numbering_system=numbering_system, 

1502 ) 

1503 

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

1505 # currency's if necessary. 

1506 if force_frac: 

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

1508 warnings.warn( 

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

1510 DeprecationWarning, 

1511 stacklevel=2, 

1512 ) 

1513 frac_prec = force_frac 

1514 elif currency and currency_digits: 

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

1516 else: 

1517 frac_prec = self.frac_prec 

1518 

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

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

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

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

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

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

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

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

1527 

1528 # Render scientific notation. 

1529 if self.exp_prec: 

1530 number = ''.join([ 

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

1532 get_exponential_symbol(locale, numbering_system=numbering_system), 

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

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

1535 ]) # fmt: skip 

1536 

1537 # Is it a significant digits pattern? 

1538 elif '@' in self.pattern: 

1539 text = self._format_significant(value, self.int_prec[0], self.int_prec[1]) 

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

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

1542 if sep: 

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

1544 

1545 # A normal number pattern. 

1546 else: 

1547 number = self._quantize_value( 

1548 value, 

1549 locale, 

1550 frac_prec, 

1551 group_separator, 

1552 numbering_system=numbering_system, 

1553 ) 

1554 

1555 retval = ''.join( 

1556 ( 

1557 self.prefix[is_negative], 

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

1559 self.suffix[is_negative], 

1560 ), 

1561 ) 

1562 

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

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

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

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

1567 

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

1569 # which are replaced with a single quote 

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

1571 

1572 return retval 

1573 

1574 # 

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

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

1577 # 

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

1579 # following steps: 

1580 # 

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

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

1583 # left of the decimal point) 

1584 # 

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

1586 # part which contained extra digits to be eliminated 

1587 # 

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

1589 # sequence of significant digits already trimmed to the maximum 

1590 # 

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

1592 # padding with zeroes on either side 

1593 # 

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

1595 exp = value.adjusted() 

1596 scale = maximum - 1 - exp 

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

1598 if scale <= 0: 

1599 result = digits + '0' * -scale 

1600 else: 

1601 intpart = digits[:-scale] 

1602 i = len(intpart) 

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

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

1605 intpart=intpart or '0', 

1606 pad='', 

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

1608 fracpart=digits[i:j], 

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

1610 ).rstrip('.') 

1611 return result 

1612 

1613 def _format_int( 

1614 self, 

1615 value: str, 

1616 min: int, 

1617 max: int, 

1618 locale: Locale | str | None, 

1619 *, 

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

1621 ) -> str: 

1622 width = len(value) 

1623 if width < min: 

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

1625 gsize = self.grouping[0] 

1626 ret = '' 

1627 symbol = get_group_symbol(locale, numbering_system=numbering_system) 

1628 while len(value) > gsize: 

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

1630 value = value[:-gsize] 

1631 gsize = self.grouping[1] 

1632 return value + ret 

1633 

1634 def _quantize_value( 

1635 self, 

1636 value: decimal.Decimal, 

1637 locale: Locale | str | None, 

1638 frac_prec: tuple[int, int], 

1639 group_separator: bool, 

1640 *, 

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

1642 ) -> str: 

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

1644 if value.is_infinite(): 

1645 return get_infinity_symbol(locale, numbering_system=numbering_system) 

1646 quantum = get_decimal_quantum(frac_prec[1]) 

1647 rounded = value.quantize(quantum) 

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

1649 integer_part = a 

1650 if group_separator: 

1651 integer_part = self._format_int( 

1652 a, 

1653 self.int_prec[0], 

1654 self.int_prec[1], 

1655 locale, 

1656 numbering_system=numbering_system, 

1657 ) 

1658 number = integer_part + self._format_frac( 

1659 b or '0', 

1660 locale=locale, 

1661 force_frac=frac_prec, 

1662 numbering_system=numbering_system, 

1663 ) 

1664 return number 

1665 

1666 def _format_frac( 

1667 self, 

1668 value: str, 

1669 locale: Locale | str | None, 

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

1671 *, 

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

1673 ) -> str: 

1674 min, max = force_frac or self.frac_prec 

1675 if len(value) < min: 

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

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

1678 return '' 

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

1680 value = value[:-1] 

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