UNPKG

@kiwicredito/banking-holidays

Version:

A TypeScript package for detecting banking holidays in USA, Puerto Rico, and Dominican Republic

340 lines (339 loc) 14 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getDateStatus = getDateStatus; exports.isHoliday = isHoliday; exports.isWorkingDay = isWorkingDay; exports.getHolidays = getHolidays; exports.getPreviousWorkingDay = getPreviousWorkingDay; exports.getNextWorkingDay = getNextWorkingDay; exports.getNextWorkingNDays = getNextWorkingNDays; exports.getPreviousWorkingNDays = getPreviousWorkingNDays; const types_1 = require("./types"); const date_fns_tz_1 = require("date-fns-tz"); const _2024_json_1 = __importDefault(require("./data/holidays/usa/2024.json")); const _2025_json_1 = __importDefault(require("./data/holidays/usa/2025.json")); const _2024_json_2 = __importDefault(require("./data/holidays/pr/2024.json")); const _2025_json_2 = __importDefault(require("./data/holidays/pr/2025.json")); const _2024_json_3 = __importDefault(require("./data/holidays/rd/2024.json")); // Map of country codes to their holiday data const holidayData = { USA: { '2024': _2024_json_1.default, '2025': _2025_json_1.default }, PR: { '2024': _2024_json_2.default, '2025': _2025_json_2.default }, RD: { '2024': _2024_json_3.default } }; /** * Parse timestamp format '/Date(timestamp)/' to Date object */ function parseTimestampFormat(dateInput) { const timestampMatch = dateInput.match(/^\/Date\((\d+)\)\/$/); if (timestampMatch) { const timestamp = parseInt(timestampMatch[1]); // Most /Date()/ formats use milliseconds since epoch // If timestamp appears to be in seconds (< year 2030 in seconds), convert to milliseconds const timestampMs = timestamp < 1893456000 ? timestamp * 1000 : timestamp; return new Date(timestampMs); } throw new Error(`Invalid timestamp format: ${dateInput}`); } /** * Create a date in the target country's timezone */ function createDateInTimezone(dateStr, country) { const timezone = types_1.COUNTRY_CONFIG[country].timezone; // Handle timestamp format '/Date(timestamp)/' if (dateStr.startsWith('/Date(') && dateStr.endsWith(')/')) { return parseTimestampFormat(dateStr); } // For date-only strings (YYYY-MM-DD), create the date directly in the target timezone // to avoid timezone shifts that change the actual date if (!dateStr.includes('T')) { const [year, month, day] = dateStr.split('-').map(Number); // Create date directly in local time (no timezone conversion needed for date-only) return new Date(year, month - 1, day); } // For datetime strings, use timezone conversion return (0, date_fns_tz_1.toZonedTime)(new Date(dateStr), timezone); } /** * Get holidays for a specific country and year */ function getHolidaysForYear(country, year) { const countryData = holidayData[country]; if (!countryData) return []; const yearData = countryData[year]; if (!yearData) return []; return yearData.holidays.map(holiday => ({ ...holiday, country })); } /** * Get all holidays for a given period and country */ function getAllHolidays(options = {}) { const { country, startDate, endDate } = options; const startYear = (startDate === null || startDate === void 0 ? void 0 : startDate.getFullYear().toString()) || '2024'; const endYear = (endDate === null || endDate === void 0 ? void 0 : endDate.getFullYear().toString()) || '2024'; const countries = country ? [country] : Object.keys(holidayData); const years = Array.from({ length: parseInt(endYear) - parseInt(startYear) + 1 }, (_, i) => (parseInt(startYear) + i).toString()); return countries.flatMap((country) => years.flatMap((year) => getHolidaysForYear(country, year))).filter((holiday) => { const holidayDate = new Date(holiday.date); if (startDate && holidayDate < startDate) { return false; } if (endDate && holidayDate > endDate) { return false; } return true; }); } /** * Check if a given date is a banking holiday and its working status */ function getDateStatus(date, country) { // If date is a string, create it in the target timezone const countryDate = typeof date === 'string' ? createDateInTimezone(date, country) : (0, date_fns_tz_1.toZonedTime)(date, types_1.COUNTRY_CONFIG[country].timezone); // Format the date in the country's timezone for comparison const dateStr = (0, date_fns_tz_1.formatInTimeZone)(countryDate, types_1.COUNTRY_CONFIG[country].timezone, 'yyyy-MM-dd'); const year = dateStr.split('-')[0]; const holidays = getHolidaysForYear(country, year); const holiday = holidays.find(h => h.date === dateStr); // Calculate day of week from the actual date string to avoid timezone issues const [yearNum, monthNum, dayNum] = dateStr.split('-').map(Number); const actualDate = new Date(yearNum, monthNum - 1, dayNum); const dayOfWeek = actualDate.getDay(); const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; return { isHoliday: !!holiday, holiday, isWorkingDay: !isWeekend && !holiday, isWeekend }; } /** * Check if a given date is a banking holiday */ function isHoliday(date, country) { const status = getDateStatus(date, country); return { isHoliday: status.isHoliday, holiday: status.holiday }; } /** * Check if a given date is a working day */ function isWorkingDay(date, country) { return getDateStatus(date, country).isWorkingDay; } /** * Get all holidays for a given period and country */ function getHolidays(options = {}) { return getAllHolidays(options); } /** * Get the previous working day from a given date * @param date The date to start from (Date object, ISO string, or timestamp format) * @param country The country to check holidays for * @returns The previous working day */ function getPreviousWorkingDay(date, country) { // Parse the input date properly let currentDate; if (typeof date === 'string') { if (date.startsWith('/Date(') && date.endsWith(')/')) { // Handle timestamp format '/Date(timestamp)/' const parsedDate = parseTimestampFormat(date); // Apply UTC normalization like Date object path const utcYear = parsedDate.getUTCFullYear(); const utcMonth = parsedDate.getUTCMonth(); const utcDate = parsedDate.getUTCDate(); currentDate = new Date(utcYear, utcMonth, utcDate); } else { // Handle regular date strings - create as UTC to avoid timezone issues const [year, month, day] = date.split('-').map(Number); currentDate = new Date(year, month - 1, day); } } else { // Handle Date objects - use UTC date to avoid timezone issues const utcYear = date.getUTCFullYear(); const utcMonth = date.getUTCMonth(); const utcDate = date.getUTCDate(); currentDate = new Date(utcYear, utcMonth, utcDate); } // Always start from the previous day (regardless of whether current day is working or not) do { currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1); } while (!isWorkingDay(currentDate, country)); return currentDate; } function getNextWorkingDay(date, country) { // Parse the input date properly let currentDate; if (typeof date === 'string') { if (date.startsWith('/Date(') && date.endsWith(')/')) { // Handle timestamp format '/Date(timestamp)/' currentDate = parseTimestampFormat(date); } else { // Handle regular date strings if (date.includes('T')) { // Handle ISO datetime strings currentDate = new Date(date); } else { // Handle simple date strings (YYYY-MM-DD) - create as local date to avoid timezone issues const [year, month, day] = date.split('-').map(Number); currentDate = new Date(year, month - 1, day); } } } else { // Handle Date objects - use UTC date to avoid timezone issues const utcYear = date.getUTCFullYear(); const utcMonth = date.getUTCMonth(); const utcDate = date.getUTCDate(); currentDate = new Date(utcYear, utcMonth, utcDate); } // Always start from the next day (regardless of whether current day is working or not) do { currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1); } while (!isWorkingDay(currentDate, country)); return currentDate; } /** * Get the working day that is N business days after the given date * @param date The date to start from (Date object or ISO string) * @param days Number of business days to add * @param country The country to check holidays for * @returns The working day N business days after the input date */ function getNextWorkingNDays(date, days, country) { if (days < 0) { throw new Error('Number of days must be non-negative. Use getPreviousWorkingNDays for negative values.'); } if (days === 0) { // If 0 days, return the next working day if current date is not working, otherwise current date const currentStatus = getDateStatus(date, country); return currentStatus.isWorkingDay ? (typeof date === 'string' ? new Date(date) : new Date(date.getTime())) : getNextWorkingDay(date, country); } // Parse the input date properly let currentDate; if (typeof date === 'string') { if (date.startsWith('/Date(') && date.endsWith(')/')) { // Handle timestamp format '/Date(timestamp)/' const parsedDate = parseTimestampFormat(date); // Apply UTC normalization like Date object path const utcYear = parsedDate.getUTCFullYear(); const utcMonth = parsedDate.getUTCMonth(); const utcDate = parsedDate.getUTCDate(); currentDate = new Date(utcYear, utcMonth, utcDate); } else { // Handle regular date strings if (date.includes('T')) { // Handle ISO datetime strings currentDate = new Date(date); } else { // Handle simple date strings (YYYY-MM-DD) - create as local date to avoid timezone issues const [year, month, day] = date.split('-').map(Number); currentDate = new Date(year, month - 1, day); } } } else { // Handle Date objects - use UTC date to avoid timezone issues const utcYear = date.getUTCFullYear(); const utcMonth = date.getUTCMonth(); const utcDate = date.getUTCDate(); currentDate = new Date(utcYear, utcMonth, utcDate); } let businessDaysAdded = 0; // Keep moving forward until we've added the required number of business days while (businessDaysAdded < days) { // Move to next day currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1); // If this day is a working day, count it if (isWorkingDay(currentDate, country)) { businessDaysAdded++; } } return currentDate; } /** * Get the working day that is N business days before the given date * @param date The date to start from (Date object or ISO string) * @param days Number of business days to subtract * @param country The country to check holidays for * @returns The working day N business days before the input date */ function getPreviousWorkingNDays(date, days, country) { if (days < 0) { throw new Error('Number of days must be non-negative. Use getNextWorkingNDays for negative values.'); } if (days === 0) { // If 0 days, return the previous working day if current date is not working, otherwise current date const currentStatus = getDateStatus(date, country); return currentStatus.isWorkingDay ? (typeof date === 'string' ? new Date(date) : new Date(date.getTime())) : getPreviousWorkingDay(date, country); } // Parse the input date properly let currentDate; if (typeof date === 'string') { if (date.startsWith('/Date(') && date.endsWith(')/')) { // Handle timestamp format '/Date(timestamp)/' currentDate = parseTimestampFormat(date); } else { // Handle regular date strings if (date.includes('T')) { // Handle ISO datetime strings currentDate = new Date(date); } else { // Handle simple date strings (YYYY-MM-DD) - create as local date to avoid timezone issues const [year, month, day] = date.split('-').map(Number); currentDate = new Date(year, month - 1, day); } } } else { // Handle Date objects - use UTC date to avoid timezone issues const utcYear = date.getUTCFullYear(); const utcMonth = date.getUTCMonth(); const utcDate = date.getUTCDate(); currentDate = new Date(utcYear, utcMonth, utcDate); } let businessDaysSubtracted = 0; // Keep moving backward until we've subtracted the required number of business days while (businessDaysSubtracted < days) { // Move to previous day currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1); // If this day is a working day, count it if (isWorkingDay(currentDate, country)) { businessDaysSubtracted++; } } return currentDate; } // Export other functions and types here