Date arithmetic traps: leap years, month ends, business days, time zones
“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:
- Divisible by 4 → leap.
- Except divisible by 100 → not leap.
- 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.