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

413 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

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_NUMERIC``, 

11 * ``LC_ALL``, and 

12 * ``LANG`` 

13 

14 :copyright: (c) 2013-2023 by the Babel Team. 

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

16""" 

17# TODO: 

18# Padding and rounding increments in pattern: 

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

20from __future__ import annotations 

21 

22import datetime 

23import decimal 

24import re 

25import warnings 

26from typing import TYPE_CHECKING, Any, cast, overload 

27 

28from babel.core import Locale, default_locale, get_global 

29from babel.localedata import LocaleDataDict 

30 

31if TYPE_CHECKING: 

32 from typing_extensions import Literal 

33 

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 

41 def __init__(self, identifier: str) -> None: 

42 """Create the exception. 

43 :param identifier: the identifier string of the unsupported currency 

44 """ 

45 Exception.__init__(self, f"Unknown currency {identifier!r}.") 

46 

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

48 self.identifier = identifier 

49 

50 

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

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

53 

54 .. versionadded:: 2.5.0 

55 

56 :param locale: filters returned currency codes by the provided locale. 

57 Expected to be a locale instance or code. If no locale is 

58 provided, returns the list of all currencies from all 

59 locales. 

60 """ 

61 # Get locale-scoped currencies. 

62 if locale: 

63 currencies = Locale.parse(locale).currencies.keys() 

64 else: 

65 currencies = get_global('all_currencies') 

66 return set(currencies) 

67 

68 

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

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

71 

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

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

74 

75 Raises a `UnknownCurrencyError` exception if the currency is unknown to Babel. 

76 """ 

77 if currency not in list_currencies(locale): 

78 raise UnknownCurrencyError(currency) 

79 

80 

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

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

83 

84 This method always return a Boolean and never raise. 

85 """ 

86 if not currency or not isinstance(currency, str): 

87 return False 

88 try: 

89 validate_currency(currency, locale) 

90 except UnknownCurrencyError: 

91 return False 

92 return True 

93 

94 

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

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

97 

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

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

100 

101 Returns None if the currency is unknown to Babel. 

102 """ 

103 if isinstance(currency, str): 

104 currency = currency.upper() 

105 if not is_currency(currency, locale): 

106 return 

107 return currency 

108 

109 

110def get_currency_name( 

111 currency: str, 

112 count: float | decimal.Decimal | None = None, 

113 locale: Locale | str | None = LC_NUMERIC, 

114) -> str: 

115 """Return the name used by the locale for the specified currency. 

116 

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

118 u'US Dollar' 

119 

120 .. versionadded:: 0.9.4 

121 

122 :param currency: the currency code. 

123 :param count: the optional count. If provided the currency name 

124 will be pluralized to that number if possible. 

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

126 """ 

127 loc = Locale.parse(locale) 

128 if count is not None: 

129 try: 

130 plural_form = loc.plural_form(count) 

131 except (OverflowError, ValueError): 

132 plural_form = 'other' 

133 plural_names = loc._data['currency_names_plural'] 

134 if currency in plural_names: 

135 currency_plural_names = plural_names[currency] 

136 if plural_form in currency_plural_names: 

137 return currency_plural_names[plural_form] 

138 if 'other' in currency_plural_names: 

139 return currency_plural_names['other'] 

140 return loc.currencies.get(currency, currency) 

141 

142 

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

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

145 

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

147 u'$' 

148 

149 :param currency: the currency code. 

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

151 """ 

152 return Locale.parse(locale).currency_symbols.get(currency, currency) 

153 

154 

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

156 """Return currency's precision. 

157 

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

159 currency's format pattern. 

160 

161 .. versionadded:: 2.5.0 

162 

163 :param currency: the currency code. 

164 """ 

165 precisions = get_global('currency_fractions') 

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

167 

168 

169def get_currency_unit_pattern( 

170 currency: str, 

171 count: float | decimal.Decimal | None = None, 

172 locale: Locale | str | None = LC_NUMERIC, 

173) -> str: 

174 """ 

175 Return the unit pattern used for long display of a currency value 

176 for a given locale. 

177 This is a string containing ``{0}`` where the numeric part 

178 should be substituted and ``{1}`` where the currency long display 

179 name should be substituted. 

180 

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

182 u'{0} {1}' 

183 

184 .. versionadded:: 2.7.0 

185 

186 :param currency: the currency code. 

187 :param count: the optional count. If provided the unit 

188 pattern for that number will be returned. 

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

190 """ 

191 loc = Locale.parse(locale) 

192 if count is not None: 

193 plural_form = loc.plural_form(count) 

194 try: 

195 return loc._data['currency_unit_patterns'][plural_form] 

196 except LookupError: 

197 # Fall back to 'other' 

198 pass 

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_decimal_symbol(locale: Locale | str | None = LC_NUMERIC) -> str: 

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

324 

325 >>> get_decimal_symbol('en_US') 

326 u'.' 

327 

328 :param locale: the `Locale` object or locale identifier 

329 """ 

330 return Locale.parse(locale).number_symbols.get('decimal', '.') 

331 

332 

333def get_plus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str: 

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

335 

336 >>> get_plus_sign_symbol('en_US') 

337 u'+' 

338 

339 :param locale: the `Locale` object or locale identifier 

340 """ 

341 return Locale.parse(locale).number_symbols.get('plusSign', '+') 

342 

343 

344def get_minus_sign_symbol(locale: Locale | str | None = LC_NUMERIC) -> str: 

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

346 

347 >>> get_minus_sign_symbol('en_US') 

348 u'-' 

349 

350 :param locale: the `Locale` object or locale identifier 

351 """ 

352 return Locale.parse(locale).number_symbols.get('minusSign', '-') 

353 

354 

355def get_exponential_symbol(locale: Locale | str | None = LC_NUMERIC) -> str: 

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

357 

358 >>> get_exponential_symbol('en_US') 

359 u'E' 

360 

361 :param locale: the `Locale` object or locale identifier 

362 """ 

363 return Locale.parse(locale).number_symbols.get('exponential', 'E') 

364 

365 

366def get_group_symbol(locale: Locale | str | None = LC_NUMERIC) -> str: 

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

368 

369 >>> get_group_symbol('en_US') 

370 u',' 

371 

372 :param locale: the `Locale` object or locale identifier 

373 """ 

374 return Locale.parse(locale).number_symbols.get('group', ',') 

375 

376 

377def get_infinity_symbol(locale: Locale | str | None = LC_NUMERIC) -> str: 

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

379 

380 >>> get_infinity_symbol('en_US') 

381 u'∞' 

382 

383 :param locale: the `Locale` object or locale identifier 

384 """ 

385 return Locale.parse(locale).number_symbols.get('infinity', '∞') 

386 

387 

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

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

390 

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

392 u'1,099' 

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

394 u'1.099' 

395 

396 .. deprecated:: 2.6.0 

397 

398 Use babel.numbers.format_decimal() instead. 

399 

400 :param number: the number to format 

401 :param locale: the `Locale` object or locale identifier 

402 

403 

404 """ 

405 warnings.warn('Use babel.numbers.format_decimal() instead.', DeprecationWarning) 

406 return format_decimal(number, locale=locale) 

407 

408 

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

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

411 

412 Precision is extracted from the fractional part only. 

413 """ 

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

415 assert isinstance(number, decimal.Decimal) 

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

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

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

419 return 0 

420 return abs(decimal_tuple.exponent) 

421 

422 

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

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

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

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

427 

428 

429def format_decimal( 

430 number: float | decimal.Decimal | str, 

431 format: str | NumberPattern | None = None, 

432 locale: Locale | str | None = LC_NUMERIC, 

433 decimal_quantization: bool = True, 

434 group_separator: bool = True, 

435) -> str: 

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

437 

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

439 u'1.234' 

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

441 u'1.235' 

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

443 u'-1.235' 

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

445 u'1,234' 

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

447 u'1,234' 

448 

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

450 each locale: 

451 

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

453 u'12,345.5' 

454 

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

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

457 this behavior with the `decimal_quantization` parameter: 

458 

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

460 u'1.235' 

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

462 u'1.2346' 

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

464 u'12345,67' 

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

466 u'12,345.67' 

467 

468 :param number: the number to format 

469 :param format: 

470 :param locale: the `Locale` object or locale identifier 

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

472 the format pattern. Defaults to `True`. 

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

474 number format. 

475 """ 

476 locale = Locale.parse(locale) 

477 if format is None: 

478 format = locale.decimal_formats[format] 

479 pattern = parse_pattern(format) 

480 return pattern.apply( 

481 number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator) 

482 

483 

484def format_compact_decimal( 

485 number: float | decimal.Decimal | str, 

486 *, 

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

488 locale: Locale | str | None = LC_NUMERIC, 

489 fraction_digits: int = 0, 

490) -> str: 

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

492 

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

494 u'12K' 

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

496 u'12 thousand' 

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

498 u'12.34K' 

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

500 u'123万' 

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

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

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

504 u'21 милион' 

505 

506 :param number: the number to format 

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

508 :param locale: the `Locale` object or locale identifier 

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

510 """ 

511 locale = Locale.parse(locale) 

512 compact_format = locale.compact_decimal_formats[format_type] 

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

514 # Did not find a format, fall back. 

515 if format is None: 

516 format = locale.decimal_formats[None] 

517 pattern = parse_pattern(format) 

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

519 

520 

521def _get_compact_format( 

522 number: float | decimal.Decimal | str, 

523 compact_format: LocaleDataDict, 

524 locale: Locale, 

525 fraction_digits: int, 

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

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

528 The algorithm is described here: 

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

530 """ 

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

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

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

534 return number, None 

535 format = None 

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

537 if abs(number) >= magnitude: 

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

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

540 pattern = parse_pattern(format).pattern 

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

542 if pattern == "0": 

543 break 

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

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

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

547 # round to the number of fraction digits requested 

548 rounded = round(number, fraction_digits) 

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

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

551 if plural_form not in compact_format: 

552 plural_form = "other" 

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

554 plural_form = "1" 

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

556 number = rounded 

557 break 

558 return number, format 

559 

560 

561class UnknownCurrencyFormatError(KeyError): 

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

563 

564 

565def format_currency( 

566 number: float | decimal.Decimal | str, 

567 currency: str, 

568 format: str | NumberPattern | None = None, 

569 locale: Locale | str | None = LC_NUMERIC, 

570 currency_digits: bool = True, 

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

572 decimal_quantization: bool = True, 

573 group_separator: bool = True, 

574) -> str: 

575 """Return formatted currency value. 

576 

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

578 '$1,099.98' 

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

580 u'US$1.099,98' 

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

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

583 

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

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

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

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

588 

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

590 u'EUR 1,099.98' 

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

592 u'1,099.98 euros' 

593 

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

595 favours that information over the given format: 

596 

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

598 u'\\xa51,100' 

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

600 u'1.099,98' 

601 

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

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

604 

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

606 u'\\xa51,099.98' 

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

608 u'1.099,98' 

609 

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

611 from the locale can be specified: 

612 

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

614 u'\\u20ac1,099.98' 

615 

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

617 raised: 

618 

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

620 Traceback (most recent call last): 

621 ... 

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

623 

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

625 u'$101299.98' 

626 

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

628 u'$101,299.98' 

629 

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

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

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

633 

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

635 u'1.00 US dollar' 

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

637 u'1,099.98 US dollars' 

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

639 u'us ga dollar 1,099.98' 

640 

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

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

643 this behavior with the `decimal_quantization` parameter: 

644 

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

646 u'$1,099.99' 

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

648 u'$1,099.9876' 

649 

650 :param number: the number to format 

651 :param currency: the currency code 

652 :param format: the format string to use 

653 :param locale: the `Locale` object or locale identifier 

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

655 :param format_type: the currency format type to use 

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

657 the format pattern. Defaults to `True`. 

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

659 number format. 

660 

661 """ 

662 if format_type == 'name': 

663 return _format_currency_long_name(number, currency, format=format, 

664 locale=locale, currency_digits=currency_digits, 

665 decimal_quantization=decimal_quantization, group_separator=group_separator) 

666 locale = Locale.parse(locale) 

667 if format: 

668 pattern = parse_pattern(format) 

669 else: 

670 try: 

671 pattern = locale.currency_formats[format_type] 

672 except KeyError: 

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

674 

675 return pattern.apply( 

676 number, locale, currency=currency, currency_digits=currency_digits, 

677 decimal_quantization=decimal_quantization, group_separator=group_separator) 

678 

679 

680def _format_currency_long_name( 

681 number: float | decimal.Decimal | str, 

682 currency: str, 

683 format: str | NumberPattern | None = None, 

684 locale: Locale | str | None = LC_NUMERIC, 

685 currency_digits: bool = True, 

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

687 decimal_quantization: bool = True, 

688 group_separator: bool = True, 

689) -> str: 

690 # Algorithm described here: 

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

692 locale = Locale.parse(locale) 

693 # Step 1. 

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

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

696 # Step 2. 

697 

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

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

700 

701 # Step 3. 

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

703 

704 # Step 4. 

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

706 

707 # Step 5. 

708 if not format: 

709 format = locale.decimal_formats[format] 

710 

711 pattern = parse_pattern(format) 

712 

713 number_part = pattern.apply( 

714 number, locale, currency=currency, currency_digits=currency_digits, 

715 decimal_quantization=decimal_quantization, group_separator=group_separator) 

716 

717 return unit_pattern.format(number_part, display_name) 

718 

719 

720def format_compact_currency( 

721 number: float | decimal.Decimal | str, 

722 currency: str, 

723 *, 

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

725 locale: Locale | str | None = LC_NUMERIC, 

726 fraction_digits: int = 0 

727) -> str: 

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

729 

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

731 u'$12K' 

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

733 u'$123.46M' 

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

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

736 

737 :param number: the number to format 

738 :param currency: the currency code 

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

740 :param locale: the `Locale` object or locale identifier 

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

742 """ 

743 locale = Locale.parse(locale) 

744 try: 

745 compact_format = locale.compact_currency_formats[format_type] 

746 except KeyError as error: 

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

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

749 # Did not find a format, fall back. 

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

751 # find first format that has a currency symbol 

752 for magnitude in compact_format['other']: 

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

754 if '¤' not in format: 

755 continue 

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

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

758 # compress adjacent spaces into one 

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

760 break 

761 if format is None: 

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

763 pattern = parse_pattern(format) 

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

765 

766 

767def format_percent( 

768 number: float | decimal.Decimal | str, 

769 format: str | NumberPattern | None = None, 

770 locale: Locale | str | None = LC_NUMERIC, 

771 decimal_quantization: bool = True, 

772 group_separator: bool = True, 

773) -> str: 

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

775 

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

777 u'34%' 

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

779 u'2,512%' 

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

781 u'2\\xa0512\\xa0%' 

782 

783 The format pattern can also be specified explicitly: 

784 

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

786 u'25,123\u2030' 

787 

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

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

790 this behavior with the `decimal_quantization` parameter: 

791 

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

793 u'2,399%' 

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

795 u'2,398.76%' 

796 

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

798 u'22929112%' 

799 

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

801 u'22.929.112%' 

802 

803 :param number: the percent number to format 

804 :param format: 

805 :param locale: the `Locale` object or locale identifier 

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

807 the format pattern. Defaults to `True`. 

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

809 number format. 

810 """ 

811 locale = Locale.parse(locale) 

812 if not format: 

813 format = locale.percent_formats[format] 

814 pattern = parse_pattern(format) 

815 return pattern.apply( 

816 number, locale, decimal_quantization=decimal_quantization, group_separator=group_separator) 

817 

818 

819def format_scientific( 

820 number: float | decimal.Decimal | str, 

821 format: str | NumberPattern | None = None, 

822 locale: Locale | str | None = LC_NUMERIC, 

823 decimal_quantization: bool = True, 

824) -> str: 

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

826 

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

828 u'1E4' 

829 

830 The format pattern can also be specified explicitly: 

831 

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

833 u'1.23E06' 

834 

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

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

837 this behavior with the `decimal_quantization` parameter: 

838 

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

840 u'1.23E3' 

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

842 u'1.2349876E3' 

843 

844 :param number: the number to format 

845 :param format: 

846 :param locale: the `Locale` object or locale identifier 

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

848 the format pattern. Defaults to `True`. 

849 """ 

850 locale = Locale.parse(locale) 

851 if not format: 

852 format = locale.scientific_formats[format] 

853 pattern = parse_pattern(format) 

854 return pattern.apply( 

855 number, locale, decimal_quantization=decimal_quantization) 

856 

857 

858class NumberFormatError(ValueError): 

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

860 

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

862 super().__init__(message) 

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

864 self.suggestions = suggestions 

865 

866 

867def parse_number(string: str, locale: Locale | str | None = LC_NUMERIC) -> int: 

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

869 

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

871 1099 

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

873 1099 

874 

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

876 

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

878 Traceback (most recent call last): 

879 ... 

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

881 

882 :param string: the string to parse 

883 :param locale: the `Locale` object or locale identifier 

884 :return: the parsed number 

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

886 """ 

887 try: 

888 return int(string.replace(get_group_symbol(locale), '')) 

889 except ValueError as ve: 

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

891 

892 

893def parse_decimal(string: str, locale: Locale | str | None = LC_NUMERIC, strict: bool = False) -> decimal.Decimal: 

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

895 

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

897 Decimal('1099.98') 

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

899 Decimal('1099.98') 

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

901 Decimal('12345.123') 

902 

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

904 

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

906 Traceback (most recent call last): 

907 ... 

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

909 

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

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

912 

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

914 Traceback (most recent call last): 

915 ... 

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

917 

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

919 Traceback (most recent call last): 

920 ... 

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

922 

923 :param string: the string to parse 

924 :param locale: the `Locale` object or locale identifier 

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

926 accepted or rejected 

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

928 decimal number 

929 """ 

930 locale = Locale.parse(locale) 

931 group_symbol = get_group_symbol(locale) 

932 decimal_symbol = get_decimal_symbol(locale) 

933 

934 if not strict and ( 

935 group_symbol == '\xa0' and # if the grouping symbol is U+00A0 NO-BREAK SPACE, 

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

937 ' ' in string # but it does contain a space instead, 

938 ): 

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

940 string = string.replace(' ', group_symbol) 

941 

942 try: 

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

944 .replace(decimal_symbol, '.')) 

945 except decimal.InvalidOperation as exc: 

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

947 if strict and group_symbol in string: 

948 proper = format_decimal(parsed, locale=locale, decimal_quantization=False) 

949 if string != proper and string.rstrip('0') != (proper + decimal_symbol): 

950 try: 

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

952 .replace(group_symbol, '.')) 

953 except decimal.InvalidOperation as exc: 

954 raise NumberFormatError( 

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

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

957 suggestions=[proper], 

958 ) from exc 

959 else: 

960 proper_alt = format_decimal(parsed_alt, locale=locale, decimal_quantization=False) 

961 if proper_alt == proper: 

962 raise NumberFormatError( 

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

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

965 suggestions=[proper], 

966 ) 

967 else: 

968 raise NumberFormatError( 

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

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

971 suggestions=[proper, proper_alt], 

972 ) 

973 return parsed 

974 

975 

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

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

978 

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

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

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

982 

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

984 

985 

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

987 """Parse primary and secondary digit grouping 

988 

989 >>> parse_grouping('##') 

990 (1000, 1000) 

991 >>> parse_grouping('#,###') 

992 (3, 3) 

993 >>> parse_grouping('#,####,###') 

994 (3, 4) 

995 """ 

996 width = len(p) 

997 g1 = p.rfind(',') 

998 if g1 == -1: 

999 return 1000, 1000 

1000 g1 = width - g1 - 1 

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

1002 if g2 == -1: 

1003 return g1, g1 

1004 g2 = width - g1 - g2 - 2 

1005 return g1, g2 

1006 

1007 

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

1009 """Parse number format patterns""" 

1010 if isinstance(pattern, NumberPattern): 

1011 return pattern 

1012 

1013 def _match_number(pattern): 

1014 rv = number_re.search(pattern) 

1015 if rv is None: 

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

1017 return rv.groups() 

1018 

1019 pos_pattern = pattern 

1020 

1021 # Do we have a negative subpattern? 

1022 if ';' in pattern: 

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

1024 pos_prefix, number, pos_suffix = _match_number(pos_pattern) 

1025 neg_prefix, _, neg_suffix = _match_number(neg_pattern) 

1026 else: 

1027 pos_prefix, number, pos_suffix = _match_number(pos_pattern) 

1028 neg_prefix = f"-{pos_prefix}" 

1029 neg_suffix = pos_suffix 

1030 if 'E' in number: 

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

1032 else: 

1033 exp = None 

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

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

1036 if '.' in number: 

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

1038 else: 

1039 integer = number 

1040 fraction = '' 

1041 

1042 def parse_precision(p): 

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

1044 min = max = 0 

1045 for c in p: 

1046 if c in '@0': 

1047 min += 1 

1048 max += 1 

1049 elif c == '#': 

1050 max += 1 

1051 elif c == ',': 

1052 continue 

1053 else: 

1054 break 

1055 return min, max 

1056 

1057 int_prec = parse_precision(integer) 

1058 frac_prec = parse_precision(fraction) 

1059 if exp: 

1060 exp_plus = exp.startswith('+') 

1061 exp = exp.lstrip('+') 

1062 exp_prec = parse_precision(exp) 

1063 else: 

1064 exp_plus = None 

1065 exp_prec = None 

1066 grouping = parse_grouping(integer) 

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

1068 (pos_suffix, neg_suffix), grouping, 

1069 int_prec, frac_prec, 

1070 exp_prec, exp_plus, number) 

1071 

1072 

1073class NumberPattern: 

1074 

1075 def __init__( 

1076 self, 

1077 pattern: str, 

1078 prefix: tuple[str, str], 

1079 suffix: tuple[str, str], 

1080 grouping: tuple[int, int], 

1081 int_prec: tuple[int, int], 

1082 frac_prec: tuple[int, int], 

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

1084 exp_plus: bool | None, 

1085 number_pattern: str | None = None, 

1086 ) -> None: 

1087 # Metadata of the decomposed parsed pattern. 

1088 self.pattern = pattern 

1089 self.prefix = prefix 

1090 self.suffix = suffix 

1091 self.number_pattern = number_pattern 

1092 self.grouping = grouping 

1093 self.int_prec = int_prec 

1094 self.frac_prec = frac_prec 

1095 self.exp_prec = exp_prec 

1096 self.exp_plus = exp_plus 

1097 self.scale = self.compute_scale() 

1098 

1099 def __repr__(self) -> str: 

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

1101 

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

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

1104 

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

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

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

1108 """ 

1109 scale = 0 

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

1111 scale = 2 

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

1113 scale = 3 

1114 return scale 

1115 

1116 def scientific_notation_elements(self, value: decimal.Decimal, locale: Locale | str | None) -> tuple[decimal.Decimal, int, str]: 

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

1118 """ 

1119 # Normalize value to only have one lead digit. 

1120 exp = value.adjusted() 

1121 value = value * get_decimal_quantum(exp) 

1122 assert value.adjusted() == 0 

1123 

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

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

1126 # greater or equal to 1. 

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

1128 exp = exp - lead_shift 

1129 value = value * get_decimal_quantum(-lead_shift) 

1130 

1131 # Get exponent sign symbol. 

1132 exp_sign = '' 

1133 if exp < 0: 

1134 exp_sign = get_minus_sign_symbol(locale) 

1135 elif self.exp_plus: 

1136 exp_sign = get_plus_sign_symbol(locale) 

1137 

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

1139 exp = abs(exp) 

1140 

1141 return value, exp, exp_sign 

1142 

1143 def apply( 

1144 self, 

1145 value: float | decimal.Decimal | str, 

1146 locale: Locale | str | None, 

1147 currency: str | None = None, 

1148 currency_digits: bool = True, 

1149 decimal_quantization: bool = True, 

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

1151 group_separator: bool = True, 

1152 ): 

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

1154 

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

1156 number string that is strictly following CLDR pattern definitions. 

1157 

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

1159 it will be cast to one. 

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

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

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

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

1164 :type currency: str|None 

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

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

1167 :type currency_digits: bool 

1168 :param decimal_quantization: Whether decimal numbers should be forcibly 

1169 quantized to produce a formatted output 

1170 strictly matching the CLDR definition for 

1171 the locale. 

1172 :type decimal_quantization: bool 

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

1174 for a single formatting invocation. 

1175 :return: Formatted decimal string. 

1176 :rtype: str 

1177 """ 

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

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

1180 

1181 value = value.scaleb(self.scale) 

1182 

1183 # Separate the absolute value from its sign. 

1184 is_negative = int(value.is_signed()) 

1185 value = abs(value).normalize() 

1186 

1187 # Prepare scientific notation metadata. 

1188 if self.exp_prec: 

1189 value, exp, exp_sign = self.scientific_notation_elements(value, locale) 

1190 

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

1192 # currency's if necessary. 

1193 if force_frac: 

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

1195 warnings.warn('The force_frac parameter to NumberPattern.apply() is deprecated.', DeprecationWarning) 

1196 frac_prec = force_frac 

1197 elif currency and currency_digits: 

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

1199 else: 

1200 frac_prec = self.frac_prec 

1201 

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

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

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

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

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

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

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

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

1210 

1211 # Render scientific notation. 

1212 if self.exp_prec: 

1213 number = ''.join([ 

1214 self._quantize_value(value, locale, frac_prec, group_separator), 

1215 get_exponential_symbol(locale), 

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

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

1218 ]) 

1219 

1220 # Is it a significant digits pattern? 

1221 elif '@' in self.pattern: 

1222 text = self._format_significant(value, 

1223 self.int_prec[0], 

1224 self.int_prec[1]) 

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

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

1227 if sep: 

1228 number += get_decimal_symbol(locale) + b 

1229 

1230 # A normal number pattern. 

1231 else: 

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

1233 

1234 retval = ''.join([ 

1235 self.prefix[is_negative], 

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

1237 self.suffix[is_negative]]) 

1238 

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

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

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

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

1243 

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

1245 # which are replaced with a single quote 

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

1247 

1248 return retval 

1249 

1250 # 

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

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

1253 # 

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

1255 # following steps: 

1256 # 

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

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

1259 # left of the decimal point) 

1260 # 

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

1262 # part which contained extra digits to be eliminated 

1263 # 

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

1265 # sequence of significant digits already trimmed to the maximum 

1266 # 

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

1268 # padding with zeroes on either side 

1269 # 

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

1271 exp = value.adjusted() 

1272 scale = maximum - 1 - exp 

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

1274 if scale <= 0: 

1275 result = digits + '0' * -scale 

1276 else: 

1277 intpart = digits[:-scale] 

1278 i = len(intpart) 

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

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

1281 intpart=intpart or '0', 

1282 pad='', 

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

1284 fracpart=digits[i:j], 

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

1286 ).rstrip('.') 

1287 return result 

1288 

1289 def _format_int(self, value: str, min: int, max: int, locale: Locale | str | None) -> str: 

1290 width = len(value) 

1291 if width < min: 

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

1293 gsize = self.grouping[0] 

1294 ret = '' 

1295 symbol = get_group_symbol(locale) 

1296 while len(value) > gsize: 

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

1298 value = value[:-gsize] 

1299 gsize = self.grouping[1] 

1300 return value + ret 

1301 

1302 def _quantize_value(self, value: decimal.Decimal, locale: Locale | str | None, frac_prec: tuple[int, int], group_separator: bool) -> str: 

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

1304 if value.is_infinite(): 

1305 return get_infinity_symbol(locale) 

1306 quantum = get_decimal_quantum(frac_prec[1]) 

1307 rounded = value.quantize(quantum) 

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

1309 integer_part = a 

1310 if group_separator: 

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

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

1313 return number 

1314 

1315 def _format_frac(self, value: str, locale: Locale | str | None, force_frac: tuple[int, int] | None = None) -> str: 

1316 min, max = force_frac or self.frac_prec 

1317 if len(value) < min: 

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

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

1320 return '' 

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

1322 value = value[:-1] 

1323 return get_decimal_symbol(locale) + value