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 October 1, 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 a whole line of music players, taken down a cloud platform, and quietly broken age calculations in systems that passed every normal test.
The difference is what software does with the field. A name is mostly stored and displayed. A birthday is calculated against: age, eligibility, cohorts, even the zodiac sign on a generated profile. Every one of those calculations 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 come in four lengths. One day exists only three years in four. The leap rule has an exception, and the exception has an exception. And there are competing ways to write a date down. A stored birthday touches all of them, again and again, for as long as the record lives.
The failures are also lopsided. Rejecting a valid date locks a real person out of a signup form. Accepting an invalid one plants a value that breaks later, in whatever 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 real. The rule behind their birthday is the one developers most often get half right. A year is a leap year if it divides by 4, except centuries, unless the century divides by 400. So 2000 was a leap year, 1900 was not, and 2100 will not be. A home-made divisible-by-4 check works perfectly for seventy-odd more years, then fails [leap-year-wiki].
Even lawmakers have had to patch this case. In Hong Kong, Taiwan and the United Kingdom, a leap-day birthday counts as 1 March in common years. Hong Kong's ordinance says so outright, and Taiwan's civil code implies it. Contracts and coming-of-age rules need an answer for a birthday that does not exist three years in four [leap-year-wiki]. Your age 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 froze on start-up worldwide. The clock driver's loop subtracted 365 days at a time and had no branch for a remainder of exactly 366. It spun until the calendar rolled over at midnight.
On 28 February 2012, Pacific time, already 29 February in UTC, Windows Azure went down. Certificates were being issued with a validity of exactly one year, by changing the year without changing the day. 29 February 2012 plus one year is 29 February 2013, which does not exist. The failures spread into an outage that lasted most of a day. Neither bug was in date parsing. Both were in date arithmetic, the part that runs long after the form said everything was fine.
The year 1900, and Excel's deliberate bug
Spreadsheets are where birthday data gets mangled, and the best-known example is deliberate. Excel's 1900 date system treats 1900 as a leap year. Serial number 60 is 29 February 1900, a day that never happened. Microsoft documents this and kept it on purpose, for compatibility with Lotus 1-2-3, which made the same error first [excel-1900].
The effects on birthday data are real. Weekdays for early-1900 dates are wrong by a day. And the 1900 system cannot store any date before 1 January 1900. Paste a centenarian's 1899 birthday into a date column and it quietly becomes text, which then sorts and filters differently from every value around it. If your data passes through a spreadsheet anywhere, and someday it will, your fixtures should include the dates that expose this.
Ages that are off by one
The most common birthday bug never crashes anything. Subtracting the birth year from the current year overstates a person's age until their birthday, for up to 364 days. Where 13, 18, 21 or 65 unlocks something, that error grants or denies eligibility. Correct age math compares the full month and day. The boundary is exact: a person turns 18 on their birthday, not the day before or after. Only fixtures pinned to the boundary prove it: a birth date exactly 18 years before the reference date, and the same date one day later.
The second off-by-one comes from timezones. A date of birth is a calendar date, not a moment in time: nobody's birthday moves when they fly. But store it in a datetime column and it gains a hidden midnight, usually in UTC. Show that value in any timezone west of Greenwich and it becomes the previous day: born 10 May, shown 9 May. The fix is to keep birth dates as plain ISO 8601 dates, such as 1990-05-10, from end to end [iso8601]. A bug report titled 'my birthday is wrong by one day' almost always has this cause.
1 January, and other defaults that look like data
Plot the birthdays in any large real database and 1 January towers over every other date. Some of that is software. Epoch values like 1970-01-01, the zero of Unix time, leak out of code that confused 'unset' with 'the beginning of time' [unix-time]. But much of it is honest data.
When a person's real date of birth is unknown, officials have long written 1 January. That is common in refugee and immigration paperwork from places where births go unregistered. Of the nearly 80,000 refugees admitted to the United States in 2009, about 11,000 had a 1 January birthday on file [npr-jan1]. The lesson cuts both ways. A pile of 1 January birthdays is not proof of corruption. And a matching system that treats 'same date of birth' as strong evidence will merge thousands of different people who share only a clerk's habit.
Two-digit years and ambiguous formats
The Year 2000 problem was, at heart, a date-of-birth problem. Two-digit years forced every system to guess the century. Rules that read '25' as 1925 or 2025, depending on a cut-off year, filed centenarians as newborns and the reverse [y2k-wiki]. Two-digit years are mostly gone from storage. They survive in date pickers, CSV exports and scanned paper forms.
Their cousin, format ambiguity, is fully alive: 03/04/2001 is 4 March in New York and 3 April in London. Writing dates in ISO 8601 removes the question [iso8601], but only in systems you control. So test data should include dates with a day above 12, because only those cannot be swapped silently. The next epoch is already scheduled. 32-bit Unix timestamps overflow on 19 January 2038, which is closer to today than the Y2K clean-up 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 life of a record, not just the form. Create it, show it in at least two timezones, calculate the age on both sides of the birthday, and export and re-import it in every format you support. The 29 February and 1899 rows fail most often in the export round trip. The boundary rows fail most often in a batch job someone wrote years after the form.
How our generator handles birthdays — including two bugs we shipped
Every profile from our identity generator has a birthday, an age and a zodiac sign, and generation is deterministic from a seed. The engine picks a target age between 18 and 80 and a month and day anywhere in the calendar. It then works out the birth year that makes the age exact on a fixed reference date. There is no clock read, so the same seed always produces the same person. In a check of 10,000 profiles on 1 October 2026, every age matched its birthday.
One result is deliberate: the engine never produces 29 February. The leap day exists only in leap years, so allowing it would tie those birth years to leap years and skew the age spread. A random generator would hit it only once in about 1,461 draws anyway, which makes it a poor way to test that path. For leap-day coverage, take the fixtures from the table above. Random data is the wrong tool for a boundary you can name.
We write this with some humility, because we shipped two of these bugs ourselves. Our first version picked the year first. It then limited the month and day to fall on or before the reference date. The default reference date was 1 January, so the 'anywhere in the calendar' draw collapsed to one value. For a while, every identity this site generated without an explicit reference date was born on 1 January. All of them were Capricorns.
The same routine also capped every month at 28 days, so nobody was ever born on the 29th, 30th or 31st. Both bugs produced flawless individual records: every date valid, every age exact. That is exactly why row-by-row validation never flagged them. Today's engine spreads birthdays across all 365 month-days, with 1 January at its expected share of about one in 365.
The conclusion matches 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. Spend the effort you save on fixtures that walk the boundaries, and on totals that catch what row checks never will. For the fields next to this one, read names that break software and phone numbers that break software. For a thousand consistent profiles to run your own histograms on, the generator is free and reproducible from a seed.
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 person in 1,461 is born on a leap day, so any system that stores dates of birth will meet one. Accept 29 February in leap years and reject it in common years. Then decide, explicitly, what the birthday is in years without a 29 February. The downloadable fixtures use 1 March. Legal eligibility rules need a separate, jurisdiction-specific check.
Why do so many database records show a 1 January birthday?
Two reasons. First, when a real date of birth is unknown, as is common in refugee and immigration paperwork, officials have long filled in 1 January. Genuine records pile up on that date. Second, software defaults: epoch values like 1970-01-01, and placeholder dates typed to get past a required field. A spike on 1 January is a data-quality signal, not a fact about people. Matching logic that relies on date of birth should expect it.
What dates of birth should I include in test fixtures?
At minimum, include 29 February 2000, which is valid because of the 400-year rule. Add 29 February 1900 and 29 February 2001, both invalid for different reasons. Add a date exactly 18 years before your reference date and the day after it. Add 1 January 1970 and 1 January 1900, a pre-1900 date, and a future date that must be rejected. Run each through creation, display, age calculation and an export and import.
How do you calculate someone's age from a date of birth correctly?
Compare the full month and day with the reference date, not just the years. Subtracting the birth year from the current year overstates the age until the birthday, for up to 364 days. That matters wherever 13, 18, 21 or 65 unlocks something. A person turns 18 on their birthday, not the day before or after. Test with dates pinned exactly 18 years back and one day either side. Store the birthday as a plain ISO 8601 date, such as 1990-05-10, never as a timestamp. Otherwise a timezone west of Greenwich shows it a 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 on purpose, for compatibility with Lotus 1-2-3, which made it first. For birthdays, weekdays for early-1900 dates are off by one. No date before 1 January 1900 can be stored as a date at all, so an 1899 birthday pasted into a date column quietly becomes text. If your data passes through a spreadsheet, include those dates in your fixtures.
Why doesn't this site's generator produce 29 February birthdays?
By design. Our engine keeps the age exact by deriving the birth year from the month and day. 29 February only exists in leap years, so allowing it would tie those people to leap years and skew the age spread. Random dates would hit the leap day only about once in 1,461 draws anyway. To test the leap-day path, add those fixtures by hand rather than hoping a generator lands on them.
Update history
- Re-checked the generator claims over 10,000 identities and every outside source, corrected the date of the Azure outage, and rewrote the prose in much shorter sentences.
- Added the 14 downloadable test cases, the question-style FAQs and a section on calculating age correctly.
- Linked the new phone-number test cases from the closing section.
- Published, including the two birthday bugs we shipped in our own generator.