// ── Jours fériés statutaires du QUÉBEC — DÉTERMINISTE (pas d'IA : ce sont des règles fixes) ── // Base = Loi sur les normes du travail (CNESST). 8 fériés. L'employeur peut AJOUTER des observances // (lendemain, congé mobile…) via `extra`, et choisir Vendredi saint OU Lundi de Pâques. // ⚠️ Tout en UTC pour éviter toute dérive de fuseau (les dates de fériés sont des jours civils). // Dimanche de Pâques (algorithme de Meeus/Jones/Butcher). function easterSunday (year) { const a = year % 19, b = Math.floor(year / 100), c = year % 100 const d = Math.floor(b / 4), e = b % 4, f = Math.floor((b + 8) / 25) const g = Math.floor((b - f + 1) / 3), h = (19 * a + b - d - g + 15) % 30 const i = Math.floor(c / 4), k = c % 4, 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) // 3 = mars, 4 = avril const day = ((h + l - 7 * m + 114) % 31) + 1 return new Date(Date.UTC(year, month - 1, day)) } // n-ième `weekday` (0=dim … 1=lun) du mois (month0 0-based). function nthWeekday (year, month0, weekday, n) { const first = new Date(Date.UTC(year, month0, 1)) const day = 1 + ((7 + weekday - first.getUTCDay()) % 7) + (n - 1) * 7 return new Date(Date.UTC(year, month0, day)) } const iso = (d) => d.toISOString().slice(0, 10) // Liste des fériés QC pour une année. easterChoice: 'good_friday' (défaut) | 'easter_monday'. function holidaysForYear (year, { easterChoice = 'good_friday', extra = [] } = {}) { const easter = easterSunday(year) const goodFriday = new Date(easter); goodFriday.setUTCDate(easter.getUTCDate() - 2) const easterMonday = new Date(easter); easterMonday.setUTCDate(easter.getUTCDate() + 1) // Journée nationale des patriotes = le LUNDI qui précède le 25 mai (donc lundi ≤ 24 mai). const patriotes = new Date(Date.UTC(year, 4, 24)); while (patriotes.getUTCDay() !== 1) patriotes.setUTCDate(patriotes.getUTCDate() - 1) const list = [ { date: iso(new Date(Date.UTC(year, 0, 1))), name: "Jour de l'An" }, { date: iso(easterChoice === 'easter_monday' ? easterMonday : goodFriday), name: easterChoice === 'easter_monday' ? 'Lundi de Pâques' : 'Vendredi saint' }, { date: iso(patriotes), name: 'Journée nationale des patriotes' }, { date: iso(new Date(Date.UTC(year, 5, 24))), name: 'Saint-Jean-Baptiste' }, { date: iso(new Date(Date.UTC(year, 6, 1))), name: 'Fête du Canada' }, { date: iso(nthWeekday(year, 8, 1, 1)), name: 'Fête du travail' }, // 1er lundi de septembre { date: iso(nthWeekday(year, 9, 1, 2)), name: 'Action de grâce' }, // 2e lundi d'octobre { date: iso(new Date(Date.UTC(year, 11, 25))), name: 'Noël' }, ] for (const x of (extra || [])) if (x && x.date) list.push({ date: x.date, name: x.name || 'Congé', custom: true }) return list.sort((p, q) => p.date.localeCompare(q.date)) } // Fériés entre deux dates ISO (incluses), traversant les années. function holidaysBetween (fromIso, toIso, opts = {}) { const y0 = parseInt(String(fromIso).slice(0, 4), 10); const y1 = parseInt(String(toIso).slice(0, 4), 10) const out = [] for (let y = y0; y <= y1; y++) for (const h of holidaysForYear(y, opts)) if (h.date >= fromIso && h.date <= toIso) out.push(h) return out } function isHoliday (dateIso, opts = {}) { const y = parseInt(String(dateIso).slice(0, 4), 10) return holidaysForYear(y, opts).find(h => h.date === dateIso) || null } module.exports = { holidaysForYear, holidaysBetween, isHoliday, easterSunday }