311 lines
9.8 KiB
Python
311 lines
9.8 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Copyright (c) 2003, Taro Ogawa. All Rights Reserved.
|
||
|
# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved.
|
||
|
|
||
|
# This library is free software; you can redistribute it and/or
|
||
|
# modify it under the terms of the GNU Lesser General Public
|
||
|
# License as published by the Free Software Foundation; either
|
||
|
# version 2.1 of the License, or (at your option) any later version.
|
||
|
# This library is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
# Lesser General Public License for more details.
|
||
|
# You should have received a copy of the GNU Lesser General Public
|
||
|
# License along with this library; if not, write to the Free Software
|
||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
||
|
# MA 02110-1301 USA
|
||
|
|
||
|
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)
|
||
|
|
||
|
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: # 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.strip(), 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 = "%s " % self.negword.strip()
|
||
|
|
||
|
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.strip() 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
|