1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
|
import warnings
from decimal import Decimal, InvalidOperation
from babel.core import Locale
from agate.data_types.base import DataType
from agate.exceptions import CastError
#: A list of currency symbols sourced from `Xe <https://www.xe.com/symbols/>`_.
DEFAULT_CURRENCY_SYMBOLS = ['؋', '$', 'ƒ', '៛', '¥', '₡', '₱', '£', '€', '¢', '﷼', '₪', '₩', '₭', '₮',
'₦', '฿', '₤', '₫']
POSITIVE = Decimal('1')
NEGATIVE = Decimal('-1')
class Number(DataType):
"""
Data representing numbers.
:param locale:
A locale specification such as :code:`en_US` or :code:`de_DE` to use
for parsing formatted numbers.
:param group_symbol:
A grouping symbol used in the numbers. Overrides the value provided by
the specified :code:`locale`.
:param decimal_symbol:
A decimal separate symbol used in the numbers. Overrides the value
provided by the specified :code:`locale`.
:param currency_symbols:
A sequence of currency symbols to strip from numbers.
:param no_leading_zeroes:
Whether to disallow leading zeroes.
"""
def __init__(self, locale='en_US', group_symbol=None, decimal_symbol=None,
currency_symbols=DEFAULT_CURRENCY_SYMBOLS, no_leading_zeroes=None, **kwargs):
super().__init__(**kwargs)
self.locale = Locale.parse(locale)
self.currency_symbols = currency_symbols
self.no_leading_zeroes = no_leading_zeroes
# Suppress Babel warning on Python 3.6
# See #665
with warnings.catch_warnings():
warnings.simplefilter("ignore")
# Babel 2.14 support.
# https://babel.pocoo.org/en/latest/changelog.html#possibly-backwards-incompatible-changes
number_symbols = self.locale.number_symbols.get('latn', self.locale.number_symbols)
self.group_symbol = group_symbol or number_symbols.get('group', ',')
self.decimal_symbol = decimal_symbol or number_symbols.get('decimal', '.')
def cast(self, d):
"""
Cast a single value to a :class:`decimal.Decimal`.
:returns:
:class:`decimal.Decimal` or :code:`None`.
"""
if isinstance(d, Decimal) or d is None:
return d
t = type(d)
if t is int:
return Decimal(d)
if t is float:
return Decimal(repr(d))
if d is False:
return Decimal(0)
if d is True:
return Decimal(1)
if not isinstance(d, str):
raise CastError('Can not parse value "%s" as Decimal.' % d)
d = d.strip()
if d.lower() in self.null_values:
return None
d = d.strip('%')
if len(d) > 0 and d[0] == '-':
d = d[1:]
sign = NEGATIVE
else:
sign = POSITIVE
for symbol in self.currency_symbols:
d = d.strip(symbol)
d = d.replace(self.group_symbol, '')
d = d.replace(self.decimal_symbol, '.')
if self.no_leading_zeroes and len(d) > 1 and d[0] == '0' and d[1] != '.':
raise CastError('Can not parse value "%s" as Decimal without leading zeroes' % d)
try:
return Decimal(d) * sign
# The Decimal class will return an InvalidOperation exception on most Python implementations,
# but PyPy3 may return a ValueError if the string is not translatable to ASCII
except (InvalidOperation, ValueError):
pass
raise CastError('Can not parse value "%s" as Decimal.' % d)
def csvify(self, d):
return d
def jsonify(self, d):
if d is None:
return d
return float(d)
|