BlogExtraction7 min read

03/04/2026 is two different dates

Date ambiguity is the most common silent corruption in document extraction, and the model cannot resolve it because the document does not contain the answer.

03/04/2026 is the third of April. It is also the fourth of March. Which one it is depends on who printed it, and nothing in those ten characters tells you.

This is the most common silent corruption in document extraction. It does not throw, it does not lower a confidence score, and it produces a date that is entirely plausible — a real date, in the right year, roughly where you expected it. It surfaces a quarter later when a payment term is computed from it and an invoice is chased a month early.

The model is not getting this wrong. The document does not contain the answer, and you asked a question that has two of them.

Why "use a better model" does not apply here

Most extraction failures are a reading problem: the glyph was smudged, the field was rotated, the layout was novel. A stronger model reads better.

Ambiguous dates are not a reading problem. The model reads 03/04/2026 perfectly. It then has to guess a convention, and it guesses using whatever is most common in its training data — which means a US-leaning guess applied uniformly to a document set that is not uniformly US. You will not notice, because a guess and a fact look identical once they are an ISO string in a column.

What actually resolves it

Four signals, in descending order of reliability.

A component above 12. 13/04/2026 can only be DD/MM. This is the strongest signal available and it is free — and critically, it generalises. Format is a property of the issuer, not of the individual date. If any date anywhere in a document resolves unambiguously, every other date in that document follows the same convention.

The issuer's locale. You usually know who sent the document before you parse it. A supplier record with a country is worth more than any heuristic. Store the resolved format on the supplier the first time you determine it, and every subsequent document from them is unambiguous by construction.

Other evidence in the document. A month spelled out anywhere — 4 Mar 2026 in a footer, a period label like March 2026 — settles the whole document. So does a due date computed from the issue date: if the terms say Net 30 and the due date is 03/05/2026, the issue date was 3 April.

The corpus. Across a supplier's history, if you have ever seen a day component above 12, you know their format permanently.

Parse at the boundary, into a type that can say "I don't know"

The mistake is storing "03/04/2026" and dealing with it later. Later, you no longer have the document, the supplier, or the sibling dates that would have resolved it.

type DateResult =
  | { kind: "resolved";  value: PlainDate; basis: Basis }
  | { kind: "ambiguous"; candidates: [PlainDate, PlainDate]; raw: string }

type Basis =
  | "unambiguous"        // a component > 12, or an ISO string
  | "month-name"         // spelled out somewhere
  | "issuer-locale"      // known supplier convention
  | "document-sibling"   // another date in the same document resolved it

function resolveDate(raw: string, ctx: DocumentContext): DateResult {
  const parts = raw.match(/^(\d{1,2})[\/.-](\d{1,2})[\/.-](\d{4})$/)
  if (!parts) return parseIso(raw)

  const [, a, b, year] = parts.map(Number) as unknown as number[]

  // Strongest signal: only one reading is a real date.
  if (a > 12) return resolved(day(a, b, year), "unambiguous")
  if (b > 12) return resolved(day(b, a, year), "unambiguous")

  // Format is a property of the issuer, so a sibling date settles this one.
  const fromDoc = ctx.resolvedFormatInThisDocument
  if (fromDoc) return resolved(apply(fromDoc, a, b, year), "document-sibling")

  const fromIssuer = ctx.issuer?.dateFormat
  if (fromIssuer) return resolved(apply(fromIssuer, a, b, year), "issuer-locale")

  // Both readings are real dates and nothing disambiguates. Say so.
  return {
    kind: "ambiguous",
    candidates: [day(a, b, year), day(b, a, year)],
    raw,
  }
}

Note the two passes this implies. Resolve every date in a document first, and if any one of them lands on unambiguous, re-run the rest with that format fixed. A single 17/03/2026 in a line item settles a header date that would otherwise have been a coin flip.

The ambiguous branch is the whole point

A DateResult that can be ambiguous forces the caller to decide, at a place in the code where the document is still in hand. Roughly one to three per cent of dates in a mixed-locale corpus land there, and they are cheap to resolve — the reviewer sees two dates and picks one, and the answer is written back to the supplier record so that supplier is never ambiguous again.

Compare that to the alternative, which is not "1–3% of dates are wrong". It is "some unknown share of dates are wrong, we do not know which, and we will find out from a customer."

Two smaller ones in the same family

Two-digit years. 03/04/26 is worse, because now three components are in play. Reject them at the boundary and route to review rather than assuming a century.

Timezones on timestamps. If you extract a time as well as a date, a timestamp with no offset is the same class of bug — it renders differently for every reader and the error is silent. Store an offset or store a PlainDate. Never store a naive datetime and hope.

Both are instances of one rule: never persist a string that has more than one meaning. Resolve it where the evidence is, or record that you could not.


More in when extraction accuracy collapses and a schema that fails loudly.

Something here

the audit is the cheapest way to find out for certain.