Skip to content

Methodology

Date & time methodology

Where naive date math fails — and how each /datetime/ tool sidesteps it.

Date and time arithmetic is one of the most-attempted, most- bug-ridden categories of code in any large codebase. The four tools under our Date & time cluster each tackle a different sub-problem; this page explains the math behind each.

Age calculator — calendar-aware borrowing

Age in years-months-days isn’t just “today minus birthday divided by 365.25” — that approximation accumulates a day of error every 4-7 years. The correct algorithm uses calendar borrowing:

  1. Compute year difference: now.year − birth.year.
  2. Compute month difference. If now.month < birth.month, borrow 12 months from the year count.
  3. Compute day difference. If now.day < birth.day, borrow days from the month count using the actual length of the previous calendar month.

Step 3 is where naive code fails. “Previous month length” isn’t 30 or 31 — it depends on which previous month (February is 28 or 29, the rest are 30 or 31). Our age calculator uses new Date(year, month, 0) (which returns the last day of the previous month) to get the right number every time, including leap years.

Worked example

Birth: 1990-12-31. Today: 2026-05-14. Year diff: 36. Month diff: −7 (May minus December). Borrow a year, month diff becomes 5. Day diff: 14 − 31 = −17. Borrow a month; previous month is April, which has 30 days, so add 30, giving day diff of 13. Result: 35 years, 4 months, 13 days.

Date difference — three units, one denominator

The date difference tool reports the gap between two dates in days, weeks, and hours simultaneously. The math is straightforward:

days = (b.getTime() − a.getTime()) / 86_400_000
weeks = days / 7 · hours = days × 24

The subtlety is timezone handling. Both dates are normalised to UTC midnight before subtraction, so DST transitions don’t introduce fractional days. A date difference spanning a spring-forward is still an integer number of days, as users expect.

Working days — Monday through Friday between two dates

The working days counter uses the simplest possible algorithm: walk each calendar day between the two dates and increment a counter when the weekday is Monday-Friday.

At ~250 weekdays per year and Date object construction averaging ~1µs in modern V8, this completes in microseconds for any date range under a century. We don’t use a closed-form formula because it’s only marginally faster and significantly harder to read.

What’s not included: national holidays. Holiday observance varies by country, by year (most are date-fixed but a meaningful subset move with the lunar calendar or are defined as “nth weekday of the month”), and by industry. We’d need to pull from a curated dataset to do this responsibly; the working-days tool stays weekday-only on purpose.

Timezone converter — IANA tzdata + Intl.DateTimeFormat

Time zones are political constructs, not geographic ones, and their rules change. Daylight saving start dates, year- round-DST experiments, splits and merges as countries reorganise — all of this is captured in the IANA tzdata bundle that ships with every modern operating system.

Our timezone converter reaches that data through the browser’s built-in Intl.DateTimeFormat. We never bundle our own tzdata. This matters because:

  • tzdata updates several times a year as countries change rules.
  • Bundling our own copy would freeze that data at build time.
  • The OS-level tzdata is updated by the OS vendor on a security cadence faster than ours.

The wall-clock conversion algorithm

Given “14:00 on 2026-07-04 in Europe/Istanbul, what is it in America/Los_Angeles?”:

  1. Treat “14:00 on 2026-07-04” as a UTC timestamp (call it guess).
  2. Ask Intl.DateTimeFormat: at guess, what offset does Europe/Istanbul have? (UTC+3.)
  3. Adjust: the actual UTC instant is guess − 3 hours.
  4. Render that UTC instant on the Los_Angeles wall clock.

This handles half-hour zones (India, UTC+5:30) and 45-minute zones (Nepal, UTC+5:45) without special-casing. The only case that breaks is the “spring forward” gap — 2:30 AM doesn’t exist in a zone that jumps from 2:00 to 3:00 — but the calculator returns the off-by-an-hour result rather than failing, which is still useful information.

The ISO 8601 default

All date inputs in the UI use ISO 8601 format (YYYY-MM-DD). All Dateobjects produced internally are timezone-naive at the boundary and only become timezone-aware when crossing into the timezone converter. The Intl.DateTimeFormat output is locale-aware in the user’s browser language; the underlying data is always ISO 8601 for storage and serialisation.

Frequently asked questions

Why not use a date library like dayjs or date-fns?
We do, internally, for the parts that benefit from one — but every visible function in /lib/datetime/ is written from primitives. Date libraries trade size for convenience, and our math is simple enough (and stable enough) that the dependency wasn't worth it.
Does the timezone converter handle historical dates?
Within IANA tzdata's reach, yes. IANA contains DST transition rules going back to roughly 1970 for most zones, and earlier for a subset. For dates pre-1970 the offset returned is the zone's modern offset, which may be historically inaccurate. Don't use the timezone converter for genealogy research.

Related

Published May 14, 2026