Validating ROC Date Input: Regex and Edge Cases
A working starting pattern: ^([1-9][0-9]{2})[/.\-](0[1-9]|1[0-2])[/.\-](0[1-9]|[12][0-9]|3[01])$.
It checks shape and range, but regex alone cannot verify a real calendar date — you still
need to construct it and see if it succeeds.
What the regex actually checks
This pattern matches a three-digit ROC year, a two-digit month from 01–12, and a two-digit day from
01–31, separated by /, . or -:
^([1-9][0-9]{2})[/.\-](0[1-9]|1[0-2])[/.\-](0[1-9]|[12][0-9]|3[01])$
| Matches | 115/07/29 · 099-01-01 · 100.12.31 |
|---|---|
| Rejects | 15/07/29 (two-digit year) · 115/13/01 (month 13) · 115/02/30 (regex can't catch this — see below) |
Test any specific value against the ROC year converter once it passes validation, as a sanity check before it goes further into a pipeline.
Why 115/02/30 passes the regex and shouldn't
The pattern above accepts day 30 for any month, because encoding "30 is invalid in February, and 31 is invalid in April, June, September and November, and 29 is only valid in February during a leap year" directly into a regular expression produces an unreadable, hard-to-maintain pattern for very little benefit. The practical answer is to let the regex handle shape and obvious range violations, then hand the three numbers to your language's date constructor and catch the error it raises for a genuinely invalid combination:
// Python
from datetime import date
def is_valid_roc_date(roc_year: int, month: int, day: int) -> bool:
if roc_year <= 0:
return False
try:
date(roc_year + 1911, month, day)
return True
except ValueError:
return False
is_valid_roc_date(115, 2, 30) # False -- February has no 30th
is_valid_roc_date(112, 2, 29) # False -- 2023 (ROC 112) is not a leap year
is_valid_roc_date(113, 2, 29) # True -- 2024 (ROC 113) is a leap year
Every mainstream language does this the same way: build the date, catch the exception (or check the error return, in Go). Regex narrows the input to plausible shapes; the date constructor is the actual calendar authority.
The checks regex genuinely handles well
Format and obvious range violations are exactly what regex is good at, and worth keeping even though the final validity check happens elsewhere:
- Rejecting a two-digit year, which is either a truncated ROC 100+ value or a different, non-ROC format entirely — see the Y1C section below.
- Rejecting a month outside 01–12 and a day outside 01–31 before they reach a date constructor, which gives a clearer error message than a generic "invalid date" exception.
- Rejecting stray whitespace, extra fields, or a separator mismatch (
115/07-29) that suggests the row was corrupted during export rather than genuinely being a different date format.
ROC year 0 and before-ROC dates
Reject an ROC year of 0 or negative outright; it is not a valid pre-1912 date, it's almost always a parsing bug or a field that was never an ROC year in the first place. Where you genuinely need to represent a pre-Republic date, use a separate "before ROC N" field or a plain Gregorian year rather than trying to force it through the same ROC-year-plus-1911 formula, which was never designed to run backward past year 1.
Detecting a Y1C-truncated field
A two-digit year field that should hold a three-digit ROC year is the classic sign of the
Y1C bug: ROC 100 (2011) stored as 00, ROC 101 as
01, and so on. Regex can flag the shape (two digits where three are expected), but it cannot tell
you whether 00 means ROC 100 or a genuinely malformed record — that requires checking
neighbouring rows for a plausible sequence, or falling back to a manual review of the source document.
Match your validator to your actual input, not every possible format
It's tempting to write one maximally permissive pattern that accepts every separator and every width you can imagine. Resist it. A validator that's too permissive will accept malformed strings that happen to look date-shaped — a product code, a phone extension, an unrelated ID — and misclassify them as dates. Write the pattern for the formats you've actually confirmed your data source produces, and reject everything else with a clear error rather than trying to guess.
Frequently asked questions
What regex validates a ROC date like 115/07/29?
^([1-9][0-9]{2})[/.\-](0[1-9]|1[0-2])[/.\-](0[1-9]|[12][0-9]|3[01])$ checks a three-digit ROC
year, a month 01–12 and a day 01–31 separated by /, . or -.
It does not check the day against the actual month length or leap years — that needs a real date library,
not regex.
Why can't regex alone fully validate a date?
Because whether day 30 or 31 is valid depends on the month, and whether 29 February is valid depends on whether the Gregorian year (ROC year + 1911) is a leap year. A regex can check format and range, but the calendar rule that ties day to month and year has to be checked by actually constructing the date and seeing if it succeeds.
How do I check a ROC year isn't zero or negative?
Reject any parsed ROC year that is 0 or less before conversion. There is no ROC year 0 — the calendar runs from before-ROC years straight to ROC year 1 (1912) — so a value of 0 almost always means the input was malformed or mis-parsed, not a legitimate date.
Should validation accept both 115/07/29 and 115.7.29?
Only if your input source genuinely produces both. Accepting more formats than you actually receive increases the chance of misreading an unrelated string as a date. Match the validator to your actual input formats and reject everything else explicitly, rather than writing a maximally permissive pattern.
How do I detect a Y1C-truncated two-digit year during validation?
If a field that should hold a three-digit ROC year contains only two digits, or contains 00
where a sequential ROC year 100 record is expected, flag it rather than silently accepting the low value. A
two-digit field cannot be safely reconstructed from pattern alone — it needs a lookup against the
surrounding record or a manual review.
Related
- ROC year converter — convert any full date, with weekday and formal written form
- The Y1C problem — when ROC year 100 broke two-digit date fields in 2011
- Storing ROC dates in a database — do's and don'ts for schema design
- ROC dates in Python — date() raises ValueError on the invalid combinations regex misses
- ROC dates in JavaScript — the same validation problem, browser and Node side
- FAQ — ROC years, lunar dates, zodiac and age
- ROC dates in Excel — locale traps when parsing ROC dates