@raven-js/cortex
Version:
Zero-dependency machine learning, AI, and data processing library for modern JavaScript
241 lines (214 loc) • 7.25 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* @file Internal holiday definition class supporting multiple calculation types.
*
* Encapsulates holiday calculation logic for fixed dates, Easter-relative dates,
* and custom algorithms. Provides validation and Holiday instance generation.
*/
import { Holiday } from "./holiday.js";
/**
* @typedef {Object} CalculatedHoliday
* @property {string} name - Holiday name
* @property {Date} date - Holiday date
* @property {boolean} workFree - Whether this is a work-free holiday
* @property {'national' | 'regional'} scope - Holiday scope
* @property {'fixed' | 'easter_relative' | 'calculated'} type - Calculation type
*/
/**
* Internal holiday definition class that encapsulates holiday calculation logic and metadata.
*
* Supports fixed calendar dates, Easter-relative calculations, and custom algorithms.
* Each definition knows how to calculate its holiday date for any given year.
* Holiday definitions are immutable after creation and include validation.
*
* @example
* // Fixed date holiday definition
* const newYear = new HolidayDefinition({
* name: 'Neujahr',
* type: 'fixed',
* month: 1,
* day: 1,
* workFree: true
* });
*
* @example
* // Easter-relative holiday definition
* const goodFriday = new HolidayDefinition({
* name: 'Karfreitag',
* type: 'easter_relative',
* offset: -2,
* workFree: true
* });
*
* @example
* // Calculate actual holiday instances
* const newYearHoliday = newYear.calculateHoliday(2024);
* const goodFridayHoliday = goodFriday.calculateHoliday(2024, easterSunday);
*/
export class HolidayDefinition {
/**
* Create a new holiday definition.
*
* @param {Object} definition - Holiday definition object
* @param {string} definition.name - Holiday name
* @param {'fixed' | 'easter_relative' | 'calculated'} definition.type - Calculation type
* @param {boolean} definition.workFree - Whether this is a work-free holiday
* @param {number} [definition.month] - Month for fixed holidays (1-12)
* @param {number} [definition.day] - Day for fixed holidays
* @param {number} [definition.offset] - Days offset from Easter for relative holidays
* @param {Function} [definition.calculator] - Custom calculation function for complex holidays
* @throws {Error} Invalid holiday definition parameters
*/
constructor(definition) {
// Validate required fields
if (typeof definition.name !== "string" || definition.name.length === 0) {
throw new Error("Holiday name must be a non-empty string");
}
if (!["fixed", "easter_relative", "calculated"].includes(definition.type)) {
throw new Error(
"Holiday type must be 'fixed', 'easter_relative', or 'calculated'",
);
}
if (typeof definition.workFree !== "boolean") {
throw new Error("Holiday workFree must be a boolean");
}
// Type-specific validation
switch (definition.type) {
case "fixed":
if (
!Number.isInteger(definition.month) ||
definition.month < 1 ||
definition.month > 12
) {
throw new Error("Fixed holiday month must be integer 1-12");
}
if (
!Number.isInteger(definition.day) ||
definition.day < 1 ||
definition.day > 31
) {
throw new Error("Fixed holiday day must be integer 1-31");
}
break;
case "easter_relative":
if (!Number.isInteger(definition.offset)) {
throw new Error("Easter relative holiday offset must be an integer");
}
break;
case "calculated":
if (typeof definition.calculator !== "function") {
throw new Error("Calculated holiday must have a calculator function");
}
break;
}
// Assign properties (immutable after construction)
this.name = definition.name;
this.type = definition.type;
this.workFree = definition.workFree;
// Type-specific properties
if (definition.type === "fixed") {
this.month = definition.month;
this.day = definition.day;
} else if (definition.type === "easter_relative") {
this.offset = definition.offset;
} else if (definition.type === "calculated") {
this.calculator = definition.calculator;
}
// Freeze the object to prevent modification
Object.freeze(this);
}
/**
* Calculate the actual holiday for this definition in a given year.
*
* @param {number} year - Year to calculate for (must be >= 1583)
* @param {Date} [easterSunday] - Easter Sunday date (required for easter_relative holidays)
* @param {'national' | 'regional'} [scope='national'] - Holiday scope for result object
* @returns {Holiday} Calculated holiday instance
* @throws {Error} Invalid parameters or missing Easter date for relative holidays
*/
calculateHoliday(year, easterSunday = null, scope = "national") {
// Validate year
if (!Number.isInteger(year) || year < 1583) {
throw new Error("Year must be an integer >= 1583 (Gregorian calendar)");
}
// Validate scope
if (!["national", "regional"].includes(scope)) {
throw new Error("Scope must be 'national' or 'regional'");
}
let date;
switch (this.type) {
case "fixed":
// Fixed date holidays
date = new Date(Date.UTC(year, this.month - 1, this.day, 0, 0, 0, 0));
break;
case "easter_relative":
// Easter-relative holidays
if (!(easterSunday instanceof Date)) {
throw new Error(
"Easter Sunday date required for easter_relative holidays",
);
}
date = new Date(easterSunday.getTime());
date.setUTCDate(date.getUTCDate() + this.offset);
break;
case "calculated":
// Custom calculated holidays
date = this.calculator(year);
if (!(date instanceof Date)) {
throw new Error(
`Calculator function for ${this.name} must return a Date object`,
);
}
break;
// Note: default case removed - constructor ensures only valid types exist
}
return new Holiday(this.name, date, this.workFree, scope, this.type);
}
/**
* Get a string representation of this holiday definition.
*
* @returns {string} Holiday description
*/
toString() {
let details = `${this.name} (${this.type})`;
if (this.type === "fixed") {
details += ` - ${this.month}/${this.day}`;
} else if (this.type === "easter_relative") {
const sign = this.offset >= 0 ? "+" : "";
details += ` - Easter${sign}${this.offset}`;
}
details += this.workFree ? " [work-free]" : " [observance]";
return details;
}
/**
* Create a HolidayDefinition instance from a plain object definition.
* Convenience factory method for bulk creation.
*
* @param {any} definition - Holiday definition object
* @returns {HolidayDefinition} New HolidayDefinition instance
*/
static from(definition) {
return new HolidayDefinition(definition);
}
/**
* Validate a holiday definition object without creating an instance.
* Useful for batch validation.
*
* @param {any} definition - Holiday definition to validate
* @returns {boolean} True if definition is valid
*/
static isValidDefinition(definition) {
try {
new HolidayDefinition(definition);
return true;
} catch (_error) {
return false;
}
}
}