|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
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) |
|
|
|
def set_high_numwords(self, *args): |
|
raise NotImplementedError |
|
|
|
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: |
|
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('-'): |
|
|
|
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) |
|
|
|
|
|
self.precision = abs(Decimal(str(value)).as_tuple().exponent) |
|
|
|
post = abs(value - pre) * 10**self.precision |
|
if abs(round(post) - post) < 0.01: |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
def inflect(self, value, text): |
|
text = text.split("/") |
|
if value == 1: |
|
return text[0] |
|
return "".join(text) |
|
|
|
|
|
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 _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 "" |
|
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, |
|
self.to_cardinal(left), |
|
self.pluralize(left, cr1), |
|
separator, |
|
cents_str, |
|
self.pluralize(right, cr2) |
|
) |
|
|
|
def setup(self): |
|
pass |
|
|