UNPKG

@oxog/kairos

Version:

Revolutionary zero-dependency JavaScript date/time library with modular architecture and dynamic holiday system

1,301 lines (1,291 loc) 220 kB
/*! * Kairos v1.0.0 * (c) 2025 Ersin Koc * Released under the MIT License * https://github.com/ersinkoc/kairos */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.kairos = {})); })(this, (function (exports) { 'use strict'; 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;