Date arithmetic traps: leap years, month ends, business days, time zones

3 min read

“What’s March 31 plus one month?” or “When is a leap-day baby’s birthday?” — date arithmetic looks simple but hides many edge cases. This article walks through the typical traps.

Month ends: what is “+1 month”?

For “March 31 + 1 month”, possible answers:

  • April 31 (doesn’t exist).
  • April 30 (most intuitive).
  • May 1 (overflow to the next month).

JavaScript’s Date takes the last route:

const d = new Date(2024, 2, 31); // March 31
d.setMonth(d.getMonth() + 1);
// becomes May 1 (April only has 30 days)

To preserve “same day of month if possible”:

function addMonths(date, months) {
	const d = new Date(date);
	const targetMonth = d.getMonth() + months;
	d.setDate(1);
	d.setMonth(targetMonth);
	const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
	d.setDate(Math.min(date.getDate(), lastDay));
	return d;
}

Leap year rules

The exact rules:

  1. Divisible by 4 → leap.
  2. Except divisible by 100 → not leap.
  3. Except divisible by 400 → leap.
function isLeap(year) {
	return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}

Examples:

  • 2000 — leap (400 divides).
  • 1900 — not leap (100 divides, 400 doesn’t).
  • 2024 — leap.
  • 2100 — not leap.

February 29 birthdays

Legal handling varies:

  • Japan: treated as February 28 in non-leap years (under civil code, age increments at 24:00 of the prior day).
  • US/UK: February 28 or March 1 depending on jurisdiction.
  • Taiwan: March 1.

If you compute age, decide a policy for leap-day birthdays in non-leap years.

Date difference: not just subtraction

Days between two dates:

const days = Math.floor((d2 - d1) / (1000 * 60 * 60 * 24));

Works most of the time, but DST transition days are 23 or 25 hours, leaving fractional results.

Compute in UTC, or use a library helper like date-fns’s differenceInCalendarDays.

Business days

“3 business days from now” excluding weekends:

function addBusinessDays(date, days) {
	const d = new Date(date);
	let added = 0;
	while (added < days) {
		d.setDate(d.getDate() + 1);
		const day = d.getDay();
		if (day !== 0 && day !== 6) added++; // skip Sun/Sat
	}
	return d;
}

Holidays need separate data. National holidays differ by country and year — use a library or a maintained dataset.

ISO 8601: the standard format

Recommended date forms:

  • Date: 2026-04-26
  • Datetime: 2026-04-26T12:00:00
  • With offset: 2026-04-26T12:00:00+09:00
  • UTC: 2026-04-26T03:00:00Z

Benefits:

  • Sortable as plain strings.
  • Parseable in every language.
  • Unambiguous.

Avoid 2026/04/26 or 04/26/2026 in APIs and logs.

Week numbers: ISO vs US

Week numbering rules differ:

  • ISO 8601 — week 1 contains January 4; weeks start Monday.
  • US — week 1 contains January 1; weeks start Sunday.
  • Japan — week 1 contains January 1; weeks start Monday.

“What day starts week 1 of 2024?” depends on the rule. Specify which one your API uses.

Time-zone traps

const birthday = new Date('1990-01-01');
// "Mon Jan 01 1990 09:00:00 GMT+0900" in JST
// "Sun Dec 31 1989 16:00:00 GMT-0800" in PST

ISO date strings without time are parsed as UTC midnight; in JST that’s 9 AM and in PST that’s the previous afternoon. For date-only data, construct in local time:

const birthday = new Date(1990, 0, 1); // local Jan 1, 1990

Computing age

function getAge(birthDate, today = new Date()) {
	let age = today.getFullYear() - birthDate.getFullYear();
	const monthDiff = today.getMonth() - birthDate.getMonth();
	if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
		age--;
	}
	return age;
}

You have to check whether this year’s birthday has happened; comparing only by year overstates by one when the birthday is later in the year.

Library options

Practical date handling in JavaScript today:

  • date-fns — light, functional, tree-shakable.
  • dayjs — Moment.js-like API, light.
  • Luxon — full-featured, the modern Moment successor.
  • Temporal — proposed native API, future replacement for much of this.

For Japanese holidays, libraries like @holiday-jp/holiday_jp ship the data.

Summary

  • Month-end addition behaves differently across implementations.
  • Leap year rule: 4 / 100 / 400.
  • Leap-day birthdays vary by jurisdiction.
  • Business-day math needs holiday data.
  • Use ISO 8601 in APIs.

For day differences and “+N days” calculations, the date calculator on this site handles both.