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.
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:
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
- Calcolo Pasqua (calcolo pasqua): keep the algorithm, return normalized dates
- festività italiane: correct, timezone-safe implementation (code)
- Common JavaScript pitfalls and tests to reproduce/fix the bug
- Libraries and references to avoid reinventing the wheel
- Sources {#sources}
- Conclusion {#conclusion}
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()vsd.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:
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.
// 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
toYMDLocalfor 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())
- Bad:
- Are you serializing with
toISOString()but later comparing with local-built strings? Tryconsole.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)orDate.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.jsvsTZ='America/New_York' node test.jsand 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
- The date-holidays npm package supplies holiday data per country/region and handles timezone/observance rules.
- For calendar integrations and community examples see the Stack Overflow Q&A about calculating holidays in JavaScript: Calculate holidays with JavaScript and calculate holidays in Javascript.
- There’s a moment.js plugin tailored for Italian business days: moment-business-days-it.
- Community contributions (examples, opening_hours PRs) can help for region-specific rules: PR adding Italian public holidays to opening_hours.js.
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
- https://www.npmjs.com/package/date-holidays
- https://stackoverflow.com/questions/79851711/calculate-holidays-with-javascript
- https://github.com/opening-hours/opening_hours.js/pull/74
- https://github.com/elrancid/moment-business-days-it
- https://stackoverflow.com/questions/32342753/calculate-holidays-in-javascript
- https://github.com/topics/holiday-calculation
- https://www.timeanddate.com/holidays/italy/
- https://github.com/topics/holidays?l=javascript
- https://pypi.org/project/italian-holidays/
- https://stackoverflow.com/questions/26361913/add-italian-holidays-into-fullcalendar
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.