Skip to content

Methodology

Age calculation methodology

Calendar-aware borrowing, not naive division. The 4-day error that naive code accumulates.

By Published 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):

  1. Year diff: tY − bY.
  2. Month diff: tM − bM. If negative, borrow 1 from the year count: subtract 1 from year diff, add 12 to month diff.
  3. 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.

  1. Year diff: 2026 − 1990 = 36.
  2. Month diff: 3 − 12 = −9. Borrow: year diff drops to 35, month diff becomes 3.
  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:

MethodResult for 1925-01-01 → 2025-01-01Error
Borrow algorithm100 y, 0 m, 0 d0 d (exact)
(days) / 365.2599.9999 y → rounds to 99~1 d (silent miscount)
(days) / 365100.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.

Related

Published May 15, 2026 · Last reviewed May 31, 2026