Batu Lab NotesPractical developer guides

Parse a fixed-point amount while rejecting surrounding currency text

By Batu · English technical notes

Also published in our primary archive.

Python can parse a fixed-point amount while rejecting surrounding currency text by keeping the currency code in its own field and applying Pattern.fullmatch() to the amount field before calling Decimal. This makes the boundary unambiguous: USD is permitted metadata, while amount must contain only an unsigned token with two fractional digits.

The fixture retains three original records. The loose transformation demonstrates why Decimal is insufficient as the field validator: it accepts both "12.50" and " 12.50 ", although the latter contains characters outside the amount token. It rejects "$12.50", but that does not repair the over-acceptance of whitespace. Importantly, the correction does not strip text or extract a numeric substring. Either action would silently normalize malformed input instead of reporting it through rejection.

The corrected list preserves the separate currency value and converts only the record whose complete amount matches [0-9]+\.[0-9]{2}. The assertions verify the original records were not mutated, document the loose result for this fixture, and check the corrected result contains Decimal('12.50') only. The pattern intentionally rejects signs, integers, grouping commas, scientific notation, other scales, and embedded currency symbols; expand it only if the field schema permits those forms.

Pattern.fullmatch() was added in Python 3.4. Python’s regular-expression documentation specifies that it requires the whole string to match, while the Decimal documentation describes decimal construction.

AI assistance disclosure: This article was prepared with AI assistance and checked with the shown synthetic example.

import re
from decimal import Decimal, InvalidOperation

records = [
    {"currency": "USD", "amount": "12.50"},
    {"currency": "USD", "amount": "$12.50"},
    {"currency": "USD", "amount": " 12.50 "},
]

loose = []
for record in records:
    try:
        loose.append((record["currency"], Decimal(record["amount"])))
    except InvalidOperation:
        loose.append((record["currency"], None))

amount_token = re.compile(r"[0-9]+\.[0-9]{2}")
accepted = [
    {"currency": record["currency"], "amount": Decimal(record["amount"])}
    for record in records
    if amount_token.fullmatch(record["amount"])
]

assert records == [
    {"currency": "USD", "amount": "12.50"},
    {"currency": "USD", "amount": "$12.50"},
    {"currency": "USD", "amount": " 12.50 "},
]
assert loose == [("USD", Decimal("12.50")), ("USD", None), ("USD", Decimal("12.50"))]
assert accepted == [{"currency": "USD", "amount": Decimal("12.50")}]

print("input:", records)
print("loose:", loose)
print("accepted:", accepted)
input: [{'currency': 'USD', 'amount': '12.50'}, {'currency': 'USD', 'amount': '$12.50'}, {'currency': 'USD', 'amount': ' 12.50 '}]
loose: [('USD', Decimal('12.50')), ('USD', None), ('USD', Decimal('12.50'))]
accepted: [{'currency': 'USD', 'amount': Decimal('12.50')}]