| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- # -*- coding: utf-8 -*-
- from __future__ import unicode_literals
- import math
- from collections import OrderedDict
- from decimal import Decimal
- from .compat import to_s
- from .currency import parse_currency_parts, prefix_currency
- class Num2Word_Base(object):
- CURRENCY_FORMS = {}
- CURRENCY_ADJECTIVES = {}
- def __init__(self):
- self.is_title = False
- self.precision = 2
- self.exclude_title = []
- self.negword = "(-) "
- self.pointword = "(.)"
- self.errmsg_nonnum = "type(%s) not in [long, int, float]"
- self.errmsg_floatord = "Cannot treat float %s as ordinal."
- self.errmsg_negord = "Cannot treat negative num %s as ordinal."
- self.errmsg_toobig = "abs(%s) must be less than %s."
- self.setup()
- # uses cards
- if any(hasattr(self, field) for field in
- ['high_numwords', 'mid_numwords', 'low_numwords']):
- self.cards = OrderedDict()
- self.set_numwords()
- self.MAXVAL = 1000 * list(self.cards.keys())[0]
- def set_numwords(self):
- self.set_high_numwords(self.high_numwords)
- self.set_mid_numwords(self.mid_numwords)
- self.set_low_numwords(self.low_numwords)
- # self.set_labeled_numbers(self.labeled_numbers)
- def set_high_numwords(self, *args):
- raise NotImplementedError
- # def set_labeled_numbers(self, labeled_numbers):
- # for key, val in labeled_numbers.items():
- # self.cards[key] = val
- def set_mid_numwords(self, mid):
- for key, val in mid:
- self.cards[key] = val
- def set_low_numwords(self, numwords):
- for word, n in zip(numwords, range(len(numwords) - 1, -1, -1)):
- self.cards[n] = word
- def splitnum(self, value):
- for elem in self.cards:
- if elem > value:
- continue
- out = []
- if value == 0:
- div, mod = 1, 0
- else:
- div, mod = divmod(value, elem)
- if div == 1:
- out.append((self.cards[1], 1))
- else:
- if div == value: # The system tallies, eg Roman Numerals
- return [(div * self.cards[elem], div*elem)]
- out.append(self.splitnum(div))
- out.append((self.cards[elem], elem))
- if mod:
- out.append(self.splitnum(mod))
- return out
- def parse_minus(self, num_str):
- """Detach minus and return it as symbol with new num_str."""
- if num_str.startswith('-'):
- # Extra spacing to compensate if there is no minus.
- return '%s ' % self.negword, num_str[1:]
- return '', num_str
- def str_to_number(self, value):
- return Decimal(value)
- def to_cardinal(self, value):
- try:
- assert int(value) == value
- except (ValueError, TypeError, AssertionError):
- return self.to_cardinal_float(value)
- out = ""
- if value < 0:
- value = abs(value)
- out = self.negword
- if value >= self.MAXVAL:
- raise OverflowError(self.errmsg_toobig % (value, self.MAXVAL))
- val = self.splitnum(value)
- words, num = self.clean(val)
- return self.title(out + words)
- def float2tuple(self, value):
- pre = int(value)
- # Simple way of finding decimal places to update the precision
- self.precision = abs(Decimal(str(value)).as_tuple().exponent)
- post = abs(value - pre) * 10**self.precision
- if abs(round(post) - post) < 0.01:
- # We generally floor all values beyond our precision (rather than
- # rounding), but in cases where we have something like 1.239999999,
- # which is probably due to python's handling of floats, we actually
- # want to consider it as 1.24 instead of 1.23
- post = int(round(post))
- else:
- post = int(math.floor(post))
- return pre, post
- def to_cardinal_float(self, value):
- try:
- float(value) == value
- except (ValueError, TypeError, AssertionError, AttributeError):
- raise TypeError(self.errmsg_nonnum % value)
- pre, post = self.float2tuple(float(value))
- post = str(post)
- post = '0' * (self.precision - len(post)) + post
- out = [self.to_cardinal(pre)]
- if self.precision:
- out.append(self.title(self.pointword))
- for i in range(self.precision):
- curr = int(post[i])
- out.append(to_s(self.to_cardinal(curr)))
- return " ".join(out)
- def merge(self, curr, next):
- raise NotImplementedError
- def clean(self, val):
- out = val
- while len(val) != 1:
- out = []
- left, right = val[:2]
- if isinstance(left, tuple) and isinstance(right, tuple):
- out.append(self.merge(left, right))
- if val[2:]:
- out.append(val[2:])
- else:
- for elem in val:
- if isinstance(elem, list):
- if len(elem) == 1:
- out.append(elem[0])
- else:
- out.append(self.clean(elem))
- else:
- out.append(elem)
- val = out
- return out[0]
- def title(self, value):
- if self.is_title:
- out = []
- value = value.split()
- for word in value:
- if word in self.exclude_title:
- out.append(word)
- else:
- out.append(word[0].upper() + word[1:])
- value = " ".join(out)
- return value
- def verify_ordinal(self, value):
- if not value == int(value):
- raise TypeError(self.errmsg_floatord % value)
- if not abs(value) == value:
- raise TypeError(self.errmsg_negord % value)
- def to_ordinal(self, value):
- return self.to_cardinal(value)
- def to_ordinal_num(self, value):
- return value
- # Trivial version
- def inflect(self, value, text):
- text = text.split("/")
- if value == 1:
- return text[0]
- return "".join(text)
- # //CHECK: generalise? Any others like pounds/shillings/pence?
- def to_splitnum(self, val, hightxt="", lowtxt="", jointxt="",
- divisor=100, longval=True, cents=True):
- out = []
- if isinstance(val, float):
- high, low = self.float2tuple(val)
- else:
- try:
- high, low = val
- except TypeError:
- high, low = divmod(val, divisor)
- if high:
- hightxt = self.title(self.inflect(high, hightxt))
- out.append(self.to_cardinal(high))
- if low:
- if longval:
- if hightxt:
- out.append(hightxt)
- if jointxt:
- out.append(self.title(jointxt))
- elif hightxt:
- out.append(hightxt)
- if low:
- if cents:
- out.append(self.to_cardinal(low))
- else:
- out.append("%02d" % low)
- if lowtxt and longval:
- out.append(self.title(self.inflect(low, lowtxt)))
- return " ".join(out)
- def to_year(self, value, **kwargs):
- return self.to_cardinal(value)
- def pluralize(self, n, forms):
- """
- Should resolve gettext form:
- http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html
- """
- raise NotImplementedError
- def _money_verbose(self, number, currency):
- return self.to_cardinal(number)
- def _cents_verbose(self, number, currency):
- return self.to_cardinal(number)
- def _cents_terse(self, number, currency):
- return "%02d" % number
- def to_currency(self, val, currency='EUR', cents=True, separator=',',
- adjective=False):
- """
- Args:
- val: Numeric value
- currency (str): Currency code
- cents (bool): Verbose cents
- separator (str): Cent separator
- adjective (bool): Prefix currency name with adjective
- Returns:
- str: Formatted string
- """
- left, right, is_negative = parse_currency_parts(val)
- try:
- cr1, cr2 = self.CURRENCY_FORMS[currency]
- except KeyError:
- raise NotImplementedError(
- 'Currency code "%s" not implemented for "%s"' %
- (currency, self.__class__.__name__))
- if adjective and currency in self.CURRENCY_ADJECTIVES:
- cr1 = prefix_currency(self.CURRENCY_ADJECTIVES[currency], cr1)
- minus_str = "%s " % self.negword if is_negative else ""
- money_str = self._money_verbose(left, currency)
- cents_str = self._cents_verbose(right, currency) \
- if cents else self._cents_terse(right, currency)
- return u'%s%s %s%s %s %s' % (
- minus_str,
- money_str,
- self.pluralize(left, cr1),
- separator,
- cents_str,
- self.pluralize(right, cr2)
- )
- def setup(self):
- pass
|