Birthdays That Break Software: Date Edge Cases for QA

February 29, January 1 defaults, the Excel 1900 bug, off-by-one ages: the date-of-birth edge cases that crash real systems, and the fixtures that catch them.

By VincentPublished August 6, 2026Last updated August 6, 20269 min read

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 birthWhat it exercisesExpected behavior
2000-02-29The 400-year rule — 2000 really was a leap yearAccepted; age correct in common years
1996-02-29Ordinary leap dayAccepted; birthday handling defined for common years
1900-02-29The century rule — this day never existedRejected; catches divisible-by-4-only leap logic and Excel's serial 60
2001-02-29Leap day in a common yearRejected
2000-04-31Day 31 in a 30-day monthRejected
Reference date minus exactly 18 yearsThe eligibility boundaryPerson is 18 today — not yesterday
One day after thatThe other side of the boundaryPerson is 17 until tomorrow
1970-01-01Unix epoch sentinel collisionAccepted, but distinguishable from an unset default
1900-01-01Legacy default and spreadsheet epochAccepted; survives an export-import round trip
1899-12-31Pre-1900 centenarianSurvives a spreadsheet round trip without turning into text
2001-12-13Day value over 12Cannot be month-day transposed silently
Tomorrow's dateFuture date of birthRejected at the boundary, not just 'far future'
Date-of-birth fixtures and the failure each one exercises. 'Reference date' means the date your age logic compares against.

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 field next to this one, start with names 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

  1. Leap year — leap day birthdays and the Gregorian ruleWikipedia
  2. Leap year problemWikipedia
  3. Excel incorrectly assumes that the year 1900 is a leap yearMicrosoft Learn
  4. Why So Many Immigrants Have Birthdays On Jan. 1NPR
  5. Year 2000 problemWikipedia
  6. ISO 8601 — date and time formatWikipedia
  7. Unix timeWikipedia

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 question big enough that lawmakers have answered it explicitly: the United Kingdom, Hong Kong and Taiwan all treat a leap-day birthday as falling on 1 March in common years.

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.

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.

We use cookies for analytics and ads to keep this generator free. See our Privacy Policy.