Normalize mixed numeric inputs with singledispatch
Normalize mixed numeric inputs with singledispatch
functools.singledispatch chooses an implementation from the runtime type of its first argument. That makes it useful when a boundary accepts several numeric representations but must produce one representation. Here, normalize_amount returns a Decimal rounded to cents for Decimal, int, and finite float inputs.
The Decimal branch retains a decimal input directly. The float branch first applies str(value), then constructs Decimal from that display form; constructing Decimal directly from a float would capture the float’s binary approximation instead. Each supported route calls quantize(CENTS), where CENTS is Decimal("0.01"). The values Decimal("7.004"), 7, and 7.006 therefore normalize to 7.00, 7.00, and 7.01.
The equality assertion checks the resulting numeric values. The following exponent assertion separately verifies that every result is represented at two decimal places, avoiding the false assumption that Decimal equality alone preserves trailing-zero scale. A dedicated bool registration rejects booleans even though bool is a subclass of int. Non-finite floats and Decimals are rejected, while types that do not match a registered type or registered abstract base class reach the default handler. A subclass of a registered type can use that type’s implementation through method-resolution-order lookup. The default context’s rounding mode governs half-cent cases.
AI assistance disclosure: This article was prepared with AI assistance.
Example
from decimal import Decimal
from functools import singledispatch
from math import isfinite
CENTS = Decimal("0.01")
@singledispatch
def normalize_amount(value):
raise TypeError(f"unsupported amount type: {type(value).__name__}")
@normalize_amount.register
def _(value: Decimal):
if not value.is_finite():
raise ValueError("amount must be finite")
return value.quantize(CENTS)
@normalize_amount.register
def _(value: int):
return Decimal(value).quantize(CENTS)
@normalize_amount.register
def _(value: float):
if not isfinite(value):
raise ValueError("amount must be finite")
return Decimal(str(value)).quantize(CENTS)
@normalize_amount.register
def _(value: bool):
raise TypeError("boolean values are not amounts")
amounts = [normalize_amount(Decimal("7.004")), normalize_amount(7), normalize_amount(7.006)]
assert amounts == [Decimal("7.00"), Decimal("7.00"), Decimal("7.01")]
assert all(amount.as_tuple().exponent == -2 for amount in amounts)
print(*amounts)
Expected output
7.00 7.00 7.01
Comments
Post a Comment