strparse: Improve extra text parsing in currency_decimal.

This allows a symbol and a currency code to be in different parts of the
string, as long as there's at most one of each.
This commit is contained in:
Brett Smith 2017-12-19 12:43:59 -05:00
parent 9d638f4d01
commit 87f3209101

View file

@ -6,8 +6,14 @@ import unicodedata
import babel.numbers import babel.numbers
CURRENCY_SPEC_PATTERN = r'^{space}(?:|{symbol}{space}{code}|{code}{space}{symbol}){space}$'.format(
code=r'[A-Za-z]{,3}',
space=r'\s*',
symbol=r'(\W?)',
)
@functools.lru_cache() @functools.lru_cache()
def _currency_pattern(locale): def _currency_amount_pattern(locale):
minus = babel.numbers.get_minus_sign_symbol(locale) minus = babel.numbers.get_minus_sign_symbol(locale)
plus = babel.numbers.get_plus_sign_symbol(locale) plus = babel.numbers.get_plus_sign_symbol(locale)
dec_sym = babel.numbers.get_decimal_symbol(locale) dec_sym = babel.numbers.get_decimal_symbol(locale)
@ -21,27 +27,22 @@ def _currency_pattern(locale):
def currency_decimal(s, locale='en_US_POSIX'): def currency_decimal(s, locale='en_US_POSIX'):
try: try:
match = re.search(_currency_pattern(locale), s) match = re.search(_currency_amount_pattern(locale), s)
except TypeError: except TypeError:
return decimal.Decimal(s) return decimal.Decimal(s)
if not match: if not match:
raise ValueError("no decimal found in {!r}".format(s)) raise ValueError("no decimal found in {!r}".format(s))
# There may be extra symbols/text before the number, after the number, extra_s = ''.join([s[:match.start()], match.group(2), s[match.end():]])
# or between the number and its sign—but only in one of those places. # The only extra text allowed is currency specifiers:
extra = None # '€', 'A$', 'US$', 'CAD', '$USD', etc.
for extra_s in [s[:match.start()], match.group(2), s[match.end():]]: extra_match = re.match(CURRENCY_SPEC_PATTERN, extra_s)
extra_s = extra_s.strip() if not extra_match:
if extra and extra_s: extra_ok = False
raise ValueError("too much extraneous text in {!r}".format(s)) else:
extra = extra_s symbol = extra_match.group(1) or extra_match.group(2)
# The only extra text allowed is currency specifiers like plain symbols, extra_ok = (not symbol) or (unicodedata.category(symbol) == 'Sc')
# 'A$', 'US$', 'CAD', 'USD $', etc. if not extra_ok:
# Trim any currency symbol. raise ValueError("non-currency text in {!r}: {!r}".format(s, extra_s))
if extra and unicodedata.category(extra[-1]) == 'Sc':
extra = extra[:-1].strip()
# Anything remaining should look like currency specifier text.
if extra and ((len(extra) > 3) or (not extra.isalpha())):
raise ValueError("non-currency text in {!r}: {!r}".format(s, extra))
return babel.numbers.parse_decimal(match.group(1) + match.group(3), locale) return babel.numbers.parse_decimal(match.group(1) + match.group(3), locale)
def date(date_s, date_fmt): def date(date_s, date_fmt):