Normalise the export
Turn a claims export with three date formats, two amount formats and three spellings of open into canonical rows, and return the rows it refused.
On this page
The claims platform's export is the pilot's feed until the read-only view arrives, and it carries three date formats, thousands separators and three spellings of "open" beside a "Re-opened". The normaliser is thirty lines. The thing it must never do is guess.
The situation
The insurer from getting the data out exports claims as a 40-column CSV, 1,200 rows a working day. Four columns feed the triage queue: claim_no, loss_date, amount and status. Two migrations left loss_date in three formats: 2026-03-04 from the current system, day-first 04/03/2026 from the previous one, and 4-Mar-26 from a spreadsheet era, where a two-digit year means 20YY. Amounts carry thousands separators and optional decimals: 1,250.50, 80, 0.5. Statuses arrive as Open, OPEN, open , Re-opened and CLOSED. A re-export repeats rows, the same claim_no twice with the later row the newer. The prototype's parser let a library guess the slash format; it guessed month-first, and every slash date with a day of 12 or under, about two in five, came out swapped without an error, and the model cited a policy term that had not started. The customer's reviewer now wants two things from every load: the canonical rows, and the claim numbers of the rows the loader refused, so a handler can fix them at the source.
The question
Implement solve against this contract so that a row the loader cannot read is refused and named, never guessed and never silently dropped.
def solve(rows):
"""Normalise export rows into canonical claim records.
rows: list of dicts with string values and the keys
'claim_no', 'loss_date', 'amount', 'status'.
Returns (records, rejected):
records — one dict per distinct claim_no, sorted by claim_no, with
'claim_no' stripped and upper-cased
'loss_date' ISO 'YYYY-MM-DD', read by exactly one of the
patterns 'YYYY-MM-DD', 'DD/MM/YYYY', 'D-Mon-YY' (20YY)
'amount' integer minor units: '1,250.50' -> 125050
'status' 'open' | 'closed' | 'reopened'
When a claim_no repeats, the last valid row wins.
rejected — the claim_no, as it appeared in the row, of every row whose
date, amount or status could not be read, in input order.
"""
rows = [
{"claim_no": "c-1001", "loss_date": "2026-03-04", "amount": "1,250.50", "status": "Open"},
{"claim_no": "C-1002", "loss_date": "04/03/2026", "amount": "80", "status": "CLOSED"},
{"claim_no": "C-1003 ", "loss_date": "4-Mar-26", "amount": "0.5", "status": "Re-opened"},
{"claim_no": "C-1004", "loss_date": "2026-13-01", "amount": "10", "status": "Open"},
]
solve(rows) == (
[{"claim_no": "C-1001", "loss_date": "2026-03-04", "amount": 125050, "status": "open"},
{"claim_no": "C-1002", "loss_date": "2026-03-04", "amount": 8000, "status": "closed"},
{"claim_no": "C-1003", "loss_date": "2026-03-04", "amount": 50, "status": "reopened"}],
["C-1004"],
)
A strong answer · Rubric · Where answers go wrong · Solution
Write your answer first, then open