FAILURE MAP
← Case archive

FA-72351 / Check-digit algorithms / Open access

Luhn normalization keeps hyphen group separators · case 01

Hyphen-grouped card numbers are rejected as malformed.

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

ROOT CAUSE

Only spaces are removed before validation; hyphens remain and fail the digit gate.

VERIFIED REPAIR

Remove both spaces and hyphens, and nothing else, before the digit gate.

Unsuccessful approach: Keeping every digit character silently accepts letters and other junk as if they were separators.

Case contract

Input a card-style number that may contain spaces or hyphens as group separators. After removing them the text must be 2..19 ASCII digits, otherwise "malformed". Doubling applies to every second digit counting from the rightmost (check) digit, doubled values above 9 lose 9, and the number is "valid" when the sum is a multiple of 10, else "invalid".

Why this case matters

Payment-card, loyalty and account numbers are screened with the Luhn mod-10 check before any lookup.

1 / The failure

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

N = 1
observations = []
def solve(s):
    t = s.replace(' ', '')
    if not t.isascii() or not t.isdigit() or len(t) < 2 or len(t) > 19:
        return 'malformed'
    total = 0
    for i, ch in enumerate(reversed(t)):
        d = ord(ch) - 48
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return 'valid' if total % 10 == 0 else 'invalid'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["59"]', ['59'], 'valid'], ['control ["182"]', ['182'], 'valid'], ['control ["97956"]', ['97956'], 'valid'], ['control ["60866456"]', ['60866456'], 'valid'], ['control ["49717735275"]', ['49717735275'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["9207919074163"]', ['9207919074163'], 'valid'], ['control ["470663048070284"]', ['470663048070284'], 'valid'], ['control ["2300017738893848"]', ['2300017738893848'], 'valid'], ['control ["7985145013991721"]', ['7985145013991721'], 'valid'], ['control ["067122965057139178"]', ['067122965057139178'], 'valid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["1513156399001929628"]', ['1513156399001929628'], 'valid'], ['control ["9505094299844733505"]', ['9505094299844733505'], 'valid'], ['control ["0906050370970"]', ['0906050370970'], 'valid'], ['control ["3165356977"]', ['3165356977'], 'valid'], ['control ["91"]', ['91'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["992"]', ['992'], 'valid'], ['control ["5553"]', ['5553'], 'valid'], ['control ["77777779"]', ['77777779'], 'valid'], ['control ["52"]', ['52'], 'invalid'], ['control ["185"]', ['185'], 'invalid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["97959"]', ['97959'], 'invalid'], ['control ["60866459"]', ['60866459'], 'invalid'], ['control ["49717735278"]', ['49717735278'], 'invalid'], ['control ["9207919074166"]', ['9207919074166'], 'invalid'], ['control ["470663048070287"]', ['470663048070287'], 'invalid']]]
for label, args, expected in fixtures[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 ["4111-1111-1111-1111"]malformedvalidFailed
regression ["4111-1111 1111-1112"]malformedinvalidFailed
partial-repair ["4111a1111"]malformedmalformedPassed
control ["59"]validvalidPassed
control ["182"]validvalidPassed
control ["97956"]validvalidPassed
control ["60866456"]validvalidPassed
control ["49717735275"]validvalidPassed

SHA-256 / ce869e502a48ebbf66e770e919b43b1aa3c4ffbba28843d1fae7a6a15bc3f0dc

2 / The unsuccessful fix

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

N = 1
observations = []
def solve(s):
    t = ''.join(c for c in s if c.isdigit())
    if not t.isascii() or not t.isdigit() or len(t) < 2 or len(t) > 19:
        return 'malformed'
    total = 0
    for i, ch in enumerate(reversed(t)):
        d = ord(ch) - 48
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return 'valid' if total % 10 == 0 else 'invalid'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["59"]', ['59'], 'valid'], ['control ["182"]', ['182'], 'valid'], ['control ["97956"]', ['97956'], 'valid'], ['control ["60866456"]', ['60866456'], 'valid'], ['control ["49717735275"]', ['49717735275'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["9207919074163"]', ['9207919074163'], 'valid'], ['control ["470663048070284"]', ['470663048070284'], 'valid'], ['control ["2300017738893848"]', ['2300017738893848'], 'valid'], ['control ["7985145013991721"]', ['7985145013991721'], 'valid'], ['control ["067122965057139178"]', ['067122965057139178'], 'valid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["1513156399001929628"]', ['1513156399001929628'], 'valid'], ['control ["9505094299844733505"]', ['9505094299844733505'], 'valid'], ['control ["0906050370970"]', ['0906050370970'], 'valid'], ['control ["3165356977"]', ['3165356977'], 'valid'], ['control ["91"]', ['91'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["992"]', ['992'], 'valid'], ['control ["5553"]', ['5553'], 'valid'], ['control ["77777779"]', ['77777779'], 'valid'], ['control ["52"]', ['52'], 'invalid'], ['control ["185"]', ['185'], 'invalid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["97959"]', ['97959'], 'invalid'], ['control ["60866459"]', ['60866459'], 'invalid'], ['control ["49717735278"]', ['49717735278'], 'invalid'], ['control ["9207919074166"]', ['9207919074166'], 'invalid'], ['control ["470663048070287"]', ['470663048070287'], 'invalid']]]
for label, args, expected in fixtures[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 ["4111-1111-1111-1111"]validvalidPassed
regression ["4111-1111 1111-1112"]invalidinvalidPassed
partial-repair ["4111a1111"]invalidmalformedFailed
control ["59"]validvalidPassed
control ["182"]validvalidPassed
control ["97956"]validvalidPassed
control ["60866456"]validvalidPassed
control ["49717735275"]validvalidPassed

SHA-256 / a1909b0d5e7f482133005eef921710ae3b73619610b715746bf023dd86a79f61

3 / The verified repair

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

N = 1
observations = []
def solve(s):
    t = s.replace(' ', '').replace('-', '')
    if not t.isascii() or not t.isdigit() or len(t) < 2 or len(t) > 19:
        return 'malformed'
    total = 0
    for i, ch in enumerate(reversed(t)):
        d = ord(ch) - 48
        if i % 2 == 1:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return 'valid' if total % 10 == 0 else 'invalid'
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["59"]', ['59'], 'valid'], ['control ["182"]', ['182'], 'valid'], ['control ["97956"]', ['97956'], 'valid'], ['control ["60866456"]', ['60866456'], 'valid'], ['control ["49717735275"]', ['49717735275'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["9207919074163"]', ['9207919074163'], 'valid'], ['control ["470663048070284"]', ['470663048070284'], 'valid'], ['control ["2300017738893848"]', ['2300017738893848'], 'valid'], ['control ["7985145013991721"]', ['7985145013991721'], 'valid'], ['control ["067122965057139178"]', ['067122965057139178'], 'valid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["1513156399001929628"]', ['1513156399001929628'], 'valid'], ['control ["9505094299844733505"]', ['9505094299844733505'], 'valid'], ['control ["0906050370970"]', ['0906050370970'], 'valid'], ['control ["3165356977"]', ['3165356977'], 'valid'], ['control ["91"]', ['91'], 'valid']], [['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["992"]', ['992'], 'valid'], ['control ["5553"]', ['5553'], 'valid'], ['control ["77777779"]', ['77777779'], 'valid'], ['control ["52"]', ['52'], 'invalid'], ['control ["185"]', ['185'], 'invalid']], [['regression ["4111-1111-1111-1111"]', ['4111-1111-1111-1111'], 'valid'], ['regression ["4111-1111 1111-1112"]', ['4111-1111 1111-1112'], 'invalid'], ['partial-repair ["4111a1111"]', ['4111a1111'], 'malformed'], ['control ["97959"]', ['97959'], 'invalid'], ['control ["60866459"]', ['60866459'], 'invalid'], ['control ["49717735278"]', ['49717735278'], 'invalid'], ['control ["9207919074166"]', ['9207919074166'], 'invalid'], ['control ["470663048070287"]', ['470663048070287'], 'invalid']]]
for label, args, expected in fixtures[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 ["4111-1111-1111-1111"]validvalidPassed
regression ["4111-1111 1111-1112"]invalidinvalidPassed
partial-repair ["4111a1111"]malformedmalformedPassed
control ["59"]validvalidPassed
control ["182"]validvalidPassed
control ["97956"]validvalidPassed
control ["60866456"]validvalidPassed
control ["49717735275"]validvalidPassed

SHA-256 / 79518d70bb9bbdc3f3c866b0200ab91a77cd25aa3ba6e8bf898b3dc11ed7fb2d

Verification & scope

A deterministic, bounded teaching model of the named scheme under the stated contract; not a certified validator. 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:48:37.804136+00:00.

Case digest / 54d43de9ee7ad688b0b430a9996fe878b2c900e96fcc0d29a8300cec18ec7b8e