Methodology
Age calculation methodology
Calendar-aware borrowing, not naive division. The 4-day error that naive code accumulates.
By Buğra SözeriPublished Updated
Computing age in years-months-days sounds trivial. It isn’t. The naive “today minus birthday divided by 365.25” approach drifts about one day every four to seven years because real years aren’t a constant number of days. The correct algorithm uses calendar borrowing — same logic as elementary-school subtraction but with variable-length “digits.”
The borrow-from-month algorithm
Given a birth date (bY-bM-bD) and today (tY-tM-tD):
- Year diff:
tY − bY. - Month diff:
tM − bM. If negative, borrow 1 from the year count: subtract 1 from year diff, add 12 to month diff. - Day diff:
tD − bD. If negative, borrow 1 from the month count: subtract 1 from month diff, add the length of the previous month to day diff.
Step 3 is where most implementations go wrong. The “previous month’s length” isn’t a constant 30 — it’s 28, 29, 30, or 31 depending on which month and (for February) which year.
Worked example
Birth: 1990-12-31. Today: 2026-03-15.
- Year diff: 2026 − 1990 = 36.
- Month diff: 3 − 12 = −9. Borrow: year diff drops to 35, month diff becomes 3.
- Day diff: 15 − 31 = −16. Borrow: month diff drops to 2, day diff += previous month’s length. Previous month is February 2026 (28 days, not a leap year). Day diff = −16 + 28 = 12.
Result: 35 years, 2 months, 12 days.
The previous-month-length trick
The cleanest way to ask “how long was the previous month?” in JavaScript:
new Date(year, month, 0).getDate() // Returns the last day of (month - 1). // Day 0 of month N is interpreted as day -0 = last day of N-1.
This handles February leap-year correctness automatically because Date knows. new Date(2024, 2, 0).getDate() returns 29; new Date(2023, 2, 0).getDate() returns 28.
Leap-year birthdays (Feb 29)
People born on February 29 raise a recurring question: what’s their birthday in non-leap years? Two conventions:
- Roll to March 1 — UK, Hong Kong default.
- Roll to February 28 — US, Taiwan, New Zealand default. The calculator uses this.
The choice affects when the “you turned N” moment happens in non-leap years. For most purposes it doesn’t matter — the underlying birth date is still Feb 29 and the difference is one day per non-leap year.
Why “days / 365.25” drifts
365.25 is the averagelength of a year over a 400-year Gregorian cycle. Any individual year is 365 or 366. Over short windows the average isn’t a good approximation:
- 4 years contain exactly 1 leap year → 1461 days → 365.25 exactly. Good.
- 3 years from 1-Mar-2021 → 1095 days. Divided by 365.25 = 2.998… → rounded, 3 years. Correct.
- 3 years from 1-Mar-2020 → 1096 days (includes Feb 29 2024 wait — no, doesn’t include it). Still 1095 → same.
- The drift comes from the century rule: 1900 was not a leap year (divisible by 100, not by 400). 2000 was. Computing ages spanning 1900 or 2100 with the 365.25 approximation produces visible day-level errors.
For age calculation, calendar borrowing is always exact. The naive approximation is fine for back-of-envelope but wrong for anything that has to match a passport or birth certificate.
Algorithm details: borrow-from-month in full
The full reference implementation is short enough to reproduce inline; the surprise is how few branches it needs once the day-0 trick handles the variable-month-length problem.
function age(birth: Date, today: Date) {
let y = today.getFullYear() - birth.getFullYear();
let m = today.getMonth() - birth.getMonth();
let d = today.getDate() - birth.getDate();
if (d < 0) {
// Borrow days from previous month — day 0 returns last day of previous month.
const prevMonthLen = new Date(today.getFullYear(), today.getMonth(), 0).getDate();
d += prevMonthLen;
m -= 1;
}
if (m < 0) {
m += 12;
y -= 1;
}
return { years: y, months: m, days: d };
}The algorithm runs in O(1) time with no division, no floating-point arithmetic, and no calendar libraries. The only spec-driven branch is the leap-year handling — and even that is delegated to Date, which implements the Gregorian rule (every year divisible by 4 except century years not divisible by 400) from ECMA-262.
Comparing against the naive day-count approach for a 100-year span:
| Method | Result for 1925-01-01 → 2025-01-01 | Error |
|---|---|---|
| Borrow algorithm | 100 y, 0 m, 0 d | 0 d (exact) |
| (days) / 365.25 | 99.9999 y → rounds to 99 | ~1 d (silent miscount) |
| (days) / 365 | 100.07 y | ~25 d |
Sources & references
The leap-year rule is the original 1582 Gregorian reform (Inter Gravissimas); the date-format we accept is ISO 8601; the previous-month-length trick is well-defined ECMAScript behaviour from ECMA-262. The Feb-29 birthday rolling conventions are jurisdiction-specific and trace to national legislation — see our age calculation methods guide for the jurisdictional table. UK and Hong Kong roll to March 1 per the 1953 Births and Deaths Registration Act; US, NZ and Taiwan use Feb 28 by administrative convention. See the Sources block below.
Assumptions & limitations
- Gregorian calendar only.Hijri, Hebrew, Chinese lunar, and Julian calendars need different algorithms; the converter doesn’t expose calendar-system selection.
- Date-only, not date-time.Inputs are truncated to midnight in the user’s local timezone. DST transitions don’t affect the result.
- Feb 29 → Feb 28 rolling.We use the US convention by default. Users in UK / HK jurisdictions should add one day to the “you turned N” marker in non-leap years.
- No before-Gregorian-adoption support. Birth dates before 1582-10-15 use the proleptic Gregorian extension — historically these would have been recorded as Julian dates and shifted forward 10-13 days for modern comparison.
- No leap-second handling.Leap seconds don’t affect calendar-day arithmetic; if you need UTC vs TAI precision, use a different tool.
- Future dates return zero, not negative. A negative age is almost always a UX bug rather than a legitimate result.
Edge cases the calculator handles
- Future birth dates — we return zero rather than negative ages. A negative age is a bug surface.
- Today equals birth date — 0 years, 0 months, 0 days.
- Across DST transitions— the calculator uses date-only math (no times), so DST changes don’t affect the result.
Frequently asked questions
- How does Convertitive calculate age in years, months, and days?
- We use a calendar-borrowing algorithm: compute the raw difference in years, months, and days, then borrow from the next-larger unit when a component goes negative. Day borrowing uses the previous month's exact length (28, 29, 30, or 31 days), obtained via JavaScript's Date(year, month, 0).getDate() so February leap-year correctness is handled automatically.
- Why doesn't Convertitive use days / 365.25 to compute age?
- 365.25 is the average year length over the full 400-year Gregorian cycle, but any single year is exactly 365 or 366 days. Using the average produces a ~1-day drift every few years and a visible error when the date range spans a century year like 1900 (not a leap year) or 2100. The borrow algorithm is O(1), has no floating-point arithmetic, and is always exact.
- What happens with February 29 birthdays in non-leap years?
- Convertitive rolls Feb 29 to Feb 28 by default — the US, Taiwan, and New Zealand convention. The UK and Hong Kong convention rolls to Mar 1 (per the 1953 Births and Deaths Registration Act). The two conventions differ by one day in non-leap years; no single convention is universally correct.
- What calendar system does the calculator assume?
- Proleptic Gregorian only — the leap-year rule from the 1582 Inter Gravissimas reform (divisible by 4, except centuries, except 400-year centuries). Hijri, Hebrew, Chinese lunar, and Julian-calendar inputs are not supported. Dates before 1582-10-15 use the proleptic Gregorian extension, which differs from recorded Julian dates by 10–13 days.
- Does a DST transition affect the age result?
- No. The calculator operates on calendar dates, not timestamps. Inputs are treated as midnight-local with no time component, so no daylight-saving transition shifts the day count.
Sources & references
Authoritative references cited by this piece. Verified by Buğra Sözeri on the dates shown and re-checked at every deploy.
- ISO 8601-1:2019 — Date and time — Representations for information interchange — International standard defining the YYYY-MM-DD date format and the Gregorian-calendar arithmetic our algorithm assumes.(as of )
- Inter Gravissimas (1582) — Gregorian calendar reform — The papal bull that defines the leap-year rule (4/100/400) underlying both the borrow algorithm and the 365.25-average correction.(as of )
- ECMA-262 — ECMAScript Date object specification — Specifies the JavaScript Date semantics — including the day-0 trick we use to retrieve previous-month length.(as of )
- IANA Time Zone Database (tzdata) — The canonical historical record of timezone offsets; relevant for visa / immigration date calculations that span timezone changes.(as of )
- UK Births and Deaths Registration Act 1953 — Leap-day birthdays — Legal reference behind the “Feb 29 rolls to Mar 1 in non-leap years” convention used in UK practice.(as of )
Related
Published May 15, 2026 · Last reviewed May 31, 2026