Programming

Fix Italian Holiday Calculation: December 28 vs 31 Bug

Learn how to fix JavaScript errors in Italian holiday calculation that incorrectly marks December 28th instead of December 31st. Includes timezone-safe code examples.

1 answer 1 view

How can I fix my JavaScript function for calculating Italian public holidays that incorrectly marks December 28th as a holiday instead of December 31st? The function uses the following algorithm to calculate holidays including Easter and other fixed dates:

javascript
function calcolaPasqua(anno){
  const a = anno % 19;
  const b = Math.floor(anno / 100);
  const c = anno % 100;
  const d = Math.floor(b / 4);
  const e = b % 4;
  const f = Math.floor((b + 8) / 25);
  const g = Math.floor((b - f + 1) / 3);
  const h = (19 * a + b - d - g + 15) % 30;
  const i = Math.floor(c / 4);
  const k = c % 4;
  const l = (32 + 2 * e + 2 * i - h - k) % 7;
  const m = Math.floor((a + 11 * h + 22 * l) / 451);
  const month = Math.floor((h + l - 7 * m + 114) / 31) - 1;
  const day = ((h + l - 7 * m + 114) % 31) + 1;
  return new Date(anno, month, day);
}

function festivitaItaliane(anno){
  const p = calcolaPasqua(anno);
  const pp = new Date(p); 
  pp.setDate(p.getDate()+1);
  
  const arr = [
    new Date(anno,0,1),
    new Date(anno,0,6),
    new Date(anno,3,25),
    new Date(anno,4,1),
    new Date(anno,5,2),
    new Date(anno,7,15),
    new Date(anno,10,1),
    new Date(anno,11,8),
    new Date(anno,11,25),
    new Date(anno,11,26),
    new Date(anno,11,31),
    p, pp
  ];
  return arr.map(d => new Date(d.getFullYear(), d.getMonth(), d.getDate()));
}

What could be causing the incorrect date assignment for December holidays, and how should I properly implement the Italian holiday calculation algorithm?

The incorrect December date most often comes from a JavaScript Date / timezone or getter mix-up (for example mixing getDay/getUTCDate with getDate, or constructing/parsing dates in different timezones), not from the Easter algorithm itself. Fix by returning and comparing date‑only values (use Date.UTC or normalize to midnight), use the correct getters (getDate/getMonth/getFullYear), and only include December 31 if you actually need it (note: Dec 31 is not a national Italian public holiday). Below I show a safe calcolo pasqua + festività italiane implementation, common pitfalls and tests.

Contents

Italian holidays — likely causes for the Dec 28 vs Dec 31 bug

What you see (Dec 28 marked instead of Dec 31) usually comes from one or more of these issues:

  • Mixing UTC/local methods: creating dates with local constructors (new Date(year, month, day)) but later serializing or comparing using UTC getters (getUTCDate/getUTCFullYear) or toISOString() can shift the visible day near year/month boundaries.
  • Using the wrong getters: getDay() returns weekday (0–6). Passing that into a Date constructor as the day-of-month will produce wrong dates. Example: d.getDay() vs d.getDate().
  • Parsing/formatting strings inconsistently: new Date('31/12/2025') is implementation-dependent. Use ISO or explicit constructors.
  • Not normalizing time-of-day: if a Date has time != 00:00 and you compare by timestamp across timezones you can get the previous/next date.
  • Wrong month index (off-by-one): JS months are 0–11; passing 12 will move the date into January of the next year. That won’t produce a 3‑day shift, but it’s a common source of incorrect month/year.
  • Business logic that falls back to marking Sundays/closest non-working day: some displays mark Sundays (e.g., Dec 28) as closed and will show those instead if a fixed date was accidentally omitted.

Check the national holiday list before treating Dec 31 as a holiday — the official Italian national holidays are the fixed dates and Easter/Pasquetta; New Year’s Eve (31 Dec) is not a national public holiday according to national lists (see the Italy holidays page for the official set).

Useful reference: the Italy holiday list (dates and local time use) is available on timeanddate.com.

Calcolo Pasqua (calcolo pasqua): keep the algorithm, return normalized dates

Your calcolaPasqua algorithm (Anonymous Gregorian algorithm) is correct in structure. The key change: return a date normalized to UTC (or consistently local midnight) so comparisons won’t shift when you later use toISOString()/getUTC* methods. Example: return using Date.UTC to avoid local-time drift:

javascript
function calcolaPasqua(anno){
  const a = anno % 19;
  const b = Math.floor(anno / 100);
  const c = anno % 100;
  const d = Math.floor(b / 4);
  const e = b % 4;
  const f = Math.floor((b + 8) / 25);
  const g = Math.floor((b - f + 1) / 3);
  const h = (19 * a + b - d - g + 15) % 30;
  const i = Math.floor(c / 4);
  const k = c % 4;
  const l = (32 + 2 * e + 2 * i - h - k) % 7;
  const m = Math.floor((a + 11 * h + 22 * l) / 451);
  const month = Math.floor((h + l - 7 * m + 114) / 31) - 1; // 0-based month
  const day = ((h + l - 7 * m + 114) % 31) + 1;
  // Return a UTC date at 00:00:00Z to avoid timezone shift when serializing
  return new Date(Date.UTC(anno, month, day));
}

Returning a UTC-normalized Date means later toISOString().slice(0,10) yields the correct YYYY-MM-DD independent of the machine’s timezone.

festività italiane: correct, timezone-safe implementation (code)

Below is a robust implementation pattern that: (1) computes Easter, (2) builds fixed-date holidays, (3) normalizes everything to ISO YYYY-MM-DD in UTC, and (4) exposes a fast lookup. Note: Dec 31 is optional — include it only if your business requires it.

javascript
// helpers
function dateUTC(anno, month0, day) {
  return new Date(Date.UTC(anno, month0, day)); // month0: 0..11
}
function isoYMDFromUTC(d) { return d.toISOString().slice(0,10); }

// calcolaPasqua from above (returns UTC date)
function calcolaPasqua(anno){ /* ... use code from previous section ... */ }

// main builder
function festivitaItaliane(anno, includeDec31 = false){
  const p = calcolaPasqua(anno); // UTC
  const pasquetta = dateUTC(p.getUTCFullYear(), p.getUTCMonth(), p.getUTCDate() + 1);

  const fixed = [
    dateUTC(anno, 0, 1),   // 01/01 Capodanno
    dateUTC(anno, 0, 6),   // 06/01 Epifania
    dateUTC(anno, 3, 25),  // 25/04 Liberazione
    dateUTC(anno, 4, 1),   // 01/05 Festa del lavoro
    dateUTC(anno, 5, 2),   // 02/06 Festa della Repubblica
    dateUTC(anno, 7, 15),  // 15/08 Assunzione
    dateUTC(anno,10, 1),   // 01/11 Tutti i Santi
    dateUTC(anno,11, 8),   // 08/12 Immacolata
    dateUTC(anno,11,25),   // 25/12 Natale
    dateUTC(anno,11,26)    // 26/12 Santo Stefano
  ];
  if (includeDec31) fixed.push(dateUTC(anno, 11, 31)); // optional; not a national holiday

  const all = fixed.concat([p, pasquetta]);
  // return sorted, unique list of ISO date strings (YYYY-MM-DD)
  return Array.from(new Set(all.map(isoYMDFromUTC))).sort();
}

// small helper to compare a local Date to the holiday set
function toYMDLocal(d){
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2,'0');
  const day = String(d.getDate()).padStart(2,'0');
  return `${y}-${m}-${day}`;
}

function isHolidayLocal(dateObj, holidaysArray){
  const set = new Set(holidaysArray);
  return set.has(toYMDLocal(dateObj));
}

// Example:
const holidays2025 = festivitaItaliane(2025, /* includeDec31? */ true);
console.log(holidays2025.includes('2025-12-31')); // true if you set includeDec31 = true
console.log(holidays2025.includes('2025-12-28')); // should be false

Why this works:

  • All holiday creation uses Date.UTC so the internal UTC date is stable.
  • The holiday set is stored as normalized YYYY-MM-DD strings so comparisons are timezone-independent.
  • Using toYMDLocal for user-supplied Date objects normalizes the local display date for comparison against the UTC-normalized set.

If you prefer working entirely in local dates, you can normalize both sides to local YMD strings (but keep normalization consistent: either both UTC or both local).

Common JavaScript pitfalls and tests to reproduce/fix the bug

What to check in your original code:

  • Did you accidentally call getDay() when rebuilding dates? That returns 0…6 (weekday).
    • Bad: new Date(d.getFullYear(), d.getMonth(), d.getDay())
    • Good: new Date(d.getFullYear(), d.getMonth(), d.getDate())
  • Are you serializing with toISOString() but later comparing with local-built strings? Try console.log(date, date.toISOString()) for a late-December date on your machine to see whether the day component changes.
  • Are you parsing non-ISO strings anywhere? Replace with new Date(year, month0, day) or Date.UTC.
  • If your UI code highlights Sundays by default, confirm that a missing fixed date (e.g., you forgot to include Dec 31 intentionally) didn’t cause the UI to show the nearest Sunday (Dec 28).
  • Reproduce across timezones: in Node you can TZ='Europe/Rome' node test.js vs TZ='America/New_York' node test.js and confirm the holiday set is stable.

Quick tests:

  • For a given year, print both the Date object and toISOString:
    • const d = new Date(2025,11,31); console.log(d, d.toISOString())
  • Use the functions above to confirm '2025-12-31' is present only when you specifically included it.

If you want an out-of-the-box library (less chance to implement subtle bugs), try the packages below.

Libraries and references to avoid reinventing the wheel

If you have region-specific or company-specific dates (crazy italian holidays 2025 or one-off closures), add them to the fixed list or to a per-region dataset (the libraries above let you add custom holidays).

Sources

Conclusion

To fix the December 28 vs December 31 issue, normalize all holiday dates (use Date.UTC or normalize to midnight), avoid mixing UTC and local getters, use getDate (not getDay) when rebuilding dates, and verify whether Dec 31 should be in your festività italiane list at all (it’s not a national holiday). If you prefer not to handle edge cases yourself, use a battle-tested package like date-holidays.

Authors
Verified by moderation
Moderation
Fix Italian Holiday Calculation: December 28 vs 31 Bug