FAILURE MAP
← Case archive

FA-62126 / Currency rounding and FX conversion / Open access

Trailing zero fraction digits beyond the exponent are rejected · case 01

'1 234,500' euros is refused although the extra digit is a zero.

Verified by executionVariant 1 · 8 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

Any fraction longer than exp is reported as ERR:precision.

VERIFIED REPAIR

Reject only non-zero digits beyond the exponent.

Unsuccessful approach: Silently truncating extra digits accepts amounts with non-zero sub-minor precision.

Case contract

solve(text, locale, exp): parse a localized amount into integer minor units at exponent exp. Group/decimal separators: en ',' '.', de '.' ',', ch "'" '.', fr ' ' or U+202F and ','. An optional leading '-' marks a negative amount (including -0.xx). If grouping is used, the first group has 1-3 digits and every later group exactly 3. A decimal separator must be followed by digits. Fraction digits beyond exp are allowed only if they are zeros, otherwise 'ERR:precision'. Malformed text returns 'ERR:format'; an unknown locale 'ERR:locale'.

Why this case matters

Currency amounts must be rounded at the right stage and in the right unit, or ledgers, quotes and settlements drift by minor units.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(text, locale, exp):
    seps = {'en': (',', '.'), 'de': ('.', ','), 'ch': ("'", '.'), 'fr': (' \u202f', ',')}
    if locale not in seps: return 'ERR:locale'
    groups, dec = seps[locale]
    dig = lambda t: t != '' and all(c in '0123456789' for c in t)
    s = text.strip()
    neg = s.startswith('-')
    if neg: s = s[1:]
    whole, _, frac = s.partition(dec)
    for g in groups[1:]: whole = whole.replace(g, groups[0])
    parts = whole.split(groups[0])
    if not all(dig(p) for p in parts) or (dec in s and not dig(frac)): return 'ERR:format'
    if len(parts) > 1 and (len(parts[0]) > 3 or any(len(p) != 3 for p in parts[1:])): return 'ERR:format'
    if len(frac) > exp: return 'ERR:precision'
    frac = frac[:exp].ljust(exp, '0')
    v = int(''.join(parts)) * 10 ** exp + (int(frac) if frac else 0)
    return -v if neg else v
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = [[('regression excess-fraction-zeros 1', ('1\u202f234,500', 'fr', 2), 123450),
  ('regression excess-fraction-zeros 2', ('100000,0010', 'fr', 3), 100000001),
  ('partial repair guard 1', ('12.345', 'en', 2), 'ERR:precision'),
  ('partial repair guard 2', ('999,50', 'fr', 0), 'ERR:precision'),
  ('control: english grouping', ('1,234.56', 'en', 2), 123456),
  ('control: german short fraction', ('1.234,5', 'de', 2), 123450),
  ('control: negative below one', ('-0,50', 'de', 2), -50),
  ('control: bad grouping', ('1,23,4.00', 'en', 2), 'ERR:format')],
 [('regression excess-fraction-zeros 1', ('-0,500', 'fr', 2), -50),
  ('regression excess-fraction-zeros 2', ('12.345,500', 'de', 2), 1234550),
  ('partial repair guard 1', ('999.500', 'en', 0), 'ERR:precision'),
  ('partial repair guard 2', ('-1.000,0010', 'de', 0), 'ERR:precision'),
  ('control: narrow space with trailing zero', ('1\u202f234,500', 'fr', 2), 123450),
  ('control: excess precision', ('12.345', 'en', 2), 'ERR:precision'),
  ('control: bad grouping', ('1,23,4.00', 'en', 2), 'ERR:format'),
  ('control: dinar fraction', ('12.5', 'en', 3), 12500)],
 [('regression excess-fraction-zeros 1', ('12345.0010', 'en', 3), 12345001),
  ('regression excess-fraction-zeros 2', ('1\u202f000,0010', 'fr', 3), 1000001),
  ('partial repair guard 1', ('999,0010', 'fr', 2), 'ERR:precision'),
  ('partial repair guard 2', ('100000.5', 'ch', 0), 'ERR:precision'),
  ('control: dinar fraction', ('12.5', 'en', 3), 12500), ('control: swiss integer', ("1'000", 'ch', 0), 1000),
  ('control: french plain space', ('12 345,6', 'fr', 2), 1234560),
  ('control: dangling separator', ('12.', 'en', 2), 'ERR:format')],
 [('regression excess-fraction-zeros 1', ('-999.0010', 'ch', 3), -999001),
  ('regression excess-fraction-zeros 2', ('1.234.567,500', 'de', 2), 123456750),
  ('partial repair guard 1', ('12345.500', 'ch', 0), 'ERR:precision'),
  ('partial repair guard 2', ('-1234567,123', 'fr', 0), 'ERR:precision'),
  ('control: dangling separator', ('12.', 'en', 2), 'ERR:format'),
  ('control: unknown locale', ('12', 'it', 2), 'ERR:locale'),
  ('control: oversized first group', ('1234,567.00', 'en', 2), 'ERR:format'),
  ('control: english grouping', ('1,234.56', 'en', 2), 123456)],
 [('regression excess-fraction-zeros 1', ('0,500', 'fr', 2), 50),
  ('regression excess-fraction-zeros 2', ('1.000,500', 'de', 2), 100050),
  ('partial repair guard 1', ('999,500', 'de', 0), 'ERR:precision'),
  ('partial repair guard 2', ('12345.123', 'en', 0), 'ERR:precision'),
  ('control: english grouping', ('1,234.56', 'en', 2), 123456),
  ('control: german short fraction', ('1.234,5', 'de', 2), 123450),
  ('control: negative below one', ('-0,50', 'de', 2), -50),
  ('control: narrow space with trailing zero', ('1\u202f234,500', 'fr', 2), 123450)]]
for label, args, expected in cases[N - 1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
regression excess-fraction-zeros 1ERR:precision123450Failed
regression excess-fraction-zeros 2ERR:precision100000001Failed
partial repair guard 1ERR:precisionERR:precisionPassed
partial repair guard 2ERR:precisionERR:precisionPassed
control: english grouping123456123456Passed
control: german short fraction123450123450Passed
control: negative below one-50-50Passed
control: bad groupingERR:formatERR:formatPassed

SHA-256 / 9432d0ceaacce46d957f5658dc6ecdb1a80ff6a1fc39a33c08a4dd715c2c5bf6

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(text, locale, exp):
    seps = {'en': (',', '.'), 'de': ('.', ','), 'ch': ("'", '.'), 'fr': (' \u202f', ',')}
    if locale not in seps: return 'ERR:locale'
    groups, dec = seps[locale]
    dig = lambda t: t != '' and all(c in '0123456789' for c in t)
    s = text.strip()
    neg = s.startswith('-')
    if neg: s = s[1:]
    whole, _, frac = s.partition(dec)
    for g in groups[1:]: whole = whole.replace(g, groups[0])
    parts = whole.split(groups[0])
    if not all(dig(p) for p in parts) or (dec in s and not dig(frac)): return 'ERR:format'
    if len(parts) > 1 and (len(parts[0]) > 3 or any(len(p) != 3 for p in parts[1:])): return 'ERR:format'
    pass
    frac = frac[:exp].ljust(exp, '0')
    v = int(''.join(parts)) * 10 ** exp + (int(frac) if frac else 0)
    return -v if neg else v
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = [[('regression excess-fraction-zeros 1', ('1\u202f234,500', 'fr', 2), 123450),
  ('regression excess-fraction-zeros 2', ('100000,0010', 'fr', 3), 100000001),
  ('partial repair guard 1', ('12.345', 'en', 2), 'ERR:precision'),
  ('partial repair guard 2', ('999,50', 'fr', 0), 'ERR:precision'),
  ('control: english grouping', ('1,234.56', 'en', 2), 123456),
  ('control: german short fraction', ('1.234,5', 'de', 2), 123450),
  ('control: negative below one', ('-0,50', 'de', 2), -50),
  ('control: bad grouping', ('1,23,4.00', 'en', 2), 'ERR:format')],
 [('regression excess-fraction-zeros 1', ('-0,500', 'fr', 2), -50),
  ('regression excess-fraction-zeros 2', ('12.345,500', 'de', 2), 1234550),
  ('partial repair guard 1', ('999.500', 'en', 0), 'ERR:precision'),
  ('partial repair guard 2', ('-1.000,0010', 'de', 0), 'ERR:precision'),
  ('control: narrow space with trailing zero', ('1\u202f234,500', 'fr', 2), 123450),
  ('control: excess precision', ('12.345', 'en', 2), 'ERR:precision'),
  ('control: bad grouping', ('1,23,4.00', 'en', 2), 'ERR:format'),
  ('control: dinar fraction', ('12.5', 'en', 3), 12500)],
 [('regression excess-fraction-zeros 1', ('12345.0010', 'en', 3), 12345001),
  ('regression excess-fraction-zeros 2', ('1\u202f000,0010', 'fr', 3), 1000001),
  ('partial repair guard 1', ('999,0010', 'fr', 2), 'ERR:precision'),
  ('partial repair guard 2', ('100000.5', 'ch', 0), 'ERR:precision'),
  ('control: dinar fraction', ('12.5', 'en', 3), 12500), ('control: swiss integer', ("1'000", 'ch', 0), 1000),
  ('control: french plain space', ('12 345,6', 'fr', 2), 1234560),
  ('control: dangling separator', ('12.', 'en', 2), 'ERR:format')],
 [('regression excess-fraction-zeros 1', ('-999.0010', 'ch', 3), -999001),
  ('regression excess-fraction-zeros 2', ('1.234.567,500', 'de', 2), 123456750),
  ('partial repair guard 1', ('12345.500', 'ch', 0), 'ERR:precision'),
  ('partial repair guard 2', ('-1234567,123', 'fr', 0), 'ERR:precision'),
  ('control: dangling separator', ('12.', 'en', 2), 'ERR:format'),
  ('control: unknown locale', ('12', 'it', 2), 'ERR:locale'),
  ('control: oversized first group', ('1234,567.00', 'en', 2), 'ERR:format'),
  ('control: english grouping', ('1,234.56', 'en', 2), 123456)],
 [('regression excess-fraction-zeros 1', ('0,500', 'fr', 2), 50),
  ('regression excess-fraction-zeros 2', ('1.000,500', 'de', 2), 100050),
  ('partial repair guard 1', ('999,500', 'de', 0), 'ERR:precision'),
  ('partial repair guard 2', ('12345.123', 'en', 0), 'ERR:precision'),
  ('control: english grouping', ('1,234.56', 'en', 2), 123456),
  ('control: german short fraction', ('1.234,5', 'de', 2), 123450),
  ('control: negative below one', ('-0,50', 'de', 2), -50),
  ('control: narrow space with trailing zero', ('1\u202f234,500', 'fr', 2), 123450)]]
for label, args, expected in cases[N - 1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
regression excess-fraction-zeros 1123450123450Passed
regression excess-fraction-zeros 2100000001100000001Passed
partial repair guard 11234ERR:precisionFailed
partial repair guard 2999ERR:precisionFailed
control: english grouping123456123456Passed
control: german short fraction123450123450Passed
control: negative below one-50-50Passed
control: bad groupingERR:formatERR:formatPassed

SHA-256 / 514af0ce63d699fc4980e3de599282df33ee93d2065ecd2dcbaa1cd9035922ac

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(text, locale, exp):
    seps = {'en': (',', '.'), 'de': ('.', ','), 'ch': ("'", '.'), 'fr': (' \u202f', ',')}
    if locale not in seps: return 'ERR:locale'
    groups, dec = seps[locale]
    dig = lambda t: t != '' and all(c in '0123456789' for c in t)
    s = text.strip()
    neg = s.startswith('-')
    if neg: s = s[1:]
    whole, _, frac = s.partition(dec)
    for g in groups[1:]: whole = whole.replace(g, groups[0])
    parts = whole.split(groups[0])
    if not all(dig(p) for p in parts) or (dec in s and not dig(frac)): return 'ERR:format'
    if len(parts) > 1 and (len(parts[0]) > 3 or any(len(p) != 3 for p in parts[1:])): return 'ERR:format'
    if frac[exp:].strip('0'): return 'ERR:precision'
    frac = frac[:exp].ljust(exp, '0')
    v = int(''.join(parts)) * 10 ** exp + (int(frac) if frac else 0)
    return -v if neg else v
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
cases = [[('regression excess-fraction-zeros 1', ('1\u202f234,500', 'fr', 2), 123450),
  ('regression excess-fraction-zeros 2', ('100000,0010', 'fr', 3), 100000001),
  ('partial repair guard 1', ('12.345', 'en', 2), 'ERR:precision'),
  ('partial repair guard 2', ('999,50', 'fr', 0), 'ERR:precision'),
  ('control: english grouping', ('1,234.56', 'en', 2), 123456),
  ('control: german short fraction', ('1.234,5', 'de', 2), 123450),
  ('control: negative below one', ('-0,50', 'de', 2), -50),
  ('control: bad grouping', ('1,23,4.00', 'en', 2), 'ERR:format')],
 [('regression excess-fraction-zeros 1', ('-0,500', 'fr', 2), -50),
  ('regression excess-fraction-zeros 2', ('12.345,500', 'de', 2), 1234550),
  ('partial repair guard 1', ('999.500', 'en', 0), 'ERR:precision'),
  ('partial repair guard 2', ('-1.000,0010', 'de', 0), 'ERR:precision'),
  ('control: narrow space with trailing zero', ('1\u202f234,500', 'fr', 2), 123450),
  ('control: excess precision', ('12.345', 'en', 2), 'ERR:precision'),
  ('control: bad grouping', ('1,23,4.00', 'en', 2), 'ERR:format'),
  ('control: dinar fraction', ('12.5', 'en', 3), 12500)],
 [('regression excess-fraction-zeros 1', ('12345.0010', 'en', 3), 12345001),
  ('regression excess-fraction-zeros 2', ('1\u202f000,0010', 'fr', 3), 1000001),
  ('partial repair guard 1', ('999,0010', 'fr', 2), 'ERR:precision'),
  ('partial repair guard 2', ('100000.5', 'ch', 0), 'ERR:precision'),
  ('control: dinar fraction', ('12.5', 'en', 3), 12500), ('control: swiss integer', ("1'000", 'ch', 0), 1000),
  ('control: french plain space', ('12 345,6', 'fr', 2), 1234560),
  ('control: dangling separator', ('12.', 'en', 2), 'ERR:format')],
 [('regression excess-fraction-zeros 1', ('-999.0010', 'ch', 3), -999001),
  ('regression excess-fraction-zeros 2', ('1.234.567,500', 'de', 2), 123456750),
  ('partial repair guard 1', ('12345.500', 'ch', 0), 'ERR:precision'),
  ('partial repair guard 2', ('-1234567,123', 'fr', 0), 'ERR:precision'),
  ('control: dangling separator', ('12.', 'en', 2), 'ERR:format'),
  ('control: unknown locale', ('12', 'it', 2), 'ERR:locale'),
  ('control: oversized first group', ('1234,567.00', 'en', 2), 'ERR:format'),
  ('control: english grouping', ('1,234.56', 'en', 2), 123456)],
 [('regression excess-fraction-zeros 1', ('0,500', 'fr', 2), 50),
  ('regression excess-fraction-zeros 2', ('1.000,500', 'de', 2), 100050),
  ('partial repair guard 1', ('999,500', 'de', 0), 'ERR:precision'),
  ('partial repair guard 2', ('12345.123', 'en', 0), 'ERR:precision'),
  ('control: english grouping', ('1,234.56', 'en', 2), 123456),
  ('control: german short fraction', ('1.234,5', 'de', 2), 123450),
  ('control: negative below one', ('-0,50', 'de', 2), -50),
  ('control: narrow space with trailing zero', ('1\u202f234,500', 'fr', 2), 123450)]]
for label, args, expected in cases[N - 1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
regression excess-fraction-zeros 1123450123450Passed
regression excess-fraction-zeros 2100000001100000001Passed
partial repair guard 1ERR:precisionERR:precisionPassed
partial repair guard 2ERR:precisionERR:precisionPassed
control: english grouping123456123456Passed
control: german short fraction123450123450Passed
control: negative below one-50-50Passed
control: bad groupingERR:formatERR:formatPassed

SHA-256 / f8599e0c44dd3df62cc4378fa9b6549755c371f0c6e6206db0894c5d93ab8cb1

Verification & scope

A deterministic, bounded teaching model with a stipulated toy contract; it makes no claim of conformance to any real regulation, standard, or institution's rules. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:47:01.709511+00:00.

Case digest / 96d6565f06026eb5c0700e4b06b1b2b8fc7bf424310e80c5449ff1ec1c87d1e5