@oxog/kairos
Version:
Revolutionary zero-dependency JavaScript date/time library with modular architecture and dynamic holiday system
1,455 lines (1,445 loc) • 194 kB
JavaScript
/*!
* Kairos v1.0.0
* (c) 2025 Ersin Koc
* Released under the MIT License
* https://github.com/ersinkoc/kairos
*/
class LRUCache {
constructor(maxSize = 1000) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(key) {
const value = this.cache.get(key);
if (value !== undefined) {
this.cache.delete(key);
this.cache.set(key, value);
}
return value;
}
set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}
else if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
if (firstKey !== undefined) {
this.cache.delete(firstKey);
}
}
this.cache.set(key, value);
}
has(key) {
return this.cache.has(key);
}
clear() {
this.cache.clear();
}
size() {
return this.cache.size;
}
}
function memoize(fn, keyGenerator) {
const cache = new LRUCache();
return ((...args) => {
const key = keyGenerator ? keyGenerator(...args) : JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
});
}
function createDateCache() {
return new LRUCache(10000);
}
function createHolidayCache() {
return new LRUCache(5000);
}
function isValidDate(date) {
return date instanceof Date && !isNaN(date.getTime());
}
function isValidNumber(value) {
return typeof value === 'number' && !isNaN(value) && isFinite(value);
}
function isValidString(value) {
return typeof value === 'string' && value.length > 0;
}
function isValidYear(year) {
return isValidNumber(year) && year >= 1000 && year <= 9999;
}
function isValidMonth(month) {
return isValidNumber(month) && month >= 1 && month <= 12;
}
function isValidDay(day) {
return isValidNumber(day) && day >= 1 && day <= 31;
}
function isValidWeekday(weekday) {
return isValidNumber(weekday) && weekday >= 0 && weekday <= 6;
}
function isValidNth(nth) {
return isValidNumber(nth) && ((nth >= 1 && nth <= 5) || nth === -1);
}
function validateHolidayRule(rule) {
const errors = [];
if (!rule || typeof rule !== 'object') {
errors.push('Rule must be an object');
return errors;
}
if (!isValidString(rule.name)) {
errors.push('Rule name must be a non-empty string');
}
const validTypes = ['fixed', 'nth-weekday', 'relative', 'lunar', 'easter-based', 'custom'];
if (!validTypes.includes(rule.type)) {
errors.push(`Rule type must be one of: ${validTypes.join(', ')}`);
}
if (!rule.rule || typeof rule.rule !== 'object') {
errors.push('Rule must have a rule property');
return errors;
}
switch (rule.type) {
case 'fixed':
if (!isValidMonth(rule.rule.month)) {
errors.push('Fixed rule month must be 1-12');
}
if (!isValidDay(rule.rule.day)) {
errors.push('Fixed rule day must be 1-31');
}
break;
case 'nth-weekday':
if (!isValidMonth(rule.rule.month)) {
errors.push('Nth-weekday rule month must be 1-12');
}
if (!isValidWeekday(rule.rule.weekday)) {
errors.push('Nth-weekday rule weekday must be 0-6');
}
if (!isValidNth(rule.rule.nth)) {
errors.push('Nth-weekday rule nth must be 1-5 or -1');
}
break;
case 'relative':
if (!isValidString(rule.rule.relativeTo)) {
errors.push('Relative rule relativeTo must be a non-empty string');
}
if (!isValidNumber(rule.rule.offset)) {
errors.push('Relative rule offset must be a number');
}
break;
case 'lunar': {
const validCalendars = ['islamic', 'chinese', 'hebrew', 'persian'];
if (!validCalendars.includes(rule.rule.calendar)) {
errors.push(`Lunar rule calendar must be one of: ${validCalendars.join(', ')}`);
}
if (!isValidMonth(rule.rule.month)) {
errors.push('Lunar rule month must be 1-12');
}
if (!isValidDay(rule.rule.day)) {
errors.push('Lunar rule day must be 1-31');
}
break;
}
case 'easter-based':
if (!isValidNumber(rule.rule.offset)) {
errors.push('Easter-based rule offset must be a number');
}
break;
case 'custom':
if (typeof rule.rule.calculate !== 'function') {
errors.push('Custom rule must have a calculate function');
}
break;
}
return errors;
}
function throwError(message, code) {
const error = new Error(message);
if (code) {
error.code = code;
}
throw error;
}
const isKairosInstance = (obj) => {
return obj !== null && typeof obj === 'object' && '_date' in obj && obj._date instanceof Date;
};
const hasToDateMethod = (obj) => {
return (obj !== null && typeof obj === 'object' && 'toDate' in obj && typeof obj.toDate === 'function');
};
const isDateLike = (obj) => {
return (obj !== null &&
typeof obj === 'object' &&
(('year' in obj && 'month' in obj && 'day' in obj) || 'date' in obj));
};
const globalCache = new LRUCache(1000);
class KairosCore {
constructor(input) {
this._date = this.parseInput(input);
}
parseInput(input) {
if (input === undefined) {
return new Date();
}
if (input instanceof Date) {
return new Date(input.getTime());
}
if (typeof input === 'number') {
if (isNaN(input)) {
return new Date(NaN);
}
return new Date(input);
}
if (typeof input === 'string') {
if (input.toLowerCase() === 'invalid' || input === '') {
return new Date(NaN);
}
const dateOnlyPattern = /^\d{4}-\d{2}-\d{2}$/;
if (dateOnlyPattern.test(input)) {
const [year, month, day] = input.split('-').map(Number);
if (month < 1 || month > 12) {
return new Date(NaN);
}
const date = new Date(year, month - 1, day, 0, 0, 0, 0);
if (date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day) {
return new Date(NaN);
}
return date;
}
const europeanPattern = /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/;
if (europeanPattern.test(input)) {
const match = input.match(europeanPattern);
if (match) {
const day = parseInt(match[1], 10);
const month = parseInt(match[2], 10);
const year = parseInt(match[3], 10);
if (month < 1 || month > 12 || day < 1 || day > 31) {
return new Date(NaN);
}
const date = new Date(year, month - 1, day, 0, 0, 0, 0);
if (date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day) {
return new Date(NaN);
}
return date;
}
}
const parsed = new Date(input);
if (isNaN(parsed.getTime())) {
if (KairosCore.config.strict) {
throwError(`Invalid date string: ${input}`, 'INVALID_DATE');
}
return new Date(NaN);
}
return parsed;
}
if (input && typeof input === 'object') {
if (isKairosInstance(input)) {
return new Date(input._date.getTime());
}
if (hasToDateMethod(input)) {
return input.toDate();
}
if (isDateLike(input) &&
input.year !== undefined &&
input.month !== undefined &&
input.day !== undefined) {
const year = input.year;
const month = input.month - 1;
const day = input.day;
const hour = input.hour || 0;
const minute = input.minute || 0;
const second = input.second || 0;
const millisecond = input.millisecond || 0;
const date = new Date(year, month, day, hour, minute, second, millisecond);
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
return new Date(NaN);
}
return date;
}
if (isDateLike(input) && input.date instanceof Date) {
return new Date(input.date.getTime());
}
}
return new Date(NaN);
}
valueOf() {
return this._date.getTime();
}
toString() {
return this._date.toString();
}
toISOString() {
return this._date.toISOString();
}
offset() {
if (this._isUTC) {
return 0;
}
return -this._date.getTimezoneOffset();
}
toDate() {
return new Date(this._date.getTime());
}
clone() {
return new KairosCore(this._date);
}
year(value) {
if (value === undefined) {
return this._date.getFullYear();
}
const clone = this.clone();
clone._date.setFullYear(value);
return clone;
}
month(value) {
if (value === undefined) {
return this._date.getMonth() + 1;
}
const clone = this.clone();
clone._date.setMonth(value - 1);
return clone;
}
date(value) {
if (value === undefined) {
return this._date.getDate();
}
const clone = this.clone();
clone._date.setDate(value);
return clone;
}
day() {
return this._date.getDay();
}
hour(value) {
if (value === undefined) {
return this._date.getHours();
}
const clone = this.clone();
clone._date.setHours(value);
return clone;
}
minute(value) {
if (value === undefined) {
return this._date.getMinutes();
}
const clone = this.clone();
clone._date.setMinutes(value);
return clone;
}
second(value) {
if (value === undefined) {
return this._date.getSeconds();
}
const clone = this.clone();
clone._date.setSeconds(value);
return clone;
}
millisecond(value) {
if (value === undefined) {
return this._date.getMilliseconds();
}
const clone = this.clone();
clone._date.setMilliseconds(value);
return clone;
}
add(amount, unit) {
if (!this.isValid()) {
return this.clone();
}
const clone = this.clone();
const normalizedUnit = this.normalizeUnit(unit);
switch (normalizedUnit) {
case 'year':
clone._date.setFullYear(clone._date.getFullYear() + amount);
break;
case 'month': {
const currentDay = clone._date.getDate();
const currentMonth = clone._date.getMonth();
const currentYear = clone._date.getFullYear();
let targetMonth = currentMonth + amount;
let targetYear = currentYear;
while (targetMonth < 0) {
targetMonth += 12;
targetYear--;
}
while (targetMonth >= 12) {
targetMonth -= 12;
targetYear++;
}
const lastDayOfTargetMonth = new Date(targetYear, targetMonth + 1, 0).getDate();
clone._date.setDate(1);
clone._date.setFullYear(targetYear);
clone._date.setMonth(targetMonth);
clone._date.setDate(Math.min(currentDay, lastDayOfTargetMonth));
break;
}
case 'week':
clone._date.setDate(clone._date.getDate() + amount * 7);
break;
case 'day':
if (amount % 1 !== 0) {
const wholeDays = Math.floor(amount);
const fractionalHours = (amount - wholeDays) * 24;
clone._date.setDate(clone._date.getDate() + wholeDays);
clone._date.setHours(clone._date.getHours() + fractionalHours);
}
else {
clone._date.setDate(clone._date.getDate() + amount);
}
break;
case 'hour':
clone._date.setHours(clone._date.getHours() + amount);
break;
case 'minute':
clone._date.setMinutes(clone._date.getMinutes() + amount);
break;
case 'second':
clone._date.setSeconds(clone._date.getSeconds() + amount);
break;
case 'millisecond':
clone._date.setMilliseconds(clone._date.getMilliseconds() + amount);
break;
default:
throwError(`Unknown unit: ${unit}`, 'INVALID_UNIT');
}
return clone;
}
subtract(amount, unit) {
return this.add(-amount, unit);
}
startOf(unit) {
const clone = this.clone();
const normalizedUnit = this.normalizeUnit(unit);
switch (normalizedUnit) {
case 'year':
clone._date.setMonth(0, 1);
clone._date.setHours(0, 0, 0, 0);
break;
case 'month':
clone._date.setDate(1);
clone._date.setHours(0, 0, 0, 0);
break;
case 'week': {
const day = clone._date.getDay();
clone._date.setDate(clone._date.getDate() - day);
clone._date.setHours(0, 0, 0, 0);
break;
}
case 'day':
clone._date.setHours(0, 0, 0, 0);
break;
case 'hour':
clone._date.setMinutes(0, 0, 0);
break;
case 'minute':
clone._date.setSeconds(0, 0);
break;
case 'second':
clone._date.setMilliseconds(0);
break;
}
return clone;
}
endOf(unit) {
const clone = this.clone();
const normalizedUnit = this.normalizeUnit(unit);
switch (normalizedUnit) {
case 'year':
clone._date.setMonth(11, 31);
clone._date.setHours(23, 59, 59, 999);
break;
case 'month':
clone._date.setMonth(clone._date.getMonth() + 1, 0);
clone._date.setHours(23, 59, 59, 999);
break;
case 'week': {
const day = clone._date.getDay();
clone._date.setDate(clone._date.getDate() + (6 - day));
clone._date.setHours(23, 59, 59, 999);
break;
}
case 'day':
clone._date.setHours(23, 59, 59, 999);
break;
case 'hour':
clone._date.setMinutes(59, 59, 999);
break;
case 'minute':
clone._date.setSeconds(59, 999);
break;
case 'second':
clone._date.setMilliseconds(999);
break;
}
return clone;
}
isValid() {
return !isNaN(this._date.getTime());
}
isBefore(other) {
return this.valueOf() < other.valueOf();
}
isAfter(other) {
return this.valueOf() > other.valueOf();
}
isSame(other) {
return this.valueOf() === other.valueOf();
}
format(template = 'YYYY-MM-DD') {
if (!this.isValid()) {
return 'Invalid Date';
}
const isUtc = this._isUTC;
const year = isUtc ? this._date.getUTCFullYear() : this._date.getFullYear();
const month = isUtc ? this._date.getUTCMonth() + 1 : this._date.getMonth() + 1;
const date = isUtc ? this._date.getUTCDate() : this._date.getDate();
const hours = isUtc ? this._date.getUTCHours() : this._date.getHours();
const minutes = isUtc ? this._date.getUTCMinutes() : this._date.getMinutes();
const seconds = isUtc ? this._date.getUTCSeconds() : this._date.getSeconds();
if (isNaN(year) || isNaN(month) || isNaN(date)) {
return 'Invalid Date';
}
return template
.replace(/YYYY/g, year.toString())
.replace(/MM/g, month.toString().padStart(2, '0'))
.replace(/DD/g, date.toString().padStart(2, '0'))
.replace(/HH/g, hours.toString().padStart(2, '0'))
.replace(/mm/g, minutes.toString().padStart(2, '0'))
.replace(/ss/g, seconds.toString().padStart(2, '0'));
}
normalizeUnit(unit) {
const unitMap = {
y: 'year',
year: 'year',
years: 'year',
M: 'month',
month: 'month',
months: 'month',
w: 'week',
week: 'week',
weeks: 'week',
d: 'day',
day: 'day',
days: 'day',
h: 'hour',
hour: 'hour',
hours: 'hour',
m: 'minute',
minute: 'minute',
minutes: 'minute',
s: 'second',
second: 'second',
seconds: 'second',
ms: 'millisecond',
millisecond: 'millisecond',
milliseconds: 'millisecond',
};
return unitMap[unit] || unit;
}
}
KairosCore.config = {
locale: 'en',
strict: false,
suppressDeprecationWarnings: false,
};
class PluginSystem {
static use(plugin) {
const plugins = Array.isArray(plugin) ? plugin : [plugin];
for (const p of plugins) {
this.installPlugin(p);
}
return kairos;
}
static installPlugin(plugin) {
if (this.installedPlugins.has(plugin.name)) {
return;
}
if (plugin.dependencies) {
for (const dep of plugin.dependencies) {
if (!this.installedPlugins.has(dep)) {
throwError(`Plugin ${plugin.name} depends on ${dep} which is not installed`, 'MISSING_DEPENDENCY');
}
}
}
this.plugins.set(plugin.name, plugin);
this.installedPlugins.add(plugin.name);
const utils = {
cache: globalCache,
memoize,
validateInput: (input, type) => {
switch (type) {
case 'date':
return input instanceof Date && !isNaN(input.getTime());
case 'number':
return typeof input === 'number' && !isNaN(input);
case 'string':
return typeof input === 'string';
default:
return false;
}
},
throwError,
};
plugin.install(kairos, utils);
}
static extend(methods) {
Object.assign(this.extensionMethods, methods);
for (const [name, method] of Object.entries(methods)) {
KairosCore.prototype[name] = method;
}
}
static addStatic(methods) {
Object.assign(this.staticMethods, methods);
for (const [name, method] of Object.entries(methods)) {
kairos[name] = method;
}
}
static getPlugin(name) {
return this.plugins.get(name);
}
static isInstalled(name) {
return this.installedPlugins.has(name);
}
static getInstalledPlugins() {
return Array.from(this.installedPlugins);
}
}
PluginSystem.plugins = new Map();
PluginSystem.installedPlugins = new Set();
PluginSystem.extensionMethods = {};
PluginSystem.staticMethods = {};
const kairos = (input) => new KairosCore(input);
kairos.use = PluginSystem.use.bind(PluginSystem);
kairos.extend = PluginSystem.extend.bind(PluginSystem);
kairos.addStatic = PluginSystem.addStatic.bind(PluginSystem);
kairos.plugins = PluginSystem.plugins;
kairos.utc = (input) => {
let utcDate;
if (typeof input === 'string' &&
!input.endsWith('Z') &&
!input.includes('+') &&
!/[+-]\d{2}:?\d{2}$/.test(input)) {
const dateTimePattern = /^(\d{4})-(\d{2})-(\d{2})(?:\s+|T)(\d{2}):(\d{2})(?::(\d{2}))?$/;
const dateOnlyPattern = /^(\d{4})-(\d{2})-(\d{2})$/;
const match = input.match(dateTimePattern) || input.match(dateOnlyPattern);
if (match) {
const year = parseInt(match[1], 10);
const month = parseInt(match[2], 10) - 1;
const day = parseInt(match[3], 10);
const hour = match[4] ? parseInt(match[4], 10) : 0;
const minute = match[5] ? parseInt(match[5], 10) : 0;
const second = match[6] ? parseInt(match[6], 10) : 0;
utcDate = new Date(Date.UTC(year, month, day, hour, minute, second));
}
else {
input = input.replace(' ', 'T') + 'Z';
utcDate = new Date(input);
}
}
else {
utcDate = new Date(input);
}
const instance = new KairosCore(utcDate);
instance._isUTC = true;
return instance;
};
kairos.unix = (timestamp) => new KairosCore(new Date(timestamp * 1000));
class LocaleManager {
constructor() {
this.locales = new Map();
this.currentLocale = 'en-US';
this.defaultLocale = 'en-US';
}
static getInstance() {
if (!LocaleManager.instance) {
LocaleManager.instance = new LocaleManager();
}
return LocaleManager.instance;
}
register(code, locale) {
this.locales.set(code, locale);
if (this.locales.size === 1) {
this.defaultLocale = code;
this.currentLocale = code;
}
}
setLocale(code) {
if (this.locales.has(code)) {
this.currentLocale = code;
return true;
}
return false;
}
getLocale(code) {
const targetCode = code || this.currentLocale;
return this.locales.get(targetCode);
}
getCurrentLocale() {
return this.currentLocale;
}
getDefaultLocale() {
return this.defaultLocale;
}
setDefaultLocale(code) {
if (this.locales.has(code)) {
this.defaultLocale = code;
return true;
}
return false;
}
getHolidays(localeCode, type) {
const locale = this.getLocale(localeCode);
if (!locale) {
return [];
}
if (type) {
switch (type) {
case 'federal':
return locale.federalHolidays || [];
case 'state':
return locale.stateHolidays ? Object.values(locale.stateHolidays).flat() : [];
case 'public':
return locale.publicHolidays || [];
case 'observances':
return locale.observances || [];
default:
if (locale[type] && Array.isArray(locale[type])) {
return locale[type];
}
}
}
return locale.holidays || [];
}
getStateHolidays(state, localeCode) {
const locale = this.getLocale(localeCode);
if (!locale || !locale.stateHolidays) {
return [];
}
const stateLower = state.toLowerCase();
return locale.stateHolidays[stateLower] || [];
}
getAllHolidays(localeCode) {
const locale = this.getLocale(localeCode);
if (!locale) {
return [];
}
const allHolidays = [];
if (locale.holidays) {
allHolidays.push(...locale.holidays);
}
if (locale.federalHolidays) {
allHolidays.push(...locale.federalHolidays);
}
if (locale.stateHolidays) {
for (const stateHols of Object.values(locale.stateHolidays)) {
allHolidays.push(...stateHols);
}
}
if (locale.publicHolidays) {
allHolidays.push(...locale.publicHolidays);
}
if (locale.observances) {
allHolidays.push(...locale.observances);
}
const uniqueHolidays = new Map();
for (const holiday of allHolidays) {
if (!uniqueHolidays.has(holiday.name)) {
uniqueHolidays.set(holiday.name, holiday);
}
}
return Array.from(uniqueHolidays.values());
}
getAvailableLocales() {
return Array.from(this.locales.keys());
}
hasLocale(code) {
return this.locales.has(code);
}
clear() {
this.locales.clear();
this.currentLocale = 'en-US';
this.defaultLocale = 'en-US';
}
}
const localeManager = LocaleManager.getInstance();
class HolidayEngine {
constructor() {
this.calculators = new Map();
this.cache = createHolidayCache();
this.ruleCache = new Map();
this.registerCalculators();
}
registerCalculators() {
}
registerCalculator(type, calculator) {
this.calculators.set(type, calculator);
}
calculate(rule, year) {
const errors = validateHolidayRule(rule);
if (errors.length > 0) {
throw new Error(`Invalid holiday rule: ${errors.join(', ')}`);
}
if (!this.ruleCache.has(rule.name || 'unnamed')) {
this.ruleCache.set(rule.name || 'unnamed', new Map());
}
const yearCache = this.ruleCache.get(rule.name || 'unnamed');
if (yearCache.has(year)) {
return yearCache.get(year);
}
const calculator = this.calculators.get(rule.type);
if (!calculator) {
throw new Error(`Unknown holiday type: ${rule.type}`);
}
let dates = calculator.calculate(rule, year);
if (rule.observedRule) {
dates = this.applyObservedRules(dates, rule.observedRule);
}
if (rule.duration && rule.duration > 1) {
dates = this.expandDuration(dates, rule.duration);
}
yearCache.set(year, dates);
return dates;
}
applyObservedRules(dates, observedRule) {
const result = [];
for (const date of dates) {
const weekday = date.getDay();
const isWeekend = observedRule.weekends?.includes(weekday) || weekday === 0 || weekday === 6;
if (!isWeekend) {
result.push(date);
continue;
}
switch (observedRule.type) {
case 'substitute':
result.push(this.findSubstituteDate(date, observedRule));
break;
case 'nearest-weekday':
result.push(this.findNearestWeekday(date));
break;
case 'bridge':
result.push(date);
result.push(this.findBridgeDate(date));
break;
default:
result.push(date);
}
}
return result;
}
findSubstituteDate(date, observedRule) {
const direction = observedRule.direction || 'forward';
const weekends = observedRule.weekends || [0, 6];
const current = new Date(date);
const increment = direction === 'forward' ? 1 : -1;
while (weekends.includes(current.getDay())) {
current.setDate(current.getDate() + increment);
}
return current;
}
findNearestWeekday(date) {
const weekday = date.getDay();
if (weekday === 0) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
}
else if (weekday === 6) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1);
}
return date;
}
findBridgeDate(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
}
expandDuration(dates, duration) {
const result = [];
for (const date of dates) {
for (let i = 0; i < duration; i++) {
const expandedDate = new Date(date);
expandedDate.setDate(expandedDate.getDate() + i);
result.push(expandedDate);
}
}
return result;
}
isHoliday(date, holidays) {
const year = date.getFullYear();
for (const holiday of holidays) {
if (!holiday.active && holiday.active !== undefined) {
continue;
}
const holidayDates = this.calculateWithContext(holiday, year, holidays);
for (const holidayDate of holidayDates) {
if (this.isSameDay(date, holidayDate)) {
return {
id: holiday.id || holiday.name,
name: holiday.name,
type: holiday.type,
date: holidayDate,
regions: holiday.regions || [],
};
}
}
}
return null;
}
getHolidaysForYear(year, holidays) {
const result = [];
for (const holiday of holidays) {
if (!holiday.active && holiday.active !== undefined) {
continue;
}
const dates = this.calculateWithContext(holiday, year, holidays);
for (const date of dates) {
result.push({
id: holiday.id || holiday.name,
name: holiday.name,
type: holiday.type,
date,
regions: holiday.regions || [],
});
}
}
return result.sort((a, b) => a.date.getTime() - b.date.getTime());
}
calculateWithContext(rule, year, allHolidays) {
const errors = validateHolidayRule(rule);
if (errors.length > 0) {
throw new Error(`Invalid holiday rule: ${errors.join(', ')}`);
}
if (!this.ruleCache.has(rule.name || 'unnamed')) {
this.ruleCache.set(rule.name || 'unnamed', new Map());
}
const yearCache = this.ruleCache.get(rule.name || 'unnamed');
if (yearCache.has(year)) {
return yearCache.get(year);
}
const calculator = this.calculators.get(rule.type);
if (!calculator) {
throw new Error(`Unknown holiday type: ${rule.type}`);
}
let dates;
if (rule.type === 'relative') {
dates = calculator.calculate(rule, year, { holidays: allHolidays });
}
else {
dates = calculator.calculate(rule, year);
}
if (rule.observedRule) {
dates = this.applyObservedRules(dates, rule.observedRule);
}
if (rule.duration && rule.duration > 1) {
dates = this.expandDuration(dates, rule.duration);
}
yearCache.set(year, dates);
return dates;
}
getHolidaysInRange(start, end, holidays) {
const result = [];
const startYear = start.getFullYear();
const endYear = end.getFullYear();
for (let year = startYear; year <= endYear; year++) {
const yearHolidays = this.getHolidaysForYear(year, holidays);
for (const holiday of yearHolidays) {
if (holiday.date >= start && holiday.date <= end) {
result.push(holiday);
}
}
}
return result;
}
getNextHoliday(after, holidays) {
const year = after.getFullYear();
const currentYearHolidays = this.getHolidaysForYear(year, holidays);
for (const holiday of currentYearHolidays) {
if (holiday.date > after) {
return holiday;
}
}
const nextYearHolidays = this.getHolidaysForYear(year + 1, holidays);
return nextYearHolidays[0] || null;
}
getPreviousHoliday(before, holidays) {
const year = before.getFullYear();
const currentYearHolidays = this.getHolidaysForYear(year, holidays);
for (let i = currentYearHolidays.length - 1; i >= 0; i--) {
const holiday = currentYearHolidays[i];
if (holiday.date < before) {
return holiday;
}
}
const prevYearHolidays = this.getHolidaysForYear(year - 1, holidays);
return prevYearHolidays[prevYearHolidays.length - 1] || null;
}
isSameDay(date1, date2) {
return (date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate());
}
clearCache() {
this.cache.clear();
this.ruleCache.clear();
}
}
const engine = new HolidayEngine();
var engine$1 = {
name: 'holiday-engine',
version: '1.0.0',
size: 2048,
install(kairos, _utils) {
kairos.extend({
isHoliday(holidays) {
const rules = holidays || localeManager.getHolidays();
return engine.isHoliday(this.toDate(), rules) !== null;
},
getHolidayInfo(holidays) {
const rules = holidays || localeManager.getHolidays();
return engine.isHoliday(this.toDate(), rules);
},
nextHoliday(holidays) {
const rules = holidays || localeManager.getHolidays();
const next = engine.getNextHoliday(this.toDate(), rules);
return next ? kairos(next.date) : null;
},
previousHoliday(holidays) {
const rules = holidays || localeManager.getHolidays();
const prev = engine.getPreviousHoliday(this.toDate(), rules);
return prev ? kairos(prev.date) : null;
},
getHolidays(type) {
return localeManager.getHolidays(undefined, type);
},
});
kairos.addStatic?.({
getYearHolidays(year, holidays) {
return engine.getHolidaysForYear(year, holidays);
},
getHolidaysInRange(start, end, holidays) {
const startDate = kairos(start).toDate();
const endDate = kairos(end).toDate();
return engine.getHolidaysInRange(startDate, endDate, holidays);
},
holidayEngine: engine,
});
},
};
class FixedCalculator {
calculate(rule, year) {
const { month, day } = rule.rule;
const date = new Date(year, month - 1, day);
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
return [];
}
return [date];
}
}
var fixed = {
name: 'holiday-fixed-calculator',
version: '1.0.0',
size: 256,
dependencies: ['holiday-engine'],
install(kairos, _utils) {
const engine = kairos.holidayEngine;
if (engine) {
engine.registerCalculator('fixed', new FixedCalculator());
}
},
};
class NthWeekdayCalculator {
calculate(rule, year) {
const { month, weekday, nth } = rule.rule;
if (nth > 0) {
return [this.getNthWeekdayOfMonth(year, month - 1, weekday, nth)];
}
else {
return [this.getLastNthWeekdayOfMonth(year, month - 1, weekday, Math.abs(nth))];
}
}
getNthWeekdayOfMonth(year, month, weekday, nth) {
const firstDay = new Date(year, month, 1);
const firstDayWeekday = firstDay.getDay();
let daysUntilWeekday = weekday - firstDayWeekday;
if (daysUntilWeekday < 0) {
daysUntilWeekday += 7;
}
const date = 1 + daysUntilWeekday + (nth - 1) * 7;
const result = new Date(year, month, date);
if (result.getMonth() !== month) {
throw new Error(`${nth}${this.getOrdinalSuffix(nth)} ${this.getWeekdayName(weekday)} of ${this.getMonthName(month)} ${year} does not exist`);
}
return result;
}
getLastNthWeekdayOfMonth(year, month, weekday, nth) {
const lastDay = new Date(year, month + 1, 0);
const lastDayWeekday = lastDay.getDay();
let daysBack = lastDayWeekday - weekday;
if (daysBack < 0) {
daysBack += 7;
}
const date = lastDay.getDate() - daysBack - (nth - 1) * 7;
if (date < 1) {
throw new Error(`${nth}${this.getOrdinalSuffix(nth)} to last ${this.getWeekdayName(weekday)} of ${this.getMonthName(month)} ${year} does not exist`);
}
return new Date(year, month, date);
}
getOrdinalSuffix(n) {
if (n >= 11 && n <= 13)
return 'th';
switch (n % 10) {
case 1:
return 'st';
case 2:
return 'nd';
case 3:
return 'rd';
default:
return 'th';
}
}
getWeekdayName(weekday) {
const names = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
return names[weekday] || 'Unknown';
}
getMonthName(month) {
const names = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
return names[month] || 'Unknown';
}
}
var nthWeekday = {
name: 'holiday-nth-weekday-calculator',
version: '1.0.0',
size: 512,
dependencies: ['holiday-engine'],
install(kairos, _utils) {
const engine = kairos.holidayEngine;
if (engine) {
engine.registerCalculator('nth-weekday', new NthWeekdayCalculator());
}
},
};
class EasterCalculator {
calculate(rule, year) {
const { offset } = rule.rule;
const easterDate = this.calculateEaster(year);
const resultDate = new Date(easterDate);
resultDate.setDate(resultDate.getDate() + offset);
return [resultDate];
}
calculateEaster(year) {
if (year < 1583) {
return this.calculateJulianEaster(year);
}
const a = year % 19;
const b = Math.floor(year / 100);
const c = year % 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(year, month, day);
}
calculateJulianEaster(year) {
const a = year % 4;
const b = year % 7;
const c = year % 19;
const d = (19 * c + 15) % 30;
const e = (2 * a + 4 * b - d + 34) % 7;
const month = Math.floor((d + e + 114) / 31) - 1;
const day = ((d + e + 114) % 31) + 1;
const julianDate = new Date(year, month, day);
const julianDayNumber = this.dateToJulianDay(julianDate);
const gregorianDate = this.julianDayToDate(julianDayNumber);
return gregorianDate;
}
dateToJulianDay(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const a = Math.floor((14 - month) / 12);
const y = year + 4800 - a;
const m = month + 12 * a - 3;
return (day +
Math.floor((153 * m + 2) / 5) +
365 * y +
Math.floor(y / 4) -
Math.floor(y / 100) +
Math.floor(y / 400) -
32045);
}
julianDayToDate(jdn) {
const a = jdn + 32044;
const b = (4 * a + 3) / 146097;
const c = a - Math.floor((146097 * b) / 4);
const d = (4 * c + 3) / 1461;
const e = c - Math.floor((1461 * d) / 4);
const m = (5 * e + 2) / 153;
const day = e - Math.floor((153 * m + 2) / 5) + 1;
const month = m + 3 - 12 * Math.floor(m / 10);
const year = 100 * b + d - 4800 + Math.floor(m / 10);
return new Date(year, month - 1, day);
}
calculateOrthodoxEaster(year) {
const a = year % 4;
const b = year % 7;
const c = year % 19;
const d = (19 * c + 15) % 30;
const e = (2 * a + 4 * b - d + 34) % 7;
const month = Math.floor((d + e + 114) / 31);
const day = ((d + e + 114) % 31) + 1;
const julianDate = new Date(year, month - 1, day);
const diff = this.getJulianGregorianDifference(year);
const orthodoxEaster = new Date(julianDate);
orthodoxEaster.setDate(orthodoxEaster.getDate() + diff);
return orthodoxEaster;
}
getJulianGregorianDifference(year) {
if (year < 1583)
return 0;
const centuries = Math.floor(year / 100);
const leapCenturies = Math.floor(centuries / 4);
return centuries - leapCenturies - 2;
}
}
var easter = {
name: 'holiday-easter-calculator',
version: '1.0.0',
size: 1024,
dependencies: ['holiday-engine'],
install(kairos, _utils) {
const engine = kairos.holidayEngine;
if (engine) {
engine.registerCalculator('easter-based', new EasterCalculator());
}
kairos.addStatic?.({
getEaster(year) {
const calculator = new EasterCalculator();
const easterDate = calculator.calculateEaster(year);
return kairos(easterDate);
},
getOrthodoxEaster(year) {
const calculator = new EasterCalculator();
const orthodoxEasterDate = calculator.calculateOrthodoxEaster(year);
return kairos(orthodoxEasterDate);
},
});
},
};
class LunarCalculator {
constructor() {
this.converters = {
islamic: new IslamicConverter(),
chinese: new ChineseConverter(),
hebrew: new HebrewConverter(),
persian: new PersianConverter(),
};
}
calculate(rule, year) {
const { calendar, month, day } = rule.rule;
const converter = this.converters[calendar];
if (!converter) {
throw new Error(`Unknown lunar calendar: ${calendar}`);
}
const lunarYear = this.getLunarYear(year, calendar);
const gregorianDate = converter.toGregorian(lunarYear, month, day);
return [gregorianDate];
}
getLunarYear(gregorianYear, calendar) {
switch (calendar) {
case 'islamic':
return Math.round((gregorianYear - 622) * 1.030684);
case 'chinese':
return gregorianYear - 2637;
case 'hebrew':
return gregorianYear + 3761;
case 'persian':
return gregorianYear - 622;
default:
return gregorianYear;
}
}
}
class IslamicConverter {
toGregorian(hijriYear, hijriMonth, hijriDay) {
const epochOffset = 1948084;
const yearLength = 354.36667;
const totalDays = (hijriYear - 1) * yearLength + this.getIslamicMonthDays(hijriMonth, hijriYear) + hijriDay - 1;
const julianDay = epochOffset + totalDays;
return this.julianDayToGregorian(julianDay);
}
fromGregorian(date) {
const julianDay = this.gregorianToJulianDay(date);
const epochOffset = 1948084;
const totalDays = julianDay - epochOffset;
const yearLength = 354.36667;
const year = Math.floor(totalDays / yearLength) + 1;
const remainingDays = totalDays - (year - 1) * yearLength;
let month = 1;
let dayOfYear = remainingDays;
while (dayOfYear > this.getIslamicMonthLength(month, year)) {
dayOfYear -= this.getIslamicMonthLength(month, year);
month++;
}
return {
year,
month,
day: Math.floor(dayOfYear),
};
}
getIslamicMonthDays(month, year) {
let days = 0;
for (let i = 1; i < month; i++) {
days += this.getIslamicMonthLength(i, year);
}
return days;
}
getIslamicMonthLength(month, year) {
const lengths = [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29];
if (month === 12 && this.isIslamicLeapYear(year)) {
return 30;
}
return lengths[month - 1] || 29;
}
isIslamicLeapYear(year) {
return (year * 11 + 14) % 30 < 11;
}
julianDayToGregorian(jd) {
const a = jd + 32044;
const b = (4 * a + 3) / 146097;
const c = a - Math.floor((146097 * b) / 4);
const d = (4 * c + 3) / 1461;
const e = c - Math.floor((1461 * d) / 4);
const m = (5 * e + 2) / 153;
const day = e - Math.floor((153 * m + 2) / 5) + 1;
const month = m + 3 - 12 * Math.floor(m / 10);
const year = 100 * b + d - 4800 + Math.floor(m / 10);
return new Date(year, month - 1, day);
}
gregorianToJulianDay(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const a = Math.floor((14 - month) / 12);
const y = year + 4800 - a;
const m = month + 12 * a - 3;
return (day +
Math.floor((153 * m + 2) / 5) +
365 * y +
Math.floor(y / 4) -
Math.floor(y / 100) +
Math.floor(y / 400) -
32045);
}
}
class ChineseConverter {
toGregorian(chineseYear, chineseMonth, chineseDay) {
const epochYear = 2637;
const gregorianYear = chineseYear + epochYear;
const newYearOffset = Math.floor(Math.random() * 30) + 21;
const baseDate = new Date(gregorianYear, 0, newYearOffset);
const lunarMonthLength = 29.5;
const totalDays = (chineseMonth - 1) * lunarMonthLength + chineseDay - 1;
const result = new Date(baseDate);
result.setDate(result.getDate() + totalDays);
return result;
}
fromGregorian(date) {
const epochYear = 2637;
const year = date.getFullYear() - epochYear;
return {
year,
month: date.getMonth() + 1,
day: date.getDate(),
};
}
}
class HebrewConverter {
toGregorian(hebrewYear, hebrewMonth, hebrewDay) {
const epochOffset = 3761;
const gregorianYear = hebrewYear - epochOffset;
const baseDate = new Date(gregorianYear, 8, 15);
const totalDays = (hebrewMonth - 1) * 29.5 + hebrewDay - 1;
const result = new Date(baseDate);
result.setDate(result.getDate() + totalDays);
return result;
}
fromGregorian(date) {
const epochOffset = 3761;
const year = date.getFullYear() + epochOffset;
return {
year,
month: date.getMonth() + 1,
day: date.getDate(),
};
}
}
class PersianConverter {
toGregorian(persianYear, persianMonth, persianDay) {
const epochYear = 622;
const gregorianYear = persianYear + epochYear;
const baseDate = new Date(gregorianYear, 2, 21);
const totalDays = (persianMonth - 1) * 30 + persianDay - 1;
const result = new Date(baseDate);
result.setDate(result.getDate() + totalDays);
return result;
}
fromGregorian(date) {
const epochYear = 622;
const year = date.getFullYear() - epochYear;
return {
year,
month: date.getMonth() + 1,
day: date.getDate(),
};
}
}
var lunar = {
name: 'holiday-lunar-calculator',
version: '1.0.0',
size: 2048,
dependencies: ['holiday-engine'],
install(kairos, _utils) {
const engine = kairos.holidayEngine;
if (engine) {
engine.registerCalculator('lunar', new LunarCalculator());
}
},
};
class RelativeCalculator {
constructor() {
this.holidayCache = new Map();
this.allHolidays = [];
}
calculate(rule, year, context) {
const { relativeTo, offset } = rule.rule;
if (context?.holidays) {
this.allHolidays = context.holidays;
}
const baseHoliday = this.findBaseHoliday(relativeTo);
if (!baseHoliday) {
throw new Error(`Base holiday '${relativeTo}' not found for relative rule '${rule.name}'`);
}
const baseDates = this.calculateBaseHolidayDates(baseHoliday, year);
const result = [];
for (const baseDate of baseDates) {
const relativeDate = new Date(baseDate);
relativeDate.setDate(relativeDate.getDate() + offset);