Date of Birth Validation Test Cases: Leap Years & Age
Download 14 date-of-birth test cases with fixed reference dates and expected ages. Cover leap years, invalid dates, future dates and birthday boundaries.
By Vincent RuanPublished August 6, 2026Last updated September 6, 20269 min read
Download date of birth validation test cases
14 fixed cases · Version 1 · Updated 2026-09-06. Each row includes the original input, test context, expected action, expected value and reason.
Test policy: Parse an exact YYYY-MM-DD Gregorian date without timestamp conversion or overflow. Reject impossible dates, empty values and dates after the reference date in context. For accepted values, expectedValue is completed years as a decimal string; age zero is valid. There is no minimum eligibility age or maximum age in this example. In common years, the February 29 birthday advances on March 1 under this fixture policy. This is a declared software convention, not a statement of legal eligibility in any jurisdiction. Import dates as text.
Preview cases and import instructions
JSON is an array of records. CSV uses the same keys and values. Load CSV through an import dialog with every column set to Text: automatic conversion can change leading zeros, plus signs, dates or strings that resemble formulas. JSON preserves Unicode and escaped control characters for automated tests. Empty expectedValue means rejection; it is not a replacement value to store.
Run the input through your implementation with the documented context, compare the action and expectedValue, then check storage and export round trips. Adjust policy-dependent cases to your product before using them as pass/fail gates. Keep the v1 filename with your test results.
| Case | Input | Expected | Value |
|---|---|---|---|
| dob-leap-400 | "2000-02-29" | accept | "26" |
| dob-leap-century | "1900-02-29" | reject | "" |
| dob-leap-common | "2001-02-29" | reject | "" |
| dob-april-31 | "2000-04-31" | reject | "" |
| dob-eighteen | "2008-09-06" | accept | "18" |
Policy background: Microsoft: Excel's 1900 leap-year behavior. Sources explain the standards and design tradeoffs; they do not endorse every application choice in this fixture set.

After we published names that break software, the natural question was which field to distrust next. The answer is the one that looks safest: date of birth. A birthday has a fixed format and only 366 possible month-day values, yet it has frozen an entire product line of music players, taken down a cloud platform for a day, and quietly corrupted age calculations in systems that passed every happy-path test. The difference between a name and a birthday is what software does with it: a name is mostly stored and displayed, but a birthday is computed against — age, eligibility, cohorts, even the zodiac sign on a generated profile — and every one of those computations replays the calendar's exceptions.
Why dates of birth are harder than they look
The calendar is an API with special cases baked in centuries ago: months of four different lengths, a day that exists only three years in four, a leap rule with an exception and an exception to the exception, and two competing conventions for writing the whole thing down. A stored birthday touches all of them, repeatedly, for as long as the record lives. Worse, the failures are asymmetric: rejecting a valid date locks a real person out of a signup form, while accepting an invalid one plants a value that detonates later, in whatever downstream job next does arithmetic on it.
The edge cases that actually break systems
29 February
About one birth in 1,461 lands on a leap day, so leap-day users are not hypothetical — and the rule that creates their birthday is the one developers most often half-implement. A year is a leap year if it is divisible by 4, except centuries, unless the century is divisible by 400: 2000 was a leap year, 1900 was not, and 2100 will not be, which means a hand-rolled divisible-by-4 check works flawlessly for the next seventy-odd years and then stops [leap-year-wiki]. Even lawmakers have had to patch this edge case: the United Kingdom, Hong Kong and Taiwan all legislate that a leap-day birthday falls on 1 March in common years, because contracts and coming-of-age rules need an answer for a birthday that does not exist three years out of four [leap-year-wiki]. Your age-calculation code needs the same explicit decision — and a test that pins it.
The leap day's greatest hits are well documented [leap-bug-wiki]. On 31 December 2008 — the 366th day of a leap year — Microsoft's Zune 30 players worldwide froze on boot: the clock driver's year-conversion loop subtracted 365 days at a time and had no branch for a remainder of exactly 366, so it spun forever until the calendar rolled over at midnight. On 29 February 2012, Windows Azure went down when internal transfer certificates were issued with a validity of exactly one year: 29 February 2012 plus one year is 29 February 2013, a date that does not exist, and the resulting certificate failures cascaded into a day-long outage. Neither bug was in date parsing. Both were in date arithmetic — the part that runs long after the input form said everything was fine.
The year 1900, and Excel's deliberate bug
Spreadsheets are where date-of-birth data goes to get mangled, and the flagship example is intentional. Excel's 1900 date system treats 1900 as a leap year: serial number 60 corresponds to 29 February 1900, a day that never happened. Microsoft documents the behavior and kept it on purpose, for compatibility with Lotus 1-2-3, which shipped the same error first [excel-1900]. The practical consequences for birthday data are real: weekday calculations for early-1900 dates are wrong by a day, and the 1900 system cannot represent any date before 1 January 1900 at all — paste a centenarian's 1899 birthday into a column of dates and it silently becomes text, which then sorts, filters and imports differently from every value around it. If your data pipeline round-trips through a spreadsheet anywhere — and at some point, someone's will — your date fixtures should include the dates that expose it.
Ages that are off by one
The most common birthday bug never crashes anything: subtracting birth year from current year overstates a person's age for the entire portion of the year before their birthday — up to 364 days of being wrong. In systems that gate anything on 13, 18, 21 or 65, that error is not cosmetic; it grants or denies eligibility. Correct age math compares the full month-day pair, and the boundary it creates is exact: a person turns 18 on their birthday, not the day before, not the day after. The only tests that prove this are fixtures pinned to the boundary — a date of birth exactly 18 years before the reference date, and the same date shifted by one day.
The second off-by-one arrives through timezones. A date of birth is a calendar date, not an instant — nobody's birthday shifts when they fly — but the moment it is stored in a datetime column it acquires an implicit midnight, usually UTC. Render that value in any timezone west of Greenwich and it displays as the previous day: born 10 May, shown 9 May. The fix is to keep dates of birth as plain dates in ISO 8601 form (1990-05-10) end to end, and let nothing 'helpfully' promote them to timestamps [iso8601]. If you have ever seen a bug report titled 'my birthday is wrong by one day', this is almost always why.
1 January, and other defaults that look like data
Plot the birthdays in any large real-world database and 1 January towers over every other date. Some of that is software — epoch sentinels like 1970-01-01, the zero value of Unix time, leaking out of code that conflated 'unset' with 'the beginning of time' [unix-time] — but much of it is genuine data recorded honestly. When a person's true date of birth is unknown, which is common in refugee and immigration paperwork from regions where births go unregistered, officials have long filled in 1 January: of the roughly 80,000 refugees admitted to the United States in 2009, about 11,000 had a 1 January birthday on file [npr-jan1]. The lesson for engineers cuts both ways. A heap of 1 January birthdays is not proof of corruption — and a duplicate-detection or identity-matching system that treats 'same date of birth' as strong evidence will quietly merge thousands of distinct people who share nothing but a clerk's convention.
Two-digit years and ambiguous formats
The Year 2000 problem was, at its core, a date-of-birth problem: two-digit years forced every system to guess a century, and windowing rules that read '25' as 1925 or 2025 depending on a pivot value classified centenarians as newborns and vice versa [y2k-wiki]. The two-digit year is mostly gone from storage, but it survives in date pickers, CSV exports and OCR'd paper forms — and its cousin, format ambiguity, is fully alive: 03/04/2001 is the 4th of March in New York and the 3rd of April in London. Unambiguous interchange is a solved problem — write dates as ISO 8601 and the parsing question disappears [iso8601] — but only for the systems you control; test data should still include dates whose day value exceeds 12, because those are the only ones that cannot be silently transposed. And the next epoch is already scheduled: 32-bit Unix timestamps overflow on 19 January 2038, which is closer to today than the Y2K remediation effort is behind us [unix-time].
A birthday fixture set worth copying
| Date of birth | What it exercises | Expected behavior |
|---|---|---|
| 2000-02-29 | The 400-year rule — 2000 really was a leap year | Accepted; age correct in common years |
| 1996-02-29 | Ordinary leap day | Accepted; birthday handling defined for common years |
| 1900-02-29 | The century rule — this day never existed | Rejected; catches divisible-by-4-only leap logic and Excel's serial 60 |
| 2001-02-29 | Leap day in a common year | Rejected |
| 2000-04-31 | Day 31 in a 30-day month | Rejected |
| Reference date minus exactly 18 years | The eligibility boundary | Person is 18 today — not yesterday |
| One day after that | The other side of the boundary | Person is 17 until tomorrow |
| 1970-01-01 | Unix epoch sentinel collision | Accepted, but distinguishable from an unset default |
| 1900-01-01 | Legacy default and spreadsheet epoch | Accepted; survives an export-import round trip |
| 1899-12-31 | Pre-1900 centenarian | Preserve as ISO date text; do not rely on Excel date serials |
| 2001-12-13 | Day value over 12 | Cannot be month-day transposed silently |
| Tomorrow's date | Future date of birth | Rejected at the boundary, not just 'far future' |
Run every fixture through the whole lifecycle, not just the input form: create the record, display it in at least two timezones, compute the age on both sides of the birthday, and round-trip it through whatever export formats your system supports. The 29 February and 1899 rows fail most often in the round trip, and the boundary rows fail most often in a batch job that somebody wrote years after the form validation.
How our generator handles birthdays — including two bugs we shipped
Every profile from our identity generator carries a birthday, an age and a zodiac sign, and the generation is deterministic from a seed: the engine picks a target age between 18 and 80, draws a month and day anywhere in the calendar, then derives the birth year that makes the age exact as of a fixed reference date — no wall-clock reads, so the same seed always produces the same person. One consequence is deliberate: the engine never produces 29 February. The leap day only exists in leap years, so allowing it would constrain birth years to leap years and skew the age distribution — and since a uniform generator would hit it only once in roughly 1,461 draws, it is a poor way to test that path anyway. If your suite needs leap-day coverage, take the fixtures from the table above; random data is the wrong tool for a boundary you can name.
We are writing this guide with some humility, because we shipped two of these bugs ourselves. Our original implementation derived the year first and then constrained the month and day to fall on or before the reference date's — and the engine's default reference was 1 January, so the 'anywhere in the calendar' draw collapsed to a single value: for a while, every identity this site generated without an explicit reference date was born on 1 January, a Capricorn, all of them. The same routine also capped every month at 28 days, so no generated person was ever born on the 29th, 30th or 31st. Both bugs produced individually flawless records — every date valid, every age exact — which is precisely why row-level validation never flagged them.
The conclusion mirrors the one we reached about names: validate less, test more. Reject the dates that cannot exist, accept everything that can — including the leap day, the centenarian and the person who turns 18 today — and spend the effort you save on fixtures that walk the boundaries and on aggregate checks that catch what per-row validation never will. If you want the same treatment of the fields next to this one, start with names that break software and continue with phone numbers that break software; if you want a thousand internally consistent profiles to run your own histograms on, the generator is free and seed-reproducible.
References & sources
- Leap year — leap day birthdays and the Gregorian rule — Wikipedia
- Leap year problem — Wikipedia
- Excel incorrectly assumes that the year 1900 is a leap year — Microsoft Learn
- Why So Many Immigrants Have Birthdays On Jan. 1 — NPR
- Year 2000 problem — Wikipedia
- ISO 8601 — date and time format — Wikipedia
- Unix time — Wikipedia
Frequently asked questions
Is 29 February a valid date of birth?
Yes. Roughly one in 1,461 people is born on a leap day, so any system that stores dates of birth will meet one. Your software must accept 29 February in leap years, reject it in common years, and decide what to do in the years when the birthday does not exist — a policy that should be explicit in your application. The downloadable fixtures use March 1 as their common-year anniversary convention; legal eligibility rules require a separate jurisdiction-specific check.
Why do so many database records show a 1 January birthday?
Two reasons, neither of them astrology. First, when a real date of birth is unknown — common in refugee and immigration paperwork — registrars have long filled in 1 January, so genuine records heap on that date. Second, software defaults: epoch values like 1970-01-01 and placeholder dates entered to get past a required field. A spike of 1 January birthdays is a data-quality signal, not a demographic fact, and identity-matching logic that weights date of birth should expect it.
What dates of birth should I include in test fixtures?
At minimum: 29 February 2000 (valid — the 400-year rule makes 2000 a leap year), 29 February 1900 and 29 February 2001 (both invalid, for different reasons), a date exactly 18 years before your reference date and the day after it (the eligibility boundary), 1 January 1970 and 1 January 1900 (sentinel collisions), 31 December and a pre-1900 date for round-trip testing, and a date in the future (must be rejected). Run each through creation, display, age calculation, and an export-import cycle.
How do you calculate someone's age from a date of birth correctly?
Compare the full month-day pair against the reference date, not just the years. Subtracting the birth year from the current year overstates the age for the whole part of the year before the birthday — up to 364 days of being wrong — which matters wherever 13, 18, 21 or 65 gates something. A person turns 18 on their birthday, not the day before or after, and the only tests that prove the boundary are fixtures pinned exactly 18 years before the reference date and one day either side. Store the birthday as a plain ISO 8601 date such as 1990-05-10, never as a timestamp, or a timezone west of Greenwich will display it one day early.
What is the Excel 1900 leap year bug?
Excel's 1900 date system treats 1900 as a leap year: serial number 60 is 29 February 1900, a day that never existed. Microsoft kept the error deliberately for compatibility with Lotus 1-2-3, which shipped it first. For date-of-birth data the consequences are that weekday calculations for early-1900 dates are off by one, and no date before 1 January 1900 can be represented at all — a centenarian's 1899 birthday pasted into a date column silently becomes text. If your pipeline round-trips through a spreadsheet anywhere, include those dates in your fixtures.
Why doesn't this site's generator produce 29 February birthdays?
By design. Our engine keeps the generated age exact by deriving the birth year from the month and day, and 29 February only exists in leap years — allowing it would constrain birth years to leap years and distort the age distribution. Uniform random dates would only hit the leap day about once in 1,461 draws anyway, so if you need to test the leap-day code path, add those fixtures by hand rather than hoping a generator lands on them.