UNPKG

js-public-holidays-za

Version:

Get South African public holidays for any date range, including observed holidays.

153 lines (135 loc) 5.41 kB
// ----------------------------------------------- // South Africa Public Holidays (ESM Module) // ----------------------------------------------- /** * Get all South African public holidays between start and end (inclusive). * @param {Date|string|number} start - Start date * @param {Date|string|number} end - End date * @param {Object} [opts] * @param {boolean} [opts.includeObservedMondays=true] * If a holiday falls on a Sunday, include the Monday as "Observed". * @returns {Array<{ date: Date, name: string, weekday: string }>} */ export function getSAPublicHolidaysBetween(start, end, opts = {}) { const { includeObservedMondays = true } = opts; const s = new Date(start); s.setHours(0, 0, 0, 0); const e = new Date(end); e.setHours(23, 59, 59, 999); if (Number.isNaN(+s) || Number.isNaN(+e)) throw new Error("Invalid start or end date"); if (s > e) throw new Error("Start must be <= end"); const startYear = s.getFullYear(); const endYear = e.getFullYear(); // Build a map keyed by yyyy-mm-dd so we can merge collisions cleanly const map = new Map(); for (let y = startYear; y <= endYear; y++) { const yearHolidays = buildSAHolidaysForYear(y); // Add base holidays for (const { date, name } of yearHolidays) { addHoliday(map, date, name); } // Observed Monday rule (Public Holidays Act): if a holiday falls on Sunday, // the following Monday is a public holiday too. if (includeObservedMondays) { for (const { date, name } of yearHolidays) { if (date.getDay() === 0) { // Sunday const monday = new Date(date); monday.setDate(date.getDate() + 1); addHoliday(map, monday, `${name} (Observed)`); } } } } // Filter by range and sort const results = []; for (const [iso, info] of map) { const d = parseISODate(iso); if (d >= s && d <= e) { results.push({ date: d, name: info.names.join("; "), weekday: d.toLocaleDateString("en-ZA", { weekday: "long" }) }); } } results.sort((a, b) => a.date - b.date); return results; } /** * Build base SA public holidays for a given year. * Includes fixed-date and Easter-relative (Good Friday, Family Day). * Note: This does NOT add any discretionary "extra" days (e.g., 27 Dec by proclamation). */ export function buildSAHolidaysForYear(y) { // Fixed-date holidays (month is 0-based) const fixed = [ [new Date(y, 0, 1), "New Year's Day"], [new Date(y, 2, 21), "Human Rights Day"], [new Date(y, 3, 27), "Freedom Day"], [new Date(y, 4, 1), "Workers' Day"], [new Date(y, 5, 16), "Youth Day"], [new Date(y, 7, 9), "National Women's Day"], [new Date(y, 8, 24), "Heritage Day"], [new Date(y, 11, 16), "Day of Reconciliation"], [new Date(y, 11, 25), "Christmas Day"], [new Date(y, 11, 26), "Day of Goodwill"] ]; // Easter-based const easter = getEasterDate(y); // Easter Sunday const goodFriday = new Date(easter); // Good Friday = Easter Sunday - 2 goodFriday.setDate(easter.getDate() - 2); const familyDay = new Date(easter); // Family Day = Easter Monday = Easter Sunday + 1 familyDay.setDate(easter.getDate() + 1); fixed.push([goodFriday, "Good Friday"]); fixed.push([familyDay, "Family Day"]); // Normalize to midnight return fixed.map(([d, name]) => ({ date: new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0), name })); } // Helper: add to map keyed by ISO (yyyy-mm-dd); merge names if same day gets multiple holidays function addHoliday(map, date, name) { const iso = toISODateOnly(date); if (!map.has(iso)) map.set(iso, { names: [] }); const entry = map.get(iso); if (!entry.names.includes(name)) entry.names.push(name); } // Easter Sunday via Meeus/Jones/Butcher algorithm export function getEasterDate(year) { const f = Math.floor, G = year % 19, C = f(year / 100), H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30, I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11)), J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7, L = I - J, month = 3 + f((L + 40) / 44), day = L + 28 - 31 * f(month / 4); return new Date(year, month - 1, day); } // ---- small date helpers ---- function toISODateOnly(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 parseISODate(iso) { const [y, m, d] = iso.split("-").map(Number); return new Date(y, m - 1, d); } // --------------------------- // Module ready for import in ESM/CommonJS environments // Example usage can be found in the README.md // CommonJS compatibility // Allows: const { getSAPublicHolidaysBetween, buildSAHolidaysForYear, getEasterDate } = require('js-public-holidays-za'); if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { module.exports = { getSAPublicHolidaysBetween, buildSAHolidaysForYear, getEasterDate }; }