@kiwicredito/banking-holidays
Version:
A TypeScript package for detecting banking holidays in USA, Puerto Rico, and Dominican Republic
340 lines (339 loc) • 14 kB
JavaScript
"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