FA-72826 / Check-digit algorithms / Open access
DNI validation is case-sensitive · case 01
Numbers typed in lowercase are reported as mismatches or malformed.
ROOT CAUSE
The input is not upper-cased before the prefix and letter checks.
VERIFIED REPAIR
Upper-case the whole identifier first.
Unsuccessful approach: Upper-casing only the control letter leaves a lowercase NIE prefix unmapped.
Case contract
Spanish DNI/NIE control letter: input upper-cased, hyphens removed, then nine characters: eight digits (or X/Y/Z followed by seven digits, the prefix standing for 0/1/2) and a letter (else "malformed"). The letter is "TRWAGMYFPDXBNJZSQVHLCKE"[number mod 23]. Return [expected letter, match].
Why this case matters
Tax and KYC forms validate the control letter of Spanish national and foreigner identity numbers.
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 len(t) != 9 or not t.isascii():
return 'malformed'
body = t[:8]
if body[0] in 'XYZ':
body = str('XYZ'.index(body[0])) + body[1:]
if not body.isdigit() or not t[8].isalpha():
return 'malformed'
letter = 'TRWAGMYFPDXBNJZSQVHLCKE'[int(body) % 23]
return [letter, t[8] == letter]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["11888450T"]', ['11888450T'], ['A', False]], ['control ["15183137S"]', ['15183137S'], ['D', False]], ['control ["96436549C"]', ['96436549C'], ['X', False]], ['control ["32369867Q"]', ['32369867Q'], ['N', False]], ['control ["84574425H"]', ['84574425H'], ['K', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["09456394Q"]', ['09456394Q'], ['J', False]], ['control ["06580396K"]', ['06580396K'], ['G', False]], ['control ["74087956U"]', ['74087956U'], ['B', False]], ['control ["08401512F"]', ['08401512F'], ['A', False]], ['control ["73708311E"]', ['73708311E'], ['G', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["Z8303596E"]', ['Z8303596E'], ['A', False]], ['control ["Y9463691F"]', ['Y9463691F'], ['X', False]], ['control ["Y6311829O"]', ['Y6311829O'], ['E', False]], ['control ["Y1496345D"]', ['Y1496345D'], ['W', False]], ['control ["Y4913243Z"]', ['Y4913243Z'], ['C', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["Z4573064S"]', ['Z4573064S'], ['W', False]], ['control ["Y2667738D"]', ['Y2667738D'], ['M', False]], ['control ["X4304629M"]', ['X4304629M'], ['H', False]], ['control ["Z8719589B"]', ['Z8719589B'], ['H', False]], ['control ["X5964504D"]', ['X5964504D'], ['Y', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["12345678Z"]', ['12345678Z'], ['Z', True]], ['control ["12345678-Z"]', ['12345678-Z'], ['Z', True]], ['control ["X1234567L"]', ['X1234567L'], ['L', True]], ['control ["Y1234567X"]', ['Y1234567X'], ['X', True]], ['control ["Z1234567R"]', ['Z1234567R'], ['R', True]]]]
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression ["12345678z"] | ['Z', False] | ['Z', True] | Failed |
| regression ["x1234567l"] | malformed | ['L', True] | Failed |
| partial-repair ["y7654321g"] | malformed | ['G', True] | Failed |
| control ["11888450T"] | ['A', False] | ['A', False] | Passed |
| control ["15183137S"] | ['D', False] | ['D', False] | Passed |
| control ["96436549C"] | ['X', False] | ['X', False] | Passed |
| control ["32369867Q"] | ['N', False] | ['N', False] | Passed |
| control ["84574425H"] | ['K', False] | ['K', False] | Passed |
SHA-256 / b2cf32b6148042e6be3ac9e52375a0ec92cdad49686d4a2f5e32d718117de20e
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(s):
t = s.replace('-', '')
if len(t) != 9 or not t.isascii():
return 'malformed'
body = t[:8]
if body[0] in 'XYZ':
body = str('XYZ'.index(body[0])) + body[1:]
if not body.isdigit() or not t[8].isalpha():
return 'malformed'
letter = 'TRWAGMYFPDXBNJZSQVHLCKE'[int(body) % 23]
return [letter, t[8].upper() == letter]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["11888450T"]', ['11888450T'], ['A', False]], ['control ["15183137S"]', ['15183137S'], ['D', False]], ['control ["96436549C"]', ['96436549C'], ['X', False]], ['control ["32369867Q"]', ['32369867Q'], ['N', False]], ['control ["84574425H"]', ['84574425H'], ['K', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["09456394Q"]', ['09456394Q'], ['J', False]], ['control ["06580396K"]', ['06580396K'], ['G', False]], ['control ["74087956U"]', ['74087956U'], ['B', False]], ['control ["08401512F"]', ['08401512F'], ['A', False]], ['control ["73708311E"]', ['73708311E'], ['G', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["Z8303596E"]', ['Z8303596E'], ['A', False]], ['control ["Y9463691F"]', ['Y9463691F'], ['X', False]], ['control ["Y6311829O"]', ['Y6311829O'], ['E', False]], ['control ["Y1496345D"]', ['Y1496345D'], ['W', False]], ['control ["Y4913243Z"]', ['Y4913243Z'], ['C', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["Z4573064S"]', ['Z4573064S'], ['W', False]], ['control ["Y2667738D"]', ['Y2667738D'], ['M', False]], ['control ["X4304629M"]', ['X4304629M'], ['H', False]], ['control ["Z8719589B"]', ['Z8719589B'], ['H', False]], ['control ["X5964504D"]', ['X5964504D'], ['Y', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["12345678Z"]', ['12345678Z'], ['Z', True]], ['control ["12345678-Z"]', ['12345678-Z'], ['Z', True]], ['control ["X1234567L"]', ['X1234567L'], ['L', True]], ['control ["Y1234567X"]', ['Y1234567X'], ['X', True]], ['control ["Z1234567R"]', ['Z1234567R'], ['R', True]]]]
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression ["12345678z"] | ['Z', True] | ['Z', True] | Passed |
| regression ["x1234567l"] | malformed | ['L', True] | Failed |
| partial-repair ["y7654321g"] | malformed | ['G', True] | Failed |
| control ["11888450T"] | ['A', False] | ['A', False] | Passed |
| control ["15183137S"] | ['D', False] | ['D', False] | Passed |
| control ["96436549C"] | ['X', False] | ['X', False] | Passed |
| control ["32369867Q"] | ['N', False] | ['N', False] | Passed |
| control ["84574425H"] | ['K', False] | ['K', False] | Passed |
SHA-256 / 4f9061909ab90af06ece138e3e2cf38c3d54961bc5d240ea1ee5e0c43e62933a
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(s):
t = s.upper().replace('-', '')
if len(t) != 9 or not t.isascii():
return 'malformed'
body = t[:8]
if body[0] in 'XYZ':
body = str('XYZ'.index(body[0])) + body[1:]
if not body.isdigit() or not t[8].isalpha():
return 'malformed'
letter = 'TRWAGMYFPDXBNJZSQVHLCKE'[int(body) % 23]
return [letter, t[8] == letter]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["11888450T"]', ['11888450T'], ['A', False]], ['control ["15183137S"]', ['15183137S'], ['D', False]], ['control ["96436549C"]', ['96436549C'], ['X', False]], ['control ["32369867Q"]', ['32369867Q'], ['N', False]], ['control ["84574425H"]', ['84574425H'], ['K', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["09456394Q"]', ['09456394Q'], ['J', False]], ['control ["06580396K"]', ['06580396K'], ['G', False]], ['control ["74087956U"]', ['74087956U'], ['B', False]], ['control ["08401512F"]', ['08401512F'], ['A', False]], ['control ["73708311E"]', ['73708311E'], ['G', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["Z8303596E"]', ['Z8303596E'], ['A', False]], ['control ["Y9463691F"]', ['Y9463691F'], ['X', False]], ['control ["Y6311829O"]', ['Y6311829O'], ['E', False]], ['control ["Y1496345D"]', ['Y1496345D'], ['W', False]], ['control ["Y4913243Z"]', ['Y4913243Z'], ['C', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["Z4573064S"]', ['Z4573064S'], ['W', False]], ['control ["Y2667738D"]', ['Y2667738D'], ['M', False]], ['control ["X4304629M"]', ['X4304629M'], ['H', False]], ['control ["Z8719589B"]', ['Z8719589B'], ['H', False]], ['control ["X5964504D"]', ['X5964504D'], ['Y', False]]], [['regression ["12345678z"]', ['12345678z'], ['Z', True]], ['regression ["x1234567l"]', ['x1234567l'], ['L', True]], ['partial-repair ["y7654321g"]', ['y7654321g'], ['G', True]], ['control ["12345678Z"]', ['12345678Z'], ['Z', True]], ['control ["12345678-Z"]', ['12345678-Z'], ['Z', True]], ['control ["X1234567L"]', ['X1234567L'], ['L', True]], ['control ["Y1234567X"]', ['Y1234567X'], ['X', True]], ['control ["Z1234567R"]', ['Z1234567R'], ['R', True]]]]
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 fixture | Actual | Expected | Outcome |
|---|---|---|---|
| regression ["12345678z"] | ['Z', True] | ['Z', True] | Passed |
| regression ["x1234567l"] | ['L', True] | ['L', True] | Passed |
| partial-repair ["y7654321g"] | ['G', True] | ['G', True] | Passed |
| control ["11888450T"] | ['A', False] | ['A', False] | Passed |
| control ["15183137S"] | ['D', False] | ['D', False] | Passed |
| control ["96436549C"] | ['X', False] | ['X', False] | Passed |
| control ["32369867Q"] | ['N', False] | ['N', False] | Passed |
| control ["84574425H"] | ['K', False] | ['K', False] | Passed |
SHA-256 / f75226d9f3b26f03e18a308654ccc83eba615ce5966e8abeee1fb3dba8b6c45d
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:41.959133+00:00.
Case digest / a96d974674429036e1d687ba3d8384a6e767414b9ee1229905baaeeeddfb47a3