Skip to content
echogeco
Data plumbing · Extract and normalise

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.

codeeasyExercise · 4 min · 30 of 64
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