cadrumo.core.decimal.grammar module¶
Canonical strict decimal grammar for operator-supplied numeric text.
This module owns the accepted shape of a hand-typed decimal amount; it does
not own any refusal surface. try_parse_canonical_decimal() returns
None for text that does not conform, and each boundary wraps it with its own
typed, localised refusal — the CLI raises
BadParameter with the operator-facing catalogue message, the
calculate-input boundary raises its own typed application error. One grammar,
many error contracts.
Relationship to the sibling helpers¶
coercion is the tolerant half of this package: coerce_decimal()
swallows a parse failure and returns a configured default, and
normalize_decimal_separators() transforms European separators for callers
that must accept machine-produced text (bank exports, AEAT PDF extraction).
Those exist to admit messy real-world input. This module is the opposite
posture: it exists to refuse anything outside one canonical form, because the
text it validates was typed by a human whose intent cannot be guessed.
Accepted grammar¶
An optional leading - (only when signed), one or more digits, and an
optional dot-separated fractional part. There is no thousands grouping, no
comma decimal separator, no scientific notation, no leading +, no embedded
whitespace, and no NaN/Infinity. The regex runs before
Decimal construction so a caller’s refusal message describes
the grammar rather than a constructor failure, and
is_finite() is asserted afterwards as defence in depth.
Fractional-digit cap¶
max_fraction_digits is the one axis on which conforming callers legitimately
differ, so it is a parameter rather than a second grammar:
Pass
2for a hand-typed euro amount. The cap is what makes the Spanish thousands shape1.000refuse instead of silently becomingDecimal("1.0")— a one-euro transaction where the operator meant one thousand.Pass
Nonefor a calculation input channel. Sub-cent precision is legitimate there: the AEAT fixed-width encoder rounds a value such asDecimal("2.345")to cents withROUND_HALF_UPper the AEAT Instrucciones, so refusing it at the input boundary would reject a value the export layer is built to accept.
- try_parse_canonical_decimal(text, *, signed=True, max_fraction_digits=None)[source]¶
Parse text as a canonical-grammar decimal, or
Nonewhen it does not conform.Never raises: a non-conforming input returns
Noneso the calling boundary can raise its own typed, localised refusal. Surrounding whitespace is stripped before validation; whitespace inside the number never conforms.- Parameters:
text (
str) – The raw operator-supplied string.signed (
bool) – WhenTrue(the default) a leading-conforms; whenFalsea negative input does not conform.max_fraction_digits (
int|None) – Maximum fractional digits accepted.None(the default) accepts any number of fractional digits; pass2for a hand-typed euro amount so the Spanish thousands shape1.000refuses.
- Return type:
- Returns:
The parsed decimal, or
Nonewhen text does not conform.
Examples
>>> try_parse_canonical_decimal("1234.56") Decimal('1234.56') >>> try_parse_canonical_decimal("-1000") Decimal('-1000') >>> try_parse_canonical_decimal("2.345") Decimal('2.345') >>> try_parse_canonical_decimal("1.000", max_fraction_digits=2) is None True >>> try_parse_canonical_decimal("1e3") is None True >>> try_parse_canonical_decimal("+100") is None True >>> try_parse_canonical_decimal("1.234,56") is None True >>> try_parse_canonical_decimal("NaN") is None True >>> try_parse_canonical_decimal("-1000", signed=False) is None True
- european_thousands_reading_is_ambiguous(text)[source]¶
Return whether a dot in text could equally mean thousands or decimals.
Spanish writes one thousand two hundred and thirty-four as
1.234and English writes one point two three four the same way. When that is the whole token there is no evidence in it to choose by, and a parser that picks anyway is not parsing — it is guessing, at a thousandfold error, on a number headed for a tax return.The ambiguity is narrow and this predicate keeps it that way, returning
Falsefor every token that carries its own evidence:A comma anywhere settles it. Spanish uses the comma as the decimal mark, so
1.234,56and1234,56are unambiguous and stay readable.One or two digits after the dot cannot be a thousands group, which is always exactly three —
1234.56and0.5are plainly decimals.Four or more cannot be one either:
1.2345is a decimal, since a grouped number would have broken it as12.345.A lead group that could not open a thousands run settles it too. A leading zero (
0.333, an ordinary coefficient) or a lead longer than three digits (1000.000) means the token was never grouped.
So the caller refuses
1.234,10.500and100.000and accepts everything else it accepted before. Coefficients and percentages written0.333keep working, which matters where this is used on values whose kind is not yet known.
- is_non_negative_canonical_decimal(text)[source]¶
Whether text is a canonical decimal carrying no sign.
The question a wire payload asks about a stringified money column. A record that bounds a Decimal field at
ge=0loses that bound the moment it is projected to a string, and the projection has to re-assert it or the published schema promises less than the record guarantees.It is a predicate rather than a validator because the callers differ on what an EMPTY value means, and that difference is real: an export column spells an absent optional as
""and must accept it, while a balance is always present and an empty one is malformed. Each raises its own refusal for the same reason – what must not differ is the grammar they test against.Written out twice before this existed, once for the ledger export columns and once for the IVA wallet balances, with the second docstring noting it was the “same shape” as the first.
- class DecimalSeparator(*values)[source]¶
Bases:
StrEnumWhich character a file uses to separate a decimal fraction.
Spanish and German banks write
1.234,56where the same amount is1,234.56elsewhere, so every tabular reader in the codebase has to record which convention a file follows. Four modules stated that pair independently before this existed – the CSV bank layouts, the bulk invoice importer, the tabular dialect detector, and the provider amount parser.- COMMA¶
Comma-decimal, the Spanish and wider continental European convention.
- PERIOD¶
Dot-decimal, the convention most exported bank CSVs use.
- DecimalSeparatorValue¶
The same vocabulary for a strict model field or payload surface.
A bare enum under strict validation refuses the plain token a serialised layout or imported row carries, so those fields take this literal over the members above rather than restating the pair.