DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

Date Arithmetic Is Harder Than It Looks: Leap Years, Timezones, and Edge Cases

"How many days between two dates" seems trivial until you encounter leap years, daylight saving transitions, timezone boundaries, and the question of whether the end date is inclusive or exclusive. Date arithmetic is one of those domains where the edge cases outnumber the normal cases.

The naive subtraction problem

const start = new Date('2024-03-01');
const end = new Date('2024-04-01');
const days = (end - start) / (1000 * 60 * 60 * 24);
// Expected: 31. Actual: might be 30.958... due to DST
Enter fullscreen mode Exit fullscreen mode

If March 10 falls in this range (DST spring forward in the US), one "day" is only 23 hours. Dividing by 86,400,000 milliseconds (24 hours) gives a non-integer result. Rounding can go either direction depending on the local timezone.

The correct approach for calendar day calculations is to work with dates, not timestamps:

function daysBetween(startStr, endStr) {
  const start = new Date(startStr + 'T00:00:00Z');
  const end = new Date(endStr + 'T00:00:00Z');
  return Math.round((end - start) / 86400000);
}
Enter fullscreen mode Exit fullscreen mode

By using UTC (the Z suffix), you avoid DST issues entirely. Rounding handles any floating-point imprecision.

Month arithmetic is not well-defined

Adding "one month" to January 31 produces... what? February 31 does not exist. Different systems handle this differently:

  • Clamp to end of month: January 31 + 1 month = February 28 (or 29 in leap years)
  • Overflow to next month: January 31 + 1 month = March 3 (31 days after Jan 31)
  • Error: January 31 + 1 month is undefined

JavaScript's Date object uses the overflow approach, which is rarely what you want. new Date(2024, 0, 31) plus one month (setMonth(1)) gives March 2, 2024, not February 29.

Libraries like date-fns and Luxon use the clamping approach, which matches most people's expectations.

Business days vs calendar days

"30 days" and "30 business days" are very different:

  • 30 calendar days from a Monday: 30 days later (4 weeks + 2 days)
  • 30 business days from a Monday: 42 calendar days later (6 weeks)

Business day calculations also need to account for holidays, which are country-specific, state-specific, and sometimes company-specific. There is no universal holiday calendar.

A pragmatic approach is to define a holiday list per locale and calculate business days by iterating through calendar days, counting only weekdays that are not holidays.

Useful date calculations

Days until a date: Subtract today from the target date in UTC.

Age calculation: Not just year subtraction. You need to check if the birthday has occurred yet this year.

function calculateAge(birthDate, today) {
  let age = today.getFullYear() - birthDate.getFullYear();
  const monthDiff = today.getMonth() - birthDate.getMonth();
  if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
    age--;
  }
  return age;
}
Enter fullscreen mode Exit fullscreen mode

Day of week: Given a date, what day is it? JavaScript's getDay() returns 0 (Sunday) through 6 (Saturday).

Week number: ISO 8601 defines week 1 as the week containing the first Thursday of the year. This means January 1 might be in week 52 or 53 of the previous year. Most JavaScript date libraries implement this correctly.

The tool

For quick date calculations -- days between dates, adding/subtracting days/months/years, business day calculations, and age computation -- I built a date calculator that handles the edge cases correctly and shows results in multiple formats.


I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.

Top comments (0)