Day 4 of Document Extraction Workbench: Cleanup is code, not a sentence in the prompt
Day 4 added the cleanup rules. A total of 125 becomes 125.00. February 30, a negative amount, and a number with three decimal places are rejected. A missing currency stays null.
The short version
Day 3 got the words off the file. Day 4 decides which of those words are allowed to count as a date, a currency, or a total.
I still did not call a model. The rules are a Python function named normalize. Five tests passed.
What the function accepts
normalize lives in work/extract.py. None stays None. A blank string, including one that is only spaces, is an error. Vendor and invoice number are returned unchanged, aside from stripping extra space.
def normalize(name, value):
if value is None:
return None
value = value.strip()
if not value:
raise ValueError("empty value")
if name == "date":
return date.fromisoformat(value).isoformat()
if name == "currency":
if value.upper() not in {"USD", "EUR", "GBP"}:
raise ValueError("unsupported or ambiguous currency")
return value.upper()
if name == "total":
try:
number = Decimal(value)
except InvalidOperation as error:
raise ValueError("invalid decimal") from error
if not number.is_finite() or number < 0:
raise ValueError("invalid invoice amount")
if number != number.quantize(Decimal("0.01")):
raise ValueError("too many decimal places")
return format(number, ".2f")
return value- A date has to be a real day written
YYYY-MM-DD. usdbecomesUSD. The only codes this project accepts areUSD,EUR, andGBP.$is rejected, because a symbol is not a currency.- Money uses
Decimal, not a binary fraction.125becomes125.00. More than two digits after the decimal point is rejected.
What the tests refuse
5 passed, 3 deselectedThe five passing cases are the ones that should fail the rules:
2026-02-30is not a calendar day.NaNis not a real amount.-1.00is negative. This project covers invoices, not credit notes.12.345has too many decimal places.- A currency of
NonestaysNone. The missing currency on invoice 2 is still not guessed.
Then I checked a value the tests do not cover:
normalize("total", "125")
125.00Why these rules are code
A prompt can say "use ISO dates and two decimal places." The model can still return February 30 or 12.345, and the draft would look fine until someone tried to pay it. normalize raises ValueError before that value is saved as a finished answer.
These exact rules would be wrong for another customer. A credit note needs negative amounts. Yen does not use two decimal places. The supported set is three currencies, and adding another one means revisiting the money rules, not only the prompt.
What I set up on Day 4
Day 4 was two commits.
feat: normalize dates, currency, and totals.normalizenow checks dates, currency codes, and amounts.docs: move the journal to the repo root.NOTES.mdmoved out ofwork/so the lesson notes sit next to the README.
Frequently asked questions
What does normalize("total", "125") return?
125.00. The raw value can leave off the cents. The cleaned value always has two decimal places.
Why is February 30 rejected?
date.fromisoformat only accepts a real calendar day. 2026-02-30 raises ValueError.
Why does a missing currency stay null?
None returns immediately, before the currency code is checked. An empty fact stays empty. $ still does not become USD.
Did a model apply these rules?
No. The rules are Python. A later lesson can still ask a model to extract the fields. These checks run after that answer comes back.
Next up
The reading and the cleanup rules exist, and a model has not touched an invoice yet. I will write about what I learn as I go, including the mistakes.
Related: Day 3 of Document Extraction Workbench: A page I can read can still have no text
