Skip to main content

Birthday maths

· 11 min read
Filip Tammergård
Software Engineer at Frilans Finans

Have you ever wondered how old you are…in milliseconds? Or exactly how many weeks there are between two moments in time? Don't worry, in this post you can get the answer to all your "How many [unit] is it between [moment] and [moment]?" questions!

TL;DR

  • Milliseconds, seconds, minutes, hours, days and weeks between two moments are trivial to compute — just divide the millisecond difference by the right constant.
  • Months and years are trickier because their length varies. The trick is to count full periods forward from the start date and add the fraction of the next period we've reached — the same anchor-based model the Temporal API uses.
  • If you just want the answer, call Temporal.Duration.prototype.total directly instead of writing the math yourself.

Try the model here:

For example your birthday

For example today's date
Years

31.841324544077

Months

382.102781952932

Weeks

1661.440494084

Days

11630.083458588

Hours

279122.003006

Minutes

16747320.180367

Seconds

1004839210.822

Milliseconds

1004839210822

Usage

Pick a start date and an end date above and get answers to everything you have ever wondered about! Before you change anything, the start date is my birthday and the end date is today's date. As you can see, the values keep updating, and they will as long as the end date is today's date. If you pick a different end date, the updates stop.

When you pick the start date you can also enter a time of day by ticking "Include time". That way you can specify your birthday with a precise time of day to find out exactly how many minutes old you are. When "Include time" is unticked, the time defaults to 00:00:00.000, and when it's ticked, hours and minutes are taken from the picker while seconds and milliseconds default to 0. The seconds and milliseconds in the result are especially interesting because they tick in real time while the end date is today's date — you can really see time pass.

The Date object

In JavaScript, the built-in Date object holds a lot of useful information and is therefore a great fit for this kind of calculation. With new Date() a new Date object is created with information about the current millisecond, which can be extracted with a number of functions belonging to the Date object. Here are a few examples for the date 2020-10-12:

const now = new Date()

const currentYear = now.getFullYear()
// currentYear = 2020

const currentMonth = now.getMonth()
// currentMonth = 9

const currentDate = now.getDate()
// currentDate = 12

const currentTime = now.getTime()
// currentTime = 1602486314645

Two of these that might look strange are getMonth and getTime. Even though it is 2020-10-12, currentMonth is 9. That's because months are counted from 0. So January is 0, all the way to December which is month 11. You might assume getTime returns a time of day like "09:05", but instead it returns something as odd as the number of milliseconds since midnight 1970-01-01. That is JavaScript's way of expressing an exact moment in time. Any time before 1970-01-01 is therefore represented as a negative number when using getTime.

The calculations

The trivial part: every unit with a fixed length can be computed by dividing the millisecond difference by the right constant.

const MS_PER_SECOND = 1000
const MS_PER_MINUTE = MS_PER_SECOND * 60
const MS_PER_HOUR = MS_PER_MINUTE * 60
const MS_PER_DAY = MS_PER_HOUR * 24
const MS_PER_WEEK = MS_PER_DAY * 7

function getMillisecondsDiff(startDate, endDate) {
return endDate.getTime() - startDate.getTime()
}

function getSecondsDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_SECOND
}

function getMinutesDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_MINUTE
}

function getHoursDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_HOUR
}

function getDaysDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_DAY
}

function getWeeksDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_WEEK
}

All these functions follow the same pattern — just swap the constant in the denominator. Thanks to the millisecond resolution we get "live" decimals that tick upwards in real time for free.

Computing exactly how many months or years it is between two Date objects is going to turn out to be a lot harder.

Computing months between moments

Before we get into computing in code, we have to consider how one month is defined. Since the length of a month varies — unlike days per week or minutes per hour — it isn't obvious how many months 10 days is. Should the average length of a month be used? Probably not — both because it's awkward to compute given the complexity around leap years, and because it would lead to oddities like February 1–15 being counted as less than a month even though more than half of the current month has passed.

Instead, we'll use an anchor-based model — the same one the Temporal API uses:

  1. Count how many whole months fit when we step forward from the start date, month by month.
  2. Place an anchor at start + that many months.
  3. The fractional part is how far we've moved from the anchor toward the next anchor (one more month), divided by the length of that period.

Let me explain with a few examples.

Example 1

Start date: 2018-01-01 at 00:00
End date: 2020-02-01 at 00:00

All of 2018 and 2019 give 24 months, and January 2020 gives one more — that's 25 whole months. The end date lands exactly on the next anchor (2020-02-01), so the fraction is 0. Total: 25 months.

Example 2

Start date: 2018-04-01 at 00:00
End date: 2020-02-01 at 00:00

From April 2018, 22 whole months fit up to February 2020. Once again, the end date lands exactly on the anchor, so the fraction is 0. Total: 22 months.

Example 3

Start date: 2018-04-24 at 00:00
End date: 2020-02-06 at 00:00

From 2018-04-24 we can step forward month by month: 2018-05-24, 2018-06-24, …, all the way to 2020-01-24. The next anchor would be 2020-02-24, but that's after the end date, so we stop there. That gives us 21 whole months, with the anchor on 2020-01-24.

The fractional part is how far we've moved from the anchor (2020-01-24) toward the next anchor (2020-02-24). The end date 2020-02-06 is 13 days after 2020-01-24, and the full period 2020-01-24 → 2020-02-24 is 31 days. So 13/31 ≈ 0.4193548 of a month. Total: 21.4193548 months.

Adding months is trickier than it sounds

To compute the anchor we need a function that adds n months to a date. Trickier than it sounds, because months have different lengths:

  • 2018-01-31 + 1 month = ? February doesn't have 31 days. The standard answer is to clamp to the last day of the target month: 2018-02-28.
  • 2020-01-31 + 1 month = 2020-02-29 (leap year!).
  • 2020-02-29 + 12 months = 2021-02-28 (clamping again).

JavaScript's built-in setMonth also has a sneaky quirk: new Date(2018, 0, 31) followed by setMonth(1) gives 2018-03-03, not 2018-02-28, because JavaScript "rolls over" the days instead of clamping. We can work around it by first setting the day to 1, then changing the month, and finally setting the day again with clamping:

function addMonths(date, n) {
const result = new Date(date)
result.setDate(1) // Avoid day-overflow when changing month
result.setMonth(date.getMonth() + n)
const daysInResultMonth = new Date(
result.getFullYear(),
result.getMonth() + 1,
0,
).getDate()
result.setDate(Math.min(date.getDate(), daysInResultMonth))
return result
}

The second-to-last line uses the day-0 trick: new Date(year, month + 1, 0) looks like "day 0 of next month", but JavaScript normalizes it to the last day of the current month. A neat way to get the days in a month without thinking about leap years yourself.

The model in code

With addMonths in place, getMonthsDiff looks like this:

function getMonthsDiff(startDate, endDate) {
let months =
(endDate.getFullYear() - startDate.getFullYear()) * 12 +
(endDate.getMonth() - startDate.getMonth())
if (addMonths(startDate, months) > endDate) months -= 1

const anchor = addMonths(startDate, months)
const nextAnchor = addMonths(startDate, months + 1)
const fractional =
(endDate.getTime() - anchor.getTime()) /
(nextAnchor.getTime() - anchor.getTime())
return months + fractional
}

The first expression computes the difference in calendar months between end and start, which is an upper bound on the number of whole months. If we've overshot — i.e., if the anchor at that many months lands after the end date (as in Example 3, where 2020-02-24 is after 2020-02-06) — we step back by one. Then we divide the distance from the anchor to the end date by the length of the next full month period.

We can now test getMonthsDiff with Example 3:

const startDate = new Date("2018-04-24 00:00")
const endDate = new Date("2020-02-06 00:00")
const monthsDiff = getMonthsDiff(startDate, endDate)

// monthsDiff = 21.419354838709676

That matches the sanity check above (21 + 13/31).

Computing years between moments

Years use the exact same anchor model — they're really just twelve months at a time:

function getYearsDiff(startDate, endDate) {
let years = endDate.getFullYear() - startDate.getFullYear()
if (addMonths(startDate, years * 12) > endDate) years -= 1

const anchor = addMonths(startDate, years * 12)
const nextAnchor = addMonths(startDate, (years + 1) * 12)
const fractional =
(endDate.getTime() - anchor.getTime()) /
(nextAnchor.getTime() - anchor.getTime())
return years + fractional
}

Testing with a simple case gives what we'd expect:

const startDate = new Date("2018-01-01")
const endDate = new Date("2020-01-01")
const yearsDiff = getYearsDiff(startDate, endDate)

// yearsDiff = 2

And with something more complicated:

const startDate = new Date("2018-05-03")
const endDate = new Date("2020-03-22")
const yearsDiff = getYearsDiff(startDate, endDate)

// yearsDiff = 1.8852459016393444

Sanity check: 1 whole year fits from 2018-05-03 to 2019-05-03. The next anchor (2020-05-03) lands after the end date, so we stop. The fraction is (2020-03-22 − 2019-05-03) / (2020-05-03 − 2019-05-03) = 324 / 366 = 0.8852459. Total: 1.8852459 years.

Putting it all together

The full implementation looks like this:

const MS_PER_SECOND = 1000
const MS_PER_MINUTE = MS_PER_SECOND * 60
const MS_PER_HOUR = MS_PER_MINUTE * 60
const MS_PER_DAY = MS_PER_HOUR * 24
const MS_PER_WEEK = MS_PER_DAY * 7

function getMillisecondsDiff(startDate, endDate) {
return endDate.getTime() - startDate.getTime()
}

function getSecondsDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_SECOND
}

function getMinutesDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_MINUTE
}

function getHoursDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_HOUR
}

function getDaysDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_DAY
}

function getWeeksDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_WEEK
}

function addMonths(date, n) {
const result = new Date(date)
result.setDate(1)
result.setMonth(date.getMonth() + n)
const daysInResultMonth = new Date(
result.getFullYear(),
result.getMonth() + 1,
0,
).getDate()
result.setDate(Math.min(date.getDate(), daysInResultMonth))
return result
}

function getMonthsDiff(startDate, endDate) {
let months =
(endDate.getFullYear() - startDate.getFullYear()) * 12 +
(endDate.getMonth() - startDate.getMonth())
if (addMonths(startDate, months) > endDate) months -= 1
const anchor = addMonths(startDate, months)
const nextAnchor = addMonths(startDate, months + 1)
const fractional =
(endDate.getTime() - anchor.getTime()) /
(nextAnchor.getTime() - anchor.getTime())
return months + fractional
}

function getYearsDiff(startDate, endDate) {
let years = endDate.getFullYear() - startDate.getFullYear()
if (addMonths(startDate, years * 12) > endDate) years -= 1
const anchor = addMonths(startDate, years * 12)
const nextAnchor = addMonths(startDate, (years + 1) * 12)
const fractional =
(endDate.getTime() - anchor.getTime()) /
(nextAnchor.getTime() - anchor.getTime())
return years + fractional
}

Better alternatives

The model above is essentially a hand-rolled version of Temporal's Duration.prototype.total. In new code I'd just use the Temporal API directly:

const start = Temporal.PlainDateTime.from("1994-08-06T10:00")
const end = Temporal.Now.plainDateTimeISO()

// Decimals in a specific unit
const years = end.since(start).total({ unit: "year", relativeTo: start })
const months = end.since(start).total({ unit: "month", relativeTo: start })
const weeks = end.since(start).total({ unit: "week" })

// Or as an integer breakdown: { years, months, days, hours, ... }
const duration = end.since(start, { largestUnit: "year" })

Temporal is a recent addition, so support still varies across environments. Where it isn't available, the @js-temporal/polyfill polyfill works.

An established library like date-fns also has functions like differenceInYears, differenceInMonths and differenceInWeeks, but they only return integers — for decimals you'd have to combine multiple calls or compute on milliseconds yourself.

So the manual implementation in this article is essentially obsolete in practice — but hopefully a useful exercise in understanding why computing the months or years between two moments isn't always trivial.

That's it! I can brag about being 1661.440494112 weeks old, and you can see for yourself how old you are — exactly.

References