Date Calculator: From Date Object Pitfalls to Workday Optimization#

I recently needed date calculations for a project. Thought new Date() with some arithmetic would do—turned out to be a minefield of edge cases. Here’s what I learned.

JavaScript Date Pitfalls#

Month Starts at Zero#

The classic trap:

const date = new Date(2024, 0, 15)  // January 15, 2024
const date2 = new Date(2024, 1, 15) // February 15, 2024, NOT January!

Month parameter is 0-11, not 1-12. setMonth() works the same way:

// Wrong: direct month arithmetic
date.setMonth(date.getMonth() + 3)  // If November, becomes February next year

// Right: let Date handle overflow
const result = new Date(date)
result.setMonth(result.getMonth() + 3)  // Date auto-carries over

Timezone Offset Issue#

new Date('2024-01-15') differs from new Date('2024-01-15T00:00:00'):

new Date('2024-01-15')
// In UTC+8, this becomes 2024-01-15 08:00:00 (parsed as UTC)

new Date('2024-01-15T00:00:00')
// This gives 2024-01-15 00:00:00 local time

Always append T00:00:00 to force local time:

const date = new Date(dateString + 'T00:00:00')

Date Difference Calculation#

Basic Implementation#

Days between two dates:

function dateDiff(start: string, end: string) {
  const startDate = new Date(start + 'T00:00:00')
  const endDate = new Date(end + 'T00:00:00')

  const diffMs = endDate.getTime() - startDate.getTime()
  const days = Math.floor(Math.abs(diffMs) / (1000 * 60 * 60 * 24))

  return days
}

For weeks and months, weeks is straightforward (divide by 7), but months are tricky since each month has different days.

Month Estimation#

Months can only be estimated using average 30.44 days:

const months = Math.round((days / 30.44) * 10) / 10  // 1 decimal place

30.44 comes from 365.25 / 12, accounting for leap years. Good enough for most use cases.

Date Addition: Cross-Month and Cross-Year#

Simple Version#

function addDays(date: 2026-02-06T16:33:53+00:00
  const result = new Date(date)
  result.setDate(result.getDate() + days)
  return result
}

The magic: setDate() handles overflow automatically:

const date = new Date(2024, 0, 31)  // January 31
date.setDate(32)  // Automatically becomes February 1

So result.setDate(result.getDate() + 90) works perfectly—Date handles all cross-month/cross-year logic.

Month Addition#

function addMonths(date: 2026-02-06T16:33:53+00:00
  const result = new Date(date)
  result.setMonth(result.getMonth() + months)
  return result
}

Edge case: January 31 + 1 month becomes March 2 or 3 (February has no 31st).

Some business scenarios need “end-of-month to end-of-month” behavior. Extra handling required:

function addMonthsEndOfMonth(date: 2026-02-06T16:33:53+00:00
  const result = new Date(date)
  const isEndOfMonth = date.getDate() === getLastDayOfMonth(date)

  result.setMonth(result.getMonth() + months)

  if (isEndOfMonth) {
    result.setDate(getLastDayOfMonth(result))
  }
  return result
}

function getLastDayOfMonth(date: 2026-02-06T16:33:53+00:00
  return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
}

Age Calculation: Precise to the Day#

Age isn’t just currentYear - birthYear:

function calculateAge(birthDate: Date): { years: number, months: number, days: number } {
  const today = new Date()
  today.setHours(0, 0, 0, 0)  // Ignore time

  let years = today.getFullYear() - birthDate.getFullYear()
  let months = today.getMonth() - birthDate.getMonth()
  let days = today.getDate() - birthDate.getDate()

  // Borrow: if days negative, borrow 1 month
  if (days < 0) {
    months--
    const prevMonth = new Date(today.getFullYear(), today.getMonth(), 0)
    days += prevMonth.getDate()
  }

  // Borrow: if months negative, borrow 1 year
  if (months < 0) {
    years--
    months += 12
  }

  return { years, months, days }
}

Example: Today is 2024-03-15, birthday is 1990-08-20:

  • Year diff: 34 years
  • Month diff: 3 - 8 = -5, after borrowing becomes 7 months
  • Day diff: 15 - 20 = -5, after borrowing becomes 29 - 5 = 24 days (February’s days)

Result: 33 years 7 months 24 days.

Workday Calculation: Excluding Weekends#

Linear Scan#

Most straightforward: iterate each day, check if weekend:

function countWorkdays(start: Date, end: Date): number {
  let count = 0
  const cur = new Date(start)

  while (cur <= end) {
    const day = cur.getDay()  // 0=Sunday, 6=Saturday
    if (day !== 0 && day !== 6) {
      count++
    }
    cur.setDate(cur.getDate() + 1)
  }

  return count
}

Time complexity O(n) where n is the date range. For a few hundred days, performance is fine.

Mathematical Optimization#

For very large ranges (10+ years), use math:

function countWorkdaysOptimized(start: Date, end: Date): number {
  const totalDays = Math.floor((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1
  const fullWeeks = Math.floor(totalDays / 7)
  const remainder = totalDays % 7

  // Each full week has 5 workdays
  let workdays = fullWeeks * 5

  // Handle remaining days
  const cur = new Date(start)
  for (let i = 0; i < remainder; i++) {
    const day = cur.getDay()
    if (day !== 0 && day !== 6) workdays++
    cur.setDate(cur.getDate() + 1)
  }

  return workdays
}

This version runs in O(1) + O(7) ≈ O(1), much faster for huge ranges.

But in practice, linear scan handles 1000 days in milliseconds. Unless you have extreme requirements, simple is fine.

This date calculator is now live: Date Calculator, supporting date difference, date addition/subtraction, age calculation, and workday counting.

Other related tools: