Writing a Reliable Regex for Extracting Delivery Times

TIL that a regex tuned against a handful of examples ('DELIVERY TIME: 2:00 PM') broke on real data within a day because of extra whitespace and inconsistent AM/PM casing I hadn't accounted for.

2 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

The Problem #

Extracting a delivery time from a string like DELIVERY TIME: 2:00 PM seemed simple enough for a quick regex, and the first version worked against every example I'd copied from the POS while writing it — and failed on real orders within the first day.

Context #

The label text came from rendered DOM text content, which meant it carried whatever whitespace, casing, and formatting quirks the POS's own templating happened to produce — not the clean, single-spaced strings I'd been testing against.

What I Tried #

const match = text.match(/DELIVERY TIME: (\d{1,2}:\d{2} [AP]M)/);

What Went Wrong #

Real extracted text had inconsistent whitespace (DELIVERY TIME: 2:00PM, no space before AM/PM), and some orders had lowercase am/pm. Each of these silently failed to match, which meant match was null and the delivery time quietly went missing for that order rather than throwing anything I'd notice immediately.

The Solution #

Made the whitespace and casing assumptions explicit instead of implicit, and normalized the input before matching rather than trying to make one regex handle every variant.

const normalized = text.trim().replace(/\s+/g, " ");
const match = normalized.match(/delivery time:\s*(\d{1,2}:\d{2}\s?[ap]m)/i);
if (!match) {
  console.error("[extension] Could not parse delivery time from:", text);
}

Why It Works #

Normalizing whitespace before matching removes an entire category of "regex almost matches" failures, and the i flag removes the casing assumption instead of trying to encode every casing variant into the pattern itself. Logging the raw text on a failed match also means the next format surprise is visible immediately instead of silently dropped.

Lessons Learned #

A regex that matches every example I hand-picked while writing it is not the same thing as a regex that matches real data — real strings carry formatting noise that curated test cases don't, and that gap is exactly where these bugs live.

What I Would Do Differently #

I'd normalize input and log unmatched cases from the first version, instead of tuning the pattern against a small set of examples and discovering the gap once real orders started failing silently.

Regex normalization before matching, case-insensitive matching, silent-failure vs. loud-failure parsing.