DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

Generating Calendars Programmatically: Harder Than You Think

Every developer thinks building a calendar is a weekend project. You render a 7-column grid, fill in the days, done. Then you discover that February sometimes has 29 days, that months start on different weekdays, that some locales start the week on Monday, that timezone offsets can make the same instant fall on two different dates, and that the JavaScript Date object will silently accept February 30th and return March 2nd instead of throwing an error.

I've built more calendar components than I'd like to admit, and every one of them taught me something I didn't expect about date handling.

The basic algorithm

The core problem is: given a month and year, generate a grid showing which day of the week each date falls on. Here's the logic:

  1. Find the first day of the month (what weekday it starts on).
  2. Find the total number of days in the month.
  3. Fill a 6x7 grid (6 weeks, 7 days) with the appropriate numbers, including trailing days from the previous month and leading days of the next month.
function getCalendarGrid(year, month) {
  // month is 0-indexed (0 = January)
  const firstDay = new Date(year, month, 1).getDay();
  const daysInMonth = new Date(year, month + 1, 0).getDate();
  const daysInPrevMonth = new Date(year, month, 0).getDate();

  const grid = [];
  let day = 1;
  let nextMonthDay = 1;

  for (let week = 0; week < 6; week++) {
    const row = [];
    for (let dow = 0; dow < 7; dow++) {
      const cellIndex = week * 7 + dow;
      if (cellIndex < firstDay) {
        // Previous month's trailing days
        row.push({
          day: daysInPrevMonth - firstDay + dow + 1,
          currentMonth: false
        });
      } else if (day > daysInMonth) {
        // Next month's leading days
        row.push({ day: nextMonthDay++, currentMonth: false });
      } else {
        row.push({ day: day++, currentMonth: true });
      }
    }
    grid.push(row);
  }
  return grid;
}
Enter fullscreen mode Exit fullscreen mode

The new Date(year, month + 1, 0) trick is one of the most useful date idioms in JavaScript. Day 0 of the next month gives you the last day of the current month, which tells you the total days. It correctly handles February in leap years, 30-day months, and 31-day months.

The leap year rules

Most developers know that leap years are divisible by 4. Fewer know the full rule:

  1. Divisible by 4: leap year
  2. Divisible by 100: NOT a leap year
  3. Divisible by 400: leap year again

So 2024 is a leap year (divisible by 4). 2100 will NOT be a leap year (divisible by 100 but not 400). 2000 WAS a leap year (divisible by 400).

function isLeapYear(year) {
  return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
Enter fullscreen mode Exit fullscreen mode

This rule exists because the solar year is approximately 365.2422 days. The leap year adjustment at the 4-year level overshoots by about 0.0078 days per year. The 100-year correction pulls it back. The 400-year correction fine-tunes it. The result is an average calendar year of 365.2425 days, which is accurate to within one day per 3,236 years.

Monday-start vs. Sunday-start

In the United States, calendars start on Sunday. In most of Europe and much of the rest of the world, calendars start on Monday. The ISO 8601 standard defines Monday as the first day of the week.

JavaScript's Date.getDay() returns 0 for Sunday through 6 for Saturday. To convert to a Monday-start calendar, shift the value: (getDay() + 6) % 7 gives you 0 for Monday through 6 for Sunday.

This seems trivial until you realize it affects how many rows your calendar grid needs. A month that starts on Sunday in a Sunday-start calendar needs 5 rows. The same month in a Monday-start calendar might need 6 rows because the first day shifts to the second row.

ISO week numbers

Week numbering sounds simple until you learn that ISO 8601 says the first week of the year is the week containing the first Thursday of January. This means January 1 can fall in week 52 or 53 of the previous year. And December 31 can fall in week 1 of the next year.

Calculating the ISO week number:

function getISOWeek(date) {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
}
Enter fullscreen mode Exit fullscreen mode

If you're building a calendar for a European audience or for project planning tools, week numbers are expected. Omitting them feels like a missing feature.

Four common calendar mistakes

  1. Assuming all months need 5 rows. February 2026 starts on a Sunday (in a Sunday-start calendar), so it fits perfectly in 4 rows. But a month that starts on Friday or Saturday with 31 days needs 6 rows. Always allocate for 6 rows or dynamically calculate the row count.

  2. Using local time for date calculations. If you construct new Date(2026, 2, 15) at 11pm in a timezone that's UTC-5, you get the right date. But if you receive a UTC timestamp and convert it to local time without thinking about the offset, you might display a date one day off. Always be explicit about whether you're working in local time or UTC.

  3. Hardcoding month lengths. Don't use an array of [31, 28, 31, 30, ...] and then patch February for leap years. Use the Date constructor to calculate the correct value. The language already handles the edge cases.

  4. Forgetting about the Intl API. Intl.DateTimeFormat can give you localized month names, day names, and formatting without any library. Use it instead of maintaining your own translation tables.

const monthName = new Intl.DateTimeFormat('en-US', { month: 'long' }).format(new Date(2026, 2));
// "March"

const dayNames = [...Array(7)].map((_, i) =>
  new Intl.DateTimeFormat('en-US', { weekday: 'short' })
    .format(new Date(2026, 0, i + 4)) // Jan 4, 2026 is a Sunday
);
// ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Enter fullscreen mode Exit fullscreen mode

Generating printable calendars

For quickly generating monthly or yearly calendar layouts that handle all the edge cases -- leap years, week start preferences, and proper grid sizing -- I built a calendar generator at zovo.one/free-tools/calendar-generator.

Dates are one of those domains where the edge cases outnumber the common cases. Every developer learns this the hard way, usually at 11pm when a timezone bug shows tomorrow's date to half their users. Understanding the calendar grid algorithm at least removes one layer of surprise.


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

Top comments (0)