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
« 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
9from odoo.release import MIN_PY_VERSION
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
19CURRENCY_SR = [("ريال", "ريالان", "ريالات", "ريالاً"),
20 ("هللة", "هللتان", "هللات", "هللة")]
21CURRENCY_EGP = [("جنيه", "جنيهان", "جنيهات", "جنيهاً"),
22 ("قرش", "قرشان", "قروش", "قرش")]
23CURRENCY_KWD = [("دينار", "ديناران", "دينارات", "ديناراً"),
24 ("فلس", "فلسان", "فلس", "فلس")]
26ARABIC_ONES = [
27 "", "واحد", "اثنان", "ثلاثة", "أربعة", "خمسة", "ستة", "سبعة", "ثمانية",
28 "تسعة",
29 "عشرة", "أحد عشر", "اثنا عشر", "ثلاثة عشر", "أربعة عشر", "خمسة عشر",
30 "ستة عشر", "سبعة عشر", "ثمانية عشر",
31 "تسعة عشر"
32]
35class Num2Word_Base:
36 CURRENCY_FORMS = {}
37 CURRENCY_ADJECTIVES = {}
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."
50 self.setup()
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()))
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)
64 def set_high_numwords(self, *args):
65 raise NotImplementedError
67 def set_mid_numwords(self, mid):
68 for key, val in mid:
69 self.cards[key] = val
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
75 def splitnum(self, value):
76 for elem in self.cards:
77 if elem > value:
78 continue
80 out = []
81 if value == 0:
82 div, mod = 1, 0
83 else:
84 div, mod = divmod(value, elem)
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))
93 out.append((self.cards[elem], elem))
95 if mod:
96 out.append(self.splitnum(mod))
98 return out
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
107 def str_to_number(self, value):
108 return Decimal(value)
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)
116 out = ""
117 if value < 0:
118 value = abs(value)
119 out = "%s " % self.negword.strip()
121 if value >= self.MAXVAL:
122 raise OverflowError(self.errmsg_toobig % (value, self.MAXVAL))
124 val = self.splitnum(value)
125 words, _ = self.clean(val)
126 return self.title(out + words)
128 def float2tuple(self, value):
129 pre = int(value)
131 # Simple way of finding decimal places to update the precision
132 self.precision = abs(Decimal(str(value)).as_tuple().exponent)
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))
144 return pre, post
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)
152 pre, post = self.float2tuple(float(value))
154 post = str(post)
155 post = '0' * (self.precision - len(post)) + post
157 out = [self.to_cardinal(pre)]
158 if self.precision:
159 out.append(self.title(self.pointword))
161 for i in range(self.precision):
162 curr = int(post[i])
163 out.append(to_s(self.to_cardinal(curr)))
165 return " ".join(out)
167 def merge(self, left, right):
168 raise NotImplementedError
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]
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
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)
209 def to_ordinal(self, value):
210 return self.to_cardinal(value)
212 def to_ordinal_num(self, value):
213 return value
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)
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 = []
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)
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)
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)))
255 return " ".join(out)
257 def to_year(self, value, **kwargs):
258 return self.to_cardinal(value)
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
267 def _money_verbose(self, number, currency):
268 return self.to_cardinal(number)
270 def _cents_verbose(self, number, currency):
271 return self.to_cardinal(number)
273 def _cents_terse(self, number, currency):
274 return "%02d" % number
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
288 """
289 left, right, is_negative = parse_currency_parts(val)
291 try:
292 cr1, cr2 = self.CURRENCY_FORMS[currency]
294 except KeyError:
295 raise NotImplementedError(
296 'Currency code "%s" not implemented for "%s"' %
297 (currency, self.__class__.__name__))
299 if adjective and currency in self.CURRENCY_ADJECTIVES:
300 cr1 = prefix_currency(self.CURRENCY_ADJECTIVES[currency], cr1)
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)
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 )
316 def setup(self):
317 pass
320class Num2Word_AR_Fixed(Num2Word_Base):
321 errmsg_toobig = "abs(%s) must be less than %s."
322 MAXVAL = 10**51
324 def __init__(self):
325 super().__init__()
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 = 'و'
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 ]
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)
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()
400 def extract_integer_and_decimal_parts(self):
401 splits = re.split('\\.', str(self.number))
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
409 def decimal_value(self, decimal_part):
410 if self.partPrecision is not len(decimal_part):
411 decimal_part_length = len(decimal_part)
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
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
426 # The following is useless (never happens)
427 # for i in range(len(result), self.partPrecision):
428 # result += '0'
429 return result
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)]
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 = ""
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 += " و "
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 += " و "
496 ret_val += self.arabicTens[int(tens)]
498 return ret_val
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
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}"
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()
520 def convert_to_arabic(self):
521 temp_number = Decimal(self.number)
523 if temp_number == Decimal(0):
524 return "صفر"
526 decimal_string = self.process_arabic_group(self._decimalValue,
527 -1,
528 Decimal(0))
529 ret_val = ""
530 group = 0
532 while temp_number > Decimal(0):
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)))
543 temp_number = int(temp_number_dec / Decimal(1000))
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}"
568 else:
569 ret_val = f"{self.arabicGroup[group]} {ret_val}"
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)
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
595 if self._decimalValue != 0:
596 formatted_number += " "
597 remaining100 = int(self._decimalValue % 100)
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]
608 if self.arabicSuffixText:
609 formatted_number += f" {self.arabicSuffixText}"
611 return formatted_number
613 def validate_number(self, number):
614 if number >= self.MAXVAL:
615 raise OverflowError(self.errmsg_toobig % (number, self.MAXVAL))
616 return number
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]
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)
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()}"
651 def to_year(self, value):
652 value = self.validate_number(value)
653 return self.to_cardinal(value)
655 def to_ordinal_num(self, value):
656 return self.to_ordinal(value).strip()
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()
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
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)
696 return integer, cents, negative
699def prefix_currency(prefix, base):
700 return tuple("%s %s" % (prefix, i) for i in base)
703try:
704 strtype = basestring
705except NameError:
706 strtype = str
709def to_s(val):
710 try:
711 return unicode(val)
712 except NameError:
713 return str(val)
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
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 }
851 def to_cardinal(self, value):
852 return '' if value is None else self._to_words(value).strip()
854 def to_ordinal(self, _):
855 raise NotImplementedError
857 def to_ordinal_num(self, _):
858 raise NotImplementedError
860 def to_year(self, _):
861 raise NotImplementedError
863 def to_currency(self, _):
864 raise NotImplementedError
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
880 def _discard_empties(self, ls):
881 return list(filter(lambda x: x is not None, ls))
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
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']
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']
911 if e:
912 ret[5] = self._digits[gender][e]
914 if len(self._discard_empties(ret)) > 1:
915 if e:
916 ret[4] = self._and
917 else:
918 ret[2] = self._and
920 if last:
921 if not s or len(self._discard_empties(ret)) == 1:
922 ret[0] = self._and
923 self._last_and = True
925 return self._sep.join(self._discard_empties(ret))
927 def _to_words(self, num=0):
928 num_groups = self._split_number(num)
929 sizeof_num_groups = len(num_groups)
931 ret = [None] * (sizeof_num_groups + 1)
932 ret_minus = ''
934 if num < 0:
935 ret_minus = self._minus + self._sep
936 elif num == 0:
937 return self._zero
939 i = sizeof_num_groups - 1
940 j = 1
941 while i >= 0:
942 if ret[j] is None:
943 ret[j] = ''
945 _pow = sizeof_num_groups - i
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
966 i -= 1
967 j += 1
969 ret = self._discard_empties(ret)
970 ret.reverse()
971 return ret_minus + ''.join(ret)
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()