Coverage for adhoc-cicd-odoo-odoo / odoo / _monkeypatches / num2words.py: 17%

562 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-09 18:15 +0000

1import decimal 

2import logging 

3import math 

4import re 

5from collections import OrderedDict 

6from decimal import ROUND_HALF_UP, Decimal 

7from math import floor 

8 

9from odoo.release import MIN_PY_VERSION 

10 

11# The following section of the code is used to monkey patch 

12# the Arabic class of num2words package as there are some problems 

13# upgrading the package to the newer version that fixed the bugs 

14# so a temporary fix was to patch the old version with the code 

15# from the new version manually. 

16# The code is taken from num2words package: https://github.com/savoirfairelinux/num2words 

17 

18 

19CURRENCY_SR = [("ريال", "ريالان", "ريالات", "ريالاً"), 

20 ("هللة", "هللتان", "هللات", "هللة")] 

21CURRENCY_EGP = [("جنيه", "جنيهان", "جنيهات", "جنيهاً"), 

22 ("قرش", "قرشان", "قروش", "قرش")] 

23CURRENCY_KWD = [("دينار", "ديناران", "دينارات", "ديناراً"), 

24 ("فلس", "فلسان", "فلس", "فلس")] 

25 

26ARABIC_ONES = [ 

27 "", "واحد", "اثنان", "ثلاثة", "أربعة", "خمسة", "ستة", "سبعة", "ثمانية", 

28 "تسعة", 

29 "عشرة", "أحد عشر", "اثنا عشر", "ثلاثة عشر", "أربعة عشر", "خمسة عشر", 

30 "ستة عشر", "سبعة عشر", "ثمانية عشر", 

31 "تسعة عشر" 

32] 

33 

34 

35class Num2Word_Base: 

36 CURRENCY_FORMS = {} 

37 CURRENCY_ADJECTIVES = {} 

38 

39 def __init__(self): 

40 self.is_title = False 

41 self.precision = 2 

42 self.exclude_title = [] 

43 self.negword = "(-) " 

44 self.pointword = "(.)" 

45 self.errmsg_nonnum = "type: %s not in [long, int, float]" 

46 self.errmsg_floatord = "Cannot treat float %s as ordinal." 

47 self.errmsg_negord = "Cannot treat negative num %s as ordinal." 

48 self.errmsg_toobig = "abs(%s) must be less than %s." 

49 

50 self.setup() 

51 

52 # uses cards 

53 if any(hasattr(self, field) for field in 53 ↛ 55line 53 didn't jump to line 55 because the condition on line 53 was never true

54 ['high_numwords', 'mid_numwords', 'low_numwords']): 

55 self.cards = OrderedDict() 

56 self.set_numwords() 

57 self.MAXVAL = 1000 * next(iter(self.cards.keys())) 

58 

59 def set_numwords(self): 

60 self.set_high_numwords(self.high_numwords) 

61 self.set_mid_numwords(self.mid_numwords) 

62 self.set_low_numwords(self.low_numwords) 

63 

64 def set_high_numwords(self, *args): 

65 raise NotImplementedError 

66 

67 def set_mid_numwords(self, mid): 

68 for key, val in mid: 

69 self.cards[key] = val 

70 

71 def set_low_numwords(self, numwords): 

72 for word, n in zip(numwords, range(len(numwords) - 1, -1, -1)): 

73 self.cards[n] = word 

74 

75 def splitnum(self, value): 

76 for elem in self.cards: 

77 if elem > value: 

78 continue 

79 

80 out = [] 

81 if value == 0: 

82 div, mod = 1, 0 

83 else: 

84 div, mod = divmod(value, elem) 

85 

86 if div == 1: 

87 out.append((self.cards[1], 1)) 

88 else: 

89 if div == value: # The system tallies, eg Roman Numerals 

90 return [(div * self.cards[elem], div * elem)] 

91 out.append(self.splitnum(div)) 

92 

93 out.append((self.cards[elem], elem)) 

94 

95 if mod: 

96 out.append(self.splitnum(mod)) 

97 

98 return out 

99 

100 def parse_minus(self, num_str): 

101 """Detach minus and return it as symbol with new num_str.""" 

102 if num_str.startswith('-'): 

103 # Extra spacing to compensate if there is no minus. 

104 return '%s ' % self.negword.strip(), num_str[1:] 

105 return '', num_str 

106 

107 def str_to_number(self, value): 

108 return Decimal(value) 

109 

110 def to_cardinal(self, value): 

111 try: 

112 assert int(value) == value 

113 except (ValueError, TypeError, AssertionError): 

114 return self.to_cardinal_float(value) 

115 

116 out = "" 

117 if value < 0: 

118 value = abs(value) 

119 out = "%s " % self.negword.strip() 

120 

121 if value >= self.MAXVAL: 

122 raise OverflowError(self.errmsg_toobig % (value, self.MAXVAL)) 

123 

124 val = self.splitnum(value) 

125 words, _ = self.clean(val) 

126 return self.title(out + words) 

127 

128 def float2tuple(self, value): 

129 pre = int(value) 

130 

131 # Simple way of finding decimal places to update the precision 

132 self.precision = abs(Decimal(str(value)).as_tuple().exponent) 

133 

134 post = abs(value - pre) * 10**self.precision 

135 if abs(round(post) - post) < 0.01: 

136 # We generally floor all values beyond our precision (rather than 

137 # rounding), but in cases where we have something like 1.239999999, 

138 # which is probably due to python's handling of floats, we actually 

139 # want to consider it as 1.24 instead of 1.23 

140 post = int(round(post)) 

141 else: 

142 post = int(math.floor(post)) 

143 

144 return pre, post 

145 

146 def to_cardinal_float(self, value): 

147 try: 

148 _ = float(value) == value 

149 except (ValueError, TypeError, AssertionError, AttributeError): 

150 raise TypeError(self.errmsg_nonnum % value) 

151 

152 pre, post = self.float2tuple(float(value)) 

153 

154 post = str(post) 

155 post = '0' * (self.precision - len(post)) + post 

156 

157 out = [self.to_cardinal(pre)] 

158 if self.precision: 

159 out.append(self.title(self.pointword)) 

160 

161 for i in range(self.precision): 

162 curr = int(post[i]) 

163 out.append(to_s(self.to_cardinal(curr))) 

164 

165 return " ".join(out) 

166 

167 def merge(self, left, right): 

168 raise NotImplementedError 

169 

170 def clean(self, val): 

171 out = val 

172 while len(val) != 1: 

173 out = [] 

174 left, right = val[:2] 

175 if isinstance(left, tuple) and isinstance(right, tuple): 

176 out.append(self.merge(left, right)) 

177 if val[2:]: 

178 out.append(val[2:]) 

179 else: 

180 for elem in val: 

181 if isinstance(elem, list): 

182 if len(elem) == 1: 

183 out.append(elem[0]) 

184 else: 

185 out.append(self.clean(elem)) 

186 else: 

187 out.append(elem) 

188 val = out 

189 return out[0] 

190 

191 def title(self, value): 

192 if self.is_title: 

193 out = [] 

194 value = value.split() 

195 for word in value: 

196 if word in self.exclude_title: 

197 out.append(word) 

198 else: 

199 out.append(word[0].upper() + word[1:]) 

200 value = " ".join(out) 

201 return value 

202 

203 def verify_ordinal(self, value): 

204 if not value == int(value): 

205 raise TypeError(self.errmsg_floatord % value) 

206 if not abs(value) == value: 

207 raise TypeError(self.errmsg_negord % value) 

208 

209 def to_ordinal(self, value): 

210 return self.to_cardinal(value) 

211 

212 def to_ordinal_num(self, value): 

213 return value 

214 

215 # Trivial version 

216 def inflect(self, value, text): 

217 text = text.split("/") 

218 if value == 1: 

219 return text[0] 

220 return "".join(text) 

221 

222 # //CHECK: generalise? Any others like pounds/shillings/pence? 

223 def to_splitnum(self, val, hightxt="", lowtxt="", jointxt="", 

224 divisor=100, longval=True, cents=True): 

225 out = [] 

226 

227 if isinstance(val, float): 

228 high, low = self.float2tuple(val) 

229 else: 

230 try: 

231 high, low = val 

232 except TypeError: 

233 high, low = divmod(val, divisor) 

234 

235 if high: 

236 hightxt = self.title(self.inflect(high, hightxt)) 

237 out.append(self.to_cardinal(high)) 

238 if low: 

239 if longval: 

240 if hightxt: 

241 out.append(hightxt) 

242 if jointxt: 

243 out.append(self.title(jointxt)) 

244 elif hightxt: 

245 out.append(hightxt) 

246 

247 if low: 

248 if cents: 

249 out.append(self.to_cardinal(low)) 

250 else: 

251 out.append("%02d" % low) 

252 if lowtxt and longval: 

253 out.append(self.title(self.inflect(low, lowtxt))) 

254 

255 return " ".join(out) 

256 

257 def to_year(self, value, **kwargs): 

258 return self.to_cardinal(value) 

259 

260 def pluralize(self, n, forms): 

261 """ 

262 Should resolve gettext form: 

263 http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html 

264 """ 

265 raise NotImplementedError 

266 

267 def _money_verbose(self, number, currency): 

268 return self.to_cardinal(number) 

269 

270 def _cents_verbose(self, number, currency): 

271 return self.to_cardinal(number) 

272 

273 def _cents_terse(self, number, currency): 

274 return "%02d" % number 

275 

276 def to_currency(self, val, currency='EUR', cents=True, separator=',', 

277 adjective=False): 

278 """ 

279 Args: 

280 val: Numeric value 

281 currency (str): Currency code 

282 cents (bool): Verbose cents 

283 separator (str): Cent separator 

284 adjective (bool): Prefix currency name with adjective 

285 Returns: 

286 str: Formatted string 

287 

288 """ 

289 left, right, is_negative = parse_currency_parts(val) 

290 

291 try: 

292 cr1, cr2 = self.CURRENCY_FORMS[currency] 

293 

294 except KeyError: 

295 raise NotImplementedError( 

296 'Currency code "%s" not implemented for "%s"' % 

297 (currency, self.__class__.__name__)) 

298 

299 if adjective and currency in self.CURRENCY_ADJECTIVES: 

300 cr1 = prefix_currency(self.CURRENCY_ADJECTIVES[currency], cr1) 

301 

302 minus_str = "%s " % self.negword.strip() if is_negative else "" 

303 money_str = self._money_verbose(left, currency) 

304 cents_str = self._cents_verbose(right, currency) \ 

305 if cents else self._cents_terse(right, currency) 

306 

307 return '%s%s %s%s %s %s' % ( 

308 minus_str, 

309 money_str, 

310 self.pluralize(left, cr1), 

311 separator, 

312 cents_str, 

313 self.pluralize(right, cr2) 

314 ) 

315 

316 def setup(self): 

317 pass 

318 

319 

320class Num2Word_AR_Fixed(Num2Word_Base): 

321 errmsg_toobig = "abs(%s) must be less than %s." 

322 MAXVAL = 10**51 

323 

324 def __init__(self): 

325 super().__init__() 

326 

327 self.number = 0 

328 self.arabicPrefixText = "" 

329 self.arabicSuffixText = "" 

330 self.integer_value = 0 

331 self._decimalValue = "" 

332 self.partPrecision = 2 

333 self.currency_unit = CURRENCY_SR[0] 

334 self.currency_subunit = CURRENCY_SR[1] 

335 self.isCurrencyPartNameFeminine = True 

336 self.isCurrencyNameFeminine = False 

337 self.separator = 'و' 

338 

339 self.arabicOnes = ARABIC_ONES 

340 self.arabicFeminineOnes = [ 

341 "", "إحدى", "اثنتان", "ثلاث", "أربع", "خمس", "ست", "سبع", "ثمان", 

342 "تسع", 

343 "عشر", "إحدى عشرة", "اثنتا عشرة", "ثلاث عشرة", "أربع عشرة", 

344 "خمس عشرة", "ست عشرة", "سبع عشرة", "ثماني عشرة", 

345 "تسع عشرة" 

346 ] 

347 self.arabicOrdinal = [ 

348 "", "اول", "ثاني", "ثالث", "رابع", "خامس", "سادس", "سابع", "ثامن", 

349 "تاسع", "عاشر", "حادي عشر", "ثاني عشر", "ثالث عشر", "رابع عشر", 

350 "خامس عشر", "سادس عشر", "سابع عشر", "ثامن عشر", "تاسع عشر" 

351 ] 

352 self.arabicTens = [ 

353 "عشرون", "ثلاثون", "أربعون", "خمسون", "ستون", "سبعون", "ثمانون", 

354 "تسعون" 

355 ] 

356 self.arabicHundreds = [ 

357 "", "مائة", "مئتان", "ثلاثمائة", "أربعمائة", "خمسمائة", "ستمائة", 

358 "سبعمائة", "ثمانمائة", "تسعمائة" 

359 ] 

360 

361 self.arabicAppendedTwos = [ 

362 "مئتا", "ألفا", "مليونا", "مليارا", "تريليونا", "كوادريليونا", 

363 "كوينتليونا", "سكستيليونا", "سبتيليونا", "أوكتيليونا ", 

364 "نونيليونا", "ديسيليونا", "أندسيليونا", "دوديسيليونا", 

365 "تريديسيليونا", "كوادريسيليونا", "كوينتينيليونا" 

366 ] 

367 self.arabicTwos = [ 

368 "مئتان", "ألفان", "مليونان", "ملياران", "تريليونان", 

369 "كوادريليونان", "كوينتليونان", "سكستيليونان", "سبتيليونان", 

370 "أوكتيليونان ", "نونيليونان ", "ديسيليونان", "أندسيليونان", 

371 "دوديسيليونان", "تريديسيليونان", "كوادريسيليونان", "كوينتينيليونان" 

372 ] 

373 self.arabicGroup = [ 

374 "مائة", "ألف", "مليون", "مليار", "تريليون", "كوادريليون", 

375 "كوينتليون", "سكستيليون", "سبتيليون", "أوكتيليون", "نونيليون", 

376 "ديسيليون", "أندسيليون", "دوديسيليون", "تريديسيليون", 

377 "كوادريسيليون", "كوينتينيليون" 

378 ] 

379 self.arabicAppendedGroup = [ 

380 "", "ألفاً", "مليوناً", "ملياراً", "تريليوناً", "كوادريليوناً", 

381 "كوينتليوناً", "سكستيليوناً", "سبتيليوناً", "أوكتيليوناً", 

382 "نونيليوناً", "ديسيليوناً", "أندسيليوناً", "دوديسيليوناً", 

383 "تريديسيليوناً", "كوادريسيليوناً", "كوينتينيليوناً" 

384 ] 

385 self.arabicPluralGroups = [ 

386 "", "آلاف", "ملايين", "مليارات", "تريليونات", "كوادريليونات", 

387 "كوينتليونات", "سكستيليونات", "سبتيليونات", "أوكتيليونات", 

388 "نونيليونات", "ديسيليونات", "أندسيليونات", "دوديسيليونات", 

389 "تريديسيليونات", "كوادريسيليونات", "كوينتينيليونات" 

390 ] 

391 assert len(self.arabicAppendedGroup) == len(self.arabicGroup) 

392 assert len(self.arabicPluralGroups) == len(self.arabicGroup) 

393 assert len(self.arabicAppendedTwos) == len(self.arabicTwos) 

394 

395 def number_to_arabic(self, arabic_prefix_text, arabic_suffix_text): 

396 self.arabicPrefixText = arabic_prefix_text 

397 self.arabicSuffixText = arabic_suffix_text 

398 self.extract_integer_and_decimal_parts() 

399 

400 def extract_integer_and_decimal_parts(self): 

401 splits = re.split('\\.', str(self.number)) 

402 

403 self.integer_value = int(splits[0]) 

404 if len(splits) > 1: 

405 self._decimalValue = int(self.decimal_value(splits[1])) 

406 else: 

407 self._decimalValue = 0 

408 

409 def decimal_value(self, decimal_part): 

410 if self.partPrecision is not len(decimal_part): 

411 decimal_part_length = len(decimal_part) 

412 

413 decimal_part_builder = decimal_part 

414 for _ in range(0, self.partPrecision - decimal_part_length): 

415 decimal_part_builder += '0' 

416 decimal_part = decimal_part_builder 

417 

418 if len(decimal_part) <= self.partPrecision: 

419 dec = len(decimal_part) 

420 else: 

421 dec = self.partPrecision 

422 result = decimal_part[0: dec] 

423 else: 

424 result = decimal_part 

425 

426 # The following is useless (never happens) 

427 # for i in range(len(result), self.partPrecision): 

428 # result += '0' 

429 return result 

430 

431 def digit_feminine_status(self, digit, group_level): 

432 if group_level == -1: 

433 if self.isCurrencyPartNameFeminine: 

434 return self.arabicFeminineOnes[int(digit)] 

435 else: 

436 # Note: this never happens 

437 return self.arabicOnes[int(digit)] 

438 elif group_level == 0: 

439 if self.isCurrencyNameFeminine: 

440 return self.arabicFeminineOnes[int(digit)] 

441 else: 

442 return self.arabicOnes[int(digit)] 

443 else: 

444 return self.arabicOnes[int(digit)] 

445 

446 def process_arabic_group(self, group_number, group_level, 

447 remaining_number): 

448 tens = Decimal(group_number) % Decimal(100) 

449 hundreds = Decimal(group_number) / Decimal(100) 

450 ret_val = "" 

451 

452 if int(hundreds) > 0: 

453 if tens == 0 and int(hundreds) == 2: 

454 ret_val = f"{self.arabicAppendedTwos[0]}" 

455 else: 

456 ret_val = f"{self.arabicHundreds[int(hundreds)]}" 

457 if ret_val and tens != 0: 

458 ret_val += " و " 

459 

460 if tens > 0: 

461 if tens < 20: 

462 # if int(group_level) >= len(self.arabicTwos): 

463 # raise OverflowError(self.errmsg_toobig % 

464 # (self.number, self.MAXVAL)) 

465 assert int(group_level) < len(self.arabicTwos) 

466 if tens == 2 and int(hundreds) == 0 and group_level > 0: 

467 power = int(math.log10(self.integer_value)) 

468 if self.integer_value > 10 and power % 3 == 0 and \ 

469 self.integer_value == 2 * (10 ** power): 

470 ret_val = f"{self.arabicAppendedTwos[int(group_level)]}" 

471 else: 

472 ret_val = f"{self.arabicTwos[int(group_level)]}" 

473 else: 

474 if tens == 1 and group_level > 0 and hundreds == 0: 

475 # Note: this never happens 

476 # (hundreds == 0 only if group_number is 0) 

477 ret_val += "" 

478 elif (tens == 1 or tens == 2) and ( 

479 group_level == 0 or group_level == -1) and \ 

480 hundreds == 0 and remaining_number == 0: 

481 # Note: this never happens (idem) 

482 ret_val += "" 

483 elif tens == 1 and group_level > 0: 

484 ret_val += self.arabicGroup[int(group_level)] 

485 else: 

486 ret_val += self.digit_feminine_status(int(tens), 

487 group_level) 

488 else: 

489 ones = tens % 10 

490 tens = (tens / 10) - 2 

491 if ones > 0: 

492 ret_val += self.digit_feminine_status(ones, group_level) 

493 if ret_val and ones != 0: 

494 ret_val += " و " 

495 

496 ret_val += self.arabicTens[int(tens)] 

497 

498 return ret_val 

499 

500 # We use this instead of built-in `abs` function, 

501 # because `abs` suffers from loss of precision for big numbers 

502 def absolute(self, number): 

503 return number if number >= 0 else -number 

504 

505 # We use this instead of `"{:09d}".format(number)`, 

506 # because the string conversion suffers from loss of 

507 # precision for big numbers 

508 def to_str(self, number): 

509 integer = int(number) 

510 if integer == number: 

511 return str(integer) 

512 decimal = round((number - integer) * 10**9) 

513 return f"{integer}.{decimal:09d}" 

514 

515 def convert(self, value): 

516 self.number = self.to_str(value) 

517 self.number_to_arabic(self.arabicPrefixText, self.arabicSuffixText) 

518 return self.convert_to_arabic() 

519 

520 def convert_to_arabic(self): 

521 temp_number = Decimal(self.number) 

522 

523 if temp_number == Decimal(0): 

524 return "صفر" 

525 

526 decimal_string = self.process_arabic_group(self._decimalValue, 

527 -1, 

528 Decimal(0)) 

529 ret_val = "" 

530 group = 0 

531 

532 while temp_number > Decimal(0): 

533 

534 temp_number_dec = Decimal(str(temp_number)) 

535 try: 

536 number_to_process = int(temp_number_dec % Decimal(str(1000))) 

537 except decimal.InvalidOperation: 

538 decimal.getcontext().prec = len( 

539 temp_number_dec.as_tuple().digits 

540 ) 

541 number_to_process = int(temp_number_dec % Decimal(str(1000))) 

542 

543 temp_number = int(temp_number_dec / Decimal(1000)) 

544 

545 group_description = \ 

546 self.process_arabic_group(number_to_process, 

547 group, 

548 Decimal(floor(temp_number))) 

549 if group_description: 

550 if group > 0: 

551 if ret_val: 

552 ret_val = f"و {ret_val}" 

553 if number_to_process != 2 and number_to_process != 1: 

554 # if group >= len(self.arabicGroup): 

555 # raise OverflowError(self.errmsg_toobig % 

556 # (self.number, self.MAXVAL) 

557 # ) 

558 assert group < len(self.arabicGroup) 

559 if number_to_process % 100 != 1: 

560 if 3 <= number_to_process <= 10: 

561 ret_val = f"{self.arabicPluralGroups[group]} {ret_val}" 

562 else: 

563 if ret_val: 

564 ret_val = f"{self.arabicAppendedGroup[group]} {ret_val}" 

565 else: 

566 ret_val = f"{self.arabicGroup[group]} {ret_val}" 

567 

568 else: 

569 ret_val = f"{self.arabicGroup[group]} {ret_val}" 

570 

571 ret_val = f"{group_description} {ret_val}" 

572 group += 1 

573 formatted_number = "" 

574 if self.arabicPrefixText: 

575 formatted_number += f"{self.arabicPrefixText} " 

576 formatted_number += ret_val 

577 if self.integer_value != 0: 

578 remaining100 = int(self.integer_value % 100) 

579 

580 if remaining100 == 0 or remaining100 == 1: 

581 formatted_number += self.currency_unit[0] 

582 elif remaining100 == 2: 

583 if self.integer_value == 2: 

584 formatted_number += self.currency_unit[1] 

585 else: 

586 formatted_number += self.currency_unit[0] 

587 elif 3 <= remaining100 <= 10: 

588 formatted_number += self.currency_unit[2] 

589 elif 11 <= remaining100 <= 99: 

590 formatted_number += self.currency_unit[3] 

591 if self._decimalValue != 0: 

592 formatted_number += f" {self.separator} " 

593 formatted_number += decimal_string 

594 

595 if self._decimalValue != 0: 

596 formatted_number += " " 

597 remaining100 = int(self._decimalValue % 100) 

598 

599 if remaining100 == 0 or remaining100 == 1: 

600 formatted_number += self.currency_subunit[0] 

601 elif remaining100 == 2: 

602 formatted_number += self.currency_subunit[1] 

603 elif 3 <= remaining100 <= 10: 

604 formatted_number += self.currency_subunit[2] 

605 elif 11 <= remaining100 <= 99: 

606 formatted_number += self.currency_subunit[3] 

607 

608 if self.arabicSuffixText: 

609 formatted_number += f" {self.arabicSuffixText}" 

610 

611 return formatted_number 

612 

613 def validate_number(self, number): 

614 if number >= self.MAXVAL: 

615 raise OverflowError(self.errmsg_toobig % (number, self.MAXVAL)) 

616 return number 

617 

618 def set_currency_prefer(self, currency): 

619 if currency == 'EGP': 

620 self.currency_unit = CURRENCY_EGP[0] 

621 self.currency_subunit = CURRENCY_EGP[1] 

622 elif currency == 'KWD': 

623 self.currency_unit = CURRENCY_KWD[0] 

624 self.currency_subunit = CURRENCY_KWD[1] 

625 else: 

626 self.currency_unit = CURRENCY_SR[0] 

627 self.currency_subunit = CURRENCY_SR[1] 

628 

629 def to_currency(self, value, currency='SR', prefix='', suffix=''): 

630 self.set_currency_prefer(currency) 

631 self.isCurrencyNameFeminine = False 

632 self.separator = "و" 

633 self.arabicOnes = ARABIC_ONES 

634 self.arabicPrefixText = prefix 

635 self.arabicSuffixText = suffix 

636 return self.convert(value=value) 

637 

638 def to_ordinal(self, number, prefix=''): 

639 if number <= 19: 

640 return f"{self.arabicOrdinal[number]}" 

641 if number < 100: 

642 self.isCurrencyNameFeminine = True 

643 else: 

644 self.isCurrencyNameFeminine = False 

645 self.currency_subunit = ('', '', '', '') 

646 self.currency_unit = ('', '', '', '') 

647 self.arabicPrefixText = prefix 

648 self.arabicSuffixText = "" 

649 return f"{self.convert(self.absolute(number)).strip()}" 

650 

651 def to_year(self, value): 

652 value = self.validate_number(value) 

653 return self.to_cardinal(value) 

654 

655 def to_ordinal_num(self, value): 

656 return self.to_ordinal(value).strip() 

657 

658 def to_cardinal(self, number): 

659 self.isCurrencyNameFeminine = False 

660 number = self.validate_number(number) 

661 minus = '' 

662 if number < 0: 

663 minus = 'سالب ' 

664 self.separator = ',' 

665 self.currency_subunit = ('', '', '', '') 

666 self.currency_unit = ('', '', '', '') 

667 self.arabicPrefixText = "" 

668 self.arabicSuffixText = "" 

669 self.arabicOnes = ARABIC_ONES 

670 return minus + self.convert(value=self.absolute(number)).strip() 

671 

672 

673def parse_currency_parts(value, is_int_with_cents=True): 

674 if isinstance(value, int): 

675 if is_int_with_cents: 

676 # assume cents if value is integer 

677 negative = value < 0 

678 value = abs(value) 

679 integer, cents = divmod(value, 100) 

680 else: 

681 negative = value < 0 

682 integer, cents = abs(value), 0 

683 

684 else: 

685 value = Decimal(value) 

686 value = value.quantize( 

687 Decimal('.01'), 

688 rounding=ROUND_HALF_UP 

689 ) 

690 negative = value < 0 

691 value = abs(value) 

692 integer, fraction = divmod(value, 1) 

693 integer = int(integer) 

694 cents = int(fraction * 100) 

695 

696 return integer, cents, negative 

697 

698 

699def prefix_currency(prefix, base): 

700 return tuple("%s %s" % (prefix, i) for i in base) 

701 

702 

703try: 

704 strtype = basestring 

705except NameError: 

706 strtype = str 

707 

708 

709def to_s(val): 

710 try: 

711 return unicode(val) 

712 except NameError: 

713 return str(val) 

714 

715 

716# Derived from num2cyrillic licensed under LGPL-3.0-only 

717# Copyright 2018 ClaimCompass, Inc (num2cyrillic authored by Velizar Shulev) https://github.com/ClaimCompass/num2cyrillic 

718# Copyright 1997 The PHP Group (PEAR::Numbers_Words, authored by Kouber Saparev) https://github.com/pear/Numbers_Words/blob/master/Numbers/Words/Locale/bg.php 

719 

720 

721class NumberToWords_BG(Num2Word_Base): 

722 locale = 'bg' 

723 lang = 'Bulgarian' 

724 lang_native = 'Български' 

725 _misc_strings = { 

726 'deset': 'десет', 

727 'edinadeset': 'единадесет', 

728 'na': 'на', 

729 'sto': 'сто', 

730 'sta': 'ста', 

731 'stotin': 'стотин', 

732 'hiliadi': 'хиляди', 

733 } 

734 _digits = { 

735 0: [None, 'едно', 'две', 'три', 'четири', 'пет', 'шест', 'седем', 'осем', 'девет'], 

736 } 

737 _digits[1] = [None, 'един', 'два'] + _digits[0][3:] 

738 _digits[-1] = [None, 'една'] + _digits[0][2:] 

739 _last_and = False 

740 _zero = 'нула' 

741 _infinity = 'безкрайност' 

742 _and = 'и' 

743 _sep = ' ' 

744 _minus = 'минус' 

745 _plural = 'а' 

746 _exponent = { 

747 0: '', 

748 3: 'хиляда', 

749 6: 'милион', 

750 9: 'милиард', 

751 12: 'трилион', 

752 15: 'квадрилион', 

753 18: 'квинтилион', 

754 21: 'секстилион', 

755 24: 'септилион', 

756 27: 'октилион', 

757 30: 'ноналион', 

758 33: 'декалион', 

759 36: 'ундекалион', 

760 39: 'дуодекалион', 

761 42: 'тредекалион', 

762 45: 'кватордекалион', 

763 48: 'квинтдекалион', 

764 51: 'сексдекалион', 

765 54: 'септдекалион', 

766 57: 'октодекалион', 

767 60: 'новемдекалион', 

768 63: 'вигинтилион', 

769 66: 'унвигинтилион', 

770 69: 'дуовигинтилион', 

771 72: 'тревигинтилион', 

772 75: 'кваторвигинтилион', 

773 78: 'квинвигинтилион', 

774 81: 'сексвигинтилион', 

775 84: 'септенвигинтилион', 

776 87: 'октовигинтилион', 

777 90: 'новемвигинтилион', 

778 93: 'тригинтилион', 

779 96: 'унтригинтилион', 

780 99: 'дуотригинтилион', 

781 102: 'третригинтилион', 

782 105: 'кватортригинтилион', 

783 108: 'квинтригинтилион', 

784 111: 'секстригинтилион', 

785 114: 'септентригинтилион', 

786 117: 'октотригинтилион', 

787 120: 'новемтригинтилион', 

788 123: 'квадрагинтилион', 

789 126: 'унквадрагинтилион', 

790 129: 'дуоквадрагинтилион', 

791 132: 'треквадрагинтилион', 

792 135: 'кваторквадрагинтилион', 

793 138: 'квинквадрагинтилион', 

794 141: 'сексквадрагинтилион', 

795 144: 'септенквадрагинтилион', 

796 147: 'октоквадрагинтилион', 

797 150: 'новемквадрагинтилион', 

798 153: 'квинквагинтилион', 

799 156: 'унквинкагинтилион', 

800 159: 'дуоквинкагинтилион', 

801 162: 'треквинкагинтилион', 

802 165: 'кваторквинкагинтилион', 

803 168: 'квинквинкагинтилион', 

804 171: 'сексквинкагинтилион', 

805 174: 'септенквинкагинтилион', 

806 177: 'октоквинкагинтилион', 

807 180: 'новемквинкагинтилион', 

808 183: 'сексагинтилион', 

809 186: 'унсексагинтилион', 

810 189: 'дуосексагинтилион', 

811 192: 'тресексагинтилион', 

812 195: 'кваторсексагинтилион', 

813 198: 'квинсексагинтилион', 

814 201: 'секссексагинтилион', 

815 204: 'септенсексагинтилион', 

816 207: 'октосексагинтилион', 

817 210: 'новемсексагинтилион', 

818 213: 'септагинтилион', 

819 216: 'унсептагинтилион', 

820 219: 'дуосептагинтилион', 

821 222: 'тресептагинтилион', 

822 225: 'кваторсептагинтилион', 

823 228: 'квинсептагинтилион', 

824 231: 'секссептагинтилион', 

825 234: 'септенсептагинтилион', 

826 237: 'октосептагинтилион', 

827 240: 'новемсептагинтилион', 

828 243: 'октогинтилион', 

829 246: 'уноктогинтилион', 

830 249: 'дуооктогинтилион', 

831 252: 'треоктогинтилион', 

832 255: 'кватороктогинтилион', 

833 258: 'квиноктогинтилион', 

834 261: 'сексоктогинтилион', 

835 264: 'септоктогинтилион', 

836 267: 'октооктогинтилион', 

837 270: 'новемоктогинтилион', 

838 273: 'нонагинтилион', 

839 276: 'уннонагинтилион', 

840 279: 'дуононагинтилион', 

841 282: 'тренонагинтилион', 

842 285: 'кваторнонагинтилион', 

843 288: 'квиннонагинтилион', 

844 291: 'секснонагинтилион', 

845 294: 'септеннонагинтилион', 

846 297: 'октононагинтилион', 

847 300: 'новемнонагинтилион', 

848 303: 'центилион', 

849 } 

850 

851 def to_cardinal(self, value): 

852 return '' if value is None else self._to_words(value).strip() 

853 

854 def to_ordinal(self, _): 

855 raise NotImplementedError 

856 

857 def to_ordinal_num(self, _): 

858 raise NotImplementedError 

859 

860 def to_year(self, _): 

861 raise NotImplementedError 

862 

863 def to_currency(self, _): 

864 raise NotImplementedError 

865 

866 def _split_number(self, num): 

867 if isinstance(num, int): 

868 num = str(num) 

869 first = [] 

870 if len(num) % 3 != 0: 

871 if len(num[1:]) % 3 == 0: 

872 first = [num[0:1]] 

873 num = num[1:] 

874 elif len(num[2:]) % 3 == 0: 

875 first = [num[0:2]] 

876 num = num[2:] 

877 ret = [num[i:i + 3] for i in range(0, len(num), 3)] 

878 return first + ret 

879 

880 def _discard_empties(self, ls): 

881 return list(filter(lambda x: x is not None, ls)) 

882 

883 def _show_digits_group(self, num, gender=0, last=False): 

884 num = int(num) 

885 e = int(num % 10) # ones 

886 d = int((num - e) % 100 / 10) # tens 

887 s = int((num - d * 10 - e) % 1000 / 100) # hundreds 

888 ret = [None] * 6 

889 

890 if s: 

891 if s == 1: 

892 ret[1] = self._misc_strings['sto'] 

893 elif s == 2 or s == 3: 

894 ret[1] = self._digits[0][s] + self._misc_strings['sta'] 

895 else: 

896 ret[1] = self._digits[0][s] + self._misc_strings['stotin'] 

897 

898 if d: 

899 if d == 1: 

900 if not e: 

901 ret[3] = self._misc_strings['deset'] 

902 else: 

903 if e == 1: 

904 ret[3] = self._misc_strings['edinadeset'] 

905 else: 

906 ret[3] = self._digits[1][e] + self._misc_strings['na'] + self._misc_strings['deset'] 

907 e = 0 

908 else: 

909 ret[3] = self._digits[1][d] + self._misc_strings['deset'] 

910 

911 if e: 

912 ret[5] = self._digits[gender][e] 

913 

914 if len(self._discard_empties(ret)) > 1: 

915 if e: 

916 ret[4] = self._and 

917 else: 

918 ret[2] = self._and 

919 

920 if last: 

921 if not s or len(self._discard_empties(ret)) == 1: 

922 ret[0] = self._and 

923 self._last_and = True 

924 

925 return self._sep.join(self._discard_empties(ret)) 

926 

927 def _to_words(self, num=0): 

928 num_groups = self._split_number(num) 

929 sizeof_num_groups = len(num_groups) 

930 

931 ret = [None] * (sizeof_num_groups + 1) 

932 ret_minus = '' 

933 

934 if num < 0: 

935 ret_minus = self._minus + self._sep 

936 elif num == 0: 

937 return self._zero 

938 

939 i = sizeof_num_groups - 1 

940 j = 1 

941 while i >= 0: 

942 if ret[j] is None: 

943 ret[j] = '' 

944 

945 _pow = sizeof_num_groups - i 

946 

947 if num_groups[i] != '000': 

948 if int(num_groups[i]) > 1: 

949 if _pow == 1: 

950 ret[j] += self._show_digits_group(num_groups[i], 0, not self._last_and and i) + self._sep 

951 ret[j] += self._exponent[(_pow - 1) * 3] 

952 elif _pow == 2: 

953 ret[j] += self._show_digits_group(num_groups[i], -1, not self._last_and and i) + self._sep 

954 ret[j] += self._misc_strings['hiliadi'] + self._sep 

955 else: 

956 ret[j] += self._show_digits_group(num_groups[i], 1, not self._last_and and i) + self._sep 

957 ret[j] += self._exponent[(_pow - 1) * 3] + self._plural + self._sep 

958 else: 

959 if _pow == 1: 

960 ret[j] += self._show_digits_group(num_groups[i], 0, not self._last_and and i) + self._sep 

961 elif _pow == 2: 

962 ret[j] += self._exponent[(_pow - 1) * 3] + self._sep 

963 else: 

964 ret[j] += self._digits[1][1] + self._sep + self._exponent[(_pow - 1) * 3] + self._sep 

965 

966 i -= 1 

967 j += 1 

968 

969 ret = self._discard_empties(ret) 

970 ret.reverse() 

971 return ret_minus + ''.join(ret) 

972 

973 

974def patch_module(): 

975 try: 

976 import num2words # noqa: PLC0415 

977 except ImportError: 

978 _logger = logging.getLogger(__name__) 

979 _logger.warning("num2words is not available, Arabic number to words conversion will not work") 

980 return 

981 if MIN_PY_VERSION >= (3, 12): 981 ↛ 982line 981 didn't jump to line 982 because the condition on line 981 was never true

982 raise RuntimeError("The num2words monkey patch is obsolete. Bump the version of the library to the latest available in the official package repository, if it hasn't already been done, and remove the patch.") 

983 num2words.CONVERTER_CLASSES["ar"] = Num2Word_AR_Fixed() 

984 num2words.CONVERTER_CLASSES["bg"] = NumberToWords_BG()