2 * @file CalendarLogic.ts
3 * @brief Business logic for holiday and date calculations
5 * @details Pure logic component for date mathematics and holiday detection with regional rules.
7 * @author Arnold Schiller <calendar@projektit.de>
9 * @copyright GPL-3.0-or-later
12 * Project IT Calendar - Business Logic Core (Holiday & Date Calculations)
13 * =======================================================================
15 * This module handles all date-related business logic, primarily focusing on:
16 * 1. Holiday calculation and regional holiday rules
17 * 2. Date mathematics and calendar algorithms
18 * 3. Pure business logic without UI or I/O dependencies
20 * IMPORTANT ARCHITECTURAL CONTEXT:
21 * ---------------------------------
22 * CalendarLogic is intentionally designed as a PURE LOGIC component:
23 * - NO UI dependencies (doesn't import St, Clutter, etc.)
24 * - NO I/O operations (except initial holiday data loading)
25 * - NO side effects (deterministic calculations only)
28 * - Easily testable (all methods are deterministic)
29 * - Reusable across different UI implementations
30 * - Independent of Cinnamon/GJS environment
32 * ------------------------------------------------------------------
33 * DATA SOURCES AND REGIONALIZATION:
34 * ------------------------------------------------------------------
35 * Holiday data is stored in JSON files in the holidays/ directory:
39 * de.json - Base German holidays (national)
40 * de-BY.json - Bavaria-specific holidays (regional)
41 * de-BE.json - Berlin-specific holidays
42 * en.json - English/International holidays
45 * Each JSON file follows this structure:
48 * "de": [ ...holiday rules... ],
49 * "de-BY": [ ...regional rules... ]
53 * Holiday rules use a compact format:
55 * "k": "f", // Type: 'f'=fixed, 'e'=easter-based, 'r'=relative
56 * "m": 1, // Month (1-12) for fixed dates
57 * "d": 1, // Day for fixed dates
58 * "o": -2, // Offset from Easter (for easter-based)
59 * "n": "New Year", // Holiday name
60 * "p": true, // Public holiday (true) or observance (false)
61 * "c": "year<=1994" // Optional condition for historical changes
64 * ------------------------------------------------------------------
66 * ------------------------------------------------------------------
67 * The system supports hierarchical regional holiday resolution:
69 * Example: User in Munich (Bavaria, Germany)
70 * 1. First checks "de-BY" (Bavaria-specific holidays)
71 * 2. Then checks "de" (German national holidays)
72 * 3. Combines both lists, removing duplicates
75 * - Regional specificity (Bavaria has different holidays than Berlin)
76 * - Fallback to national holidays
77 * - No code changes needed for new regions (just add JSON files)
79 * ------------------------------------------------------------------
80 * USAGE BY OTHER COMPONENTS:
81 * ------------------------------------------------------------------
82 * CalendarLogic is used by:
84 * 1. applet.ts (Header Display):
85 * - setHeaderDate() calls getHolidaysForDate() to show holiday names
86 * - Displays holidays in the popup header
88 * 2. CalendarView.ts (Month Grid):
89 * - renderMonthView() calls getHolidaysForDate() for each cell
90 * - Highlights holiday cells with special CSS classes
91 * - Shows holiday tooltips on hover
93 * 3. CalendarDay.ts (Type Definitions):
94 * - Provides TypeScript interfaces used here
95 * - Ensures type safety across date calculations
97 * ------------------------------------------------------------------
99 * ------------------------------------------------------------------
100 * 1. HYBRID MODULE SYSTEM:
101 * Uses 'export' for AMD/UMD compatibility and 'global' assignment for
102 * monolithic bundling (outFile). This satisfies both Cinnamon's
103 * internal requireModule and tsc's 'None' module setting.
105 * 2. GJS COMPATIBILITY:
106 * Uses native GLib for file operations instead of Node.js 'fs',
107 * ensuring it runs inside the Cinnamon/SpiderMonkey environment.
110 * Imports types from CalendarDay.ts. Note that in 'None' mode,
111 * these imports are purely for the compiler and emit no JS code.
113 * ------------------------------------------------------------------
114 * @author Arnold Schiller <calendar@projektit.de>
115 * @link https://github.com/ArnoldSchiller/calendar
116 * @link https://projektit.de/kalender
117 * @license GPL-3.0-or-later
120/* ================================================================
121 * TYPE IMPORTS (TypeScript Only - Stripped at Runtime)
122 * ================================================================
124 * These imports are ONLY for TypeScript type checking and development.
125 * In production (GJS runtime), they don't exist - hence the 'None' module mode.
127 * CalendarDay.ts defines interfaces for type-safe date handling.
130import { CalendarDay, DayType } from './CalendarDay';
132/* ================================================================
133 * GJS / CINNAMON IMPORTS
134 * ================================================================
136 * GLib is used for file operations and system locale detection.
137 * This is the ONLY external dependency, making CalendarLogic mostly
138 * environment-agnostic.
141const GLib = imports.gi.GLib;
143/* ================================================================
144 * HOLIDAY INTERFACE DEFINITION
145 * ================================================================
147 * Defines the structure of holiday rules in JSON files.
149 * WHY THIS COMPACT FORMAT?
150 * - Minimizes file size (important for applet performance)
151 * - Easy to parse and process
152 * - Human-readable for manual editing
155export interface Holiday {
158 * - 'f': Fixed date (e.g., Christmas on Dec 25)
159 * - 'e': Easter-based (e.g., Good Friday = Easter - 2)
160 * - 'r': Relative (e.g., "first Monday in September")
165 * Day of month (1-31) for fixed dates.
166 * Optional for non-fixed holidays.
171 * Month of year (1-12) for fixed dates.
172 * Optional for non-fixed holidays.
177 * Offset in days from Easter (for easter-based holidays).
178 * Example: -2 = Good Friday, +49 = Pentecost Monday
183 * Holiday name in local language.
184 * Displayed in UI headers and tooltips.
189 * Public holiday flag:
190 * - true: Official public holiday (banks/schools closed)
191 * - false: Observance or traditional holiday (normal work day)
196 * Optional condition for historical rule changes.
197 * Format: "year<=1994" or "year>=2000"
198 * Used for holidays that changed over time.
203/* ================================================================
204 * CALENDAR LOGIC CLASS
205 * ================================================================
207 * Main business logic class for all date and holiday calculations.
209 * PERFORMANCE CHARACTERISTICS:
210 * - Holiday data loaded ONCE at initialization
211 * - All calculations are O(1) or O(n) where n is small (< 100 rules)
212 * - No network calls or blocking I/O after initialization
216 * @class CalendarLogic
217 * @brief Main calendarlogic class
219 * @details For detailed documentation see the main class documentation.
222 * @class CalendarLogic
223 * @brief Main calendarlogic class
225 * @details For detailed documentation see the main class documentation.
227export class CalendarLogic {
229 * In-memory cache of holiday rules by region.
230 * Structure: { "de": [Holiday...], "de-BY": [Holiday...], ... }
232 * WHY CACHE IN MEMORY?
233 * - JSON files are small (< 10KB each)
234 * - Eliminates disk I/O during calendar navigation
235 * - Fast random access for date lookups
237 private holidayData: { [region: string]: Holiday[] } = {};
240 * Filesystem path to the applet directory.
241 * Used to locate holiday JSON files in holidays/ subdirectory.
243 private appletDir: string;
245 /* ============================================================
247 * ============================================================
249 * Initializes the logic component and loads holiday data.
251 * @param appletDir - Absolute path to applet directory (from metadata.path)
254 constructor(appletDir: string) {
255 this.appletDir = appletDir;
259 /* ============================================================
260 * HOLIDAY DATA LOADING
261 * ============================================================
263 * Loads holiday definitions based on system locale.
265 * LOCALE RESOLUTION LOGIC:
266 * 1. Get system locale (e.g., "de_DE.UTF-8")
267 * 2. Extract language code (e.g., "de")
268 * 3. Look for holidays/{lang}.json
269 * 4. Fallback: No holidays if file not found
271 * WHY AUTO-DETECT LOCALE?
272 * - Users don't need to configure their region
273 * - Works out-of-the-box for common locales
274 * - Can be extended with manual region selection
277 private loadHolidays() {
279 // Get system locale - GLib provides reliable locale detection
280 let locale = GLib.get_language_names()[0] || "en";
282 // Extract base language code (e.g., "de_DE" → "de")
283 let lang = locale.split('_')[0].split('.')[0].toLowerCase();
285 // Construct path to holiday definitions file
286 let filePath = `${this.appletDir}/holidays/${lang}.json`;
288 // Check if holiday file exists for this language
289 if (GLib.file_test(filePath, GLib.FileTest.EXISTS)) {
290 // Read file contents (synchronous - only done once at startup)
291 let [success, content] = GLib.file_get_contents(filePath);
294 // Parse JSON and extract regions data
295 let json = JSON.parse(content.toString());
296 this.holidayData = json.regions || {};
300 // Log error but don't crash - applet works without holidays
301 if (typeof global !== 'undefined') {
302 global.log(`[CalendarLogic] Error loading holidays: ${e}`);
307 /* ============================================================
308 * PUBLIC API: GET HOLIDAYS FOR DATE
309 * ============================================================
311 * Primary public method used by UI components.
312 * Returns all holidays for a specific date and region.
314 * REGIONAL RESOLUTION LOGIC:
315 * 1. Get region-specific rules (e.g., "de-BY" for Bavaria)
316 * 2. Get base language rules (e.g., "de" for Germany)
317 * 3. Combine both lists, removing duplicates
319 * @param date - JavaScript Date object to check
320 * @param region - Regional code (e.g., "de-BY"). Defaults to "de"
321 * @returns Array of unique holiday names (empty if no holidays)
324 * - getHolidaysForDate(new Date(2026-01-01), "de") → ["New Year"]
325 * - getHolidaysForDate(new Date(2026-10-03), "de") → ["German Unity Day"]
326 * - getHolidaysForDate(new Date(2026-12-25), "de") → ["Christmas Day"]
329 public getHolidaysForDate(date: Date, region: string = "de"): string[] {
330 let dayHolidays: string[] = [];
332 // Get regional rules (e.g., Bavaria-specific)
333 let rules = this.holidayData[region] || [];
335 // Get base language rules (e.g., German national)
336 let baseRegion = region.split('-')[0];
337 let baseRules = this.holidayData[baseRegion] || [];
339 // Combine regional + base rules (regional takes precedence)
340 let allRules = rules.concat(baseRules);
342 // Check each rule against the target date
343 for (let rule of allRules) {
344 if (this.isHolidayMatch(rule, date)) {
345 dayHolidays.push(rule.n);
351 * Using a Set ensures that if a holiday (like "New Year") is defined in
352 * both the base and regional rules, it only appears once in the UI.
354 * WHY USE SET INSTEAD OF ARRAY CHECKS?
356 * - Better performance for small arrays
357 * - Maintains insertion order
359 return [...new Set(dayHolidays)];
362 /* ============================================================
363 * HOLIDAY RULE MATCHING ENGINE
364 * ============================================================
366 * Core matching logic for different holiday types.
368 * SUPPORTED HOLIDAY TYPES:
369 * 1. Fixed dates (k='f'): Christmas, New Year, etc.
370 * 2. Easter-based (k='e'): Good Friday, Pentecost, etc.
371 * 3. Relative dates (k='r'): Not implemented in current version
373 * @param rule - Holiday rule definition from JSON
374 * @param date - Date to check against
375 * @returns true if date matches the holiday rule
378 private isHolidayMatch(rule: Holiday, date: Date): boolean {
379 const year = date.getFullYear();
380 const month = date.getMonth() + 1; // Convert JS 0-index to 1-index
381 const day = date.getDate();
383 // Check year-based conditions first (e.g., historical changes)
384 if (rule.c && !this.checkCondition(rule.c, year)) {
388 // Fixed date holiday (e.g., Christmas on Dec 25)
389 if (rule.k === 'f') {
390 return rule.m === month && rule.d === day;
393 // Easter-based holiday (e.g., Good Friday = Easter - 2)
394 if (rule.k === 'e') {
395 let easter = this.getEaster(year);
396 let target = new Date(easter);
397 target.setDate(easter.getDate() + (rule.o || 0));
398 return target.getMonth() + 1 === month && target.getDate() === day;
401 // Relative date holiday (not yet implemented)
402 if (rule.k === 'r') {
403 // Future enhancement: "first Monday in September" etc.
410 /* ============================================================
412 * ============================================================
414 * Parses simple year-based conditions for historical accuracy.
417 * - "year<=1994" (holiday existed until 1994)
418 * - "year>=2000" (holiday started in 2000)
419 * - "year==1990" (holiday only in 1990)
421 * WHY NEED CONDITIONS?
422 * - Holidays change over time (political/religious)
423 * - German reunification (1990) changed many holidays
424 * - Some holidays were added/removed by law
426 * @param cond - Condition string from JSON
427 * @param year - Year to check
428 * @returns true if condition is satisfied
431 private checkCondition(cond: string, year: number): boolean {
432 const match = cond.match(/year([<>=!]+)(\d+)/);
433 if (!match || !match[1] || !match[2]) return true;
435 const operator = match[1];
436 const val = parseInt(match[2]);
439 case "<=": return year <= val;
440 case ">=": return year >= val;
441 case "==": return year === val;
442 case "<": return year < val;
443 case ">": return year > val;
444 default: return true;
448 /* ============================================================
449 * EASTER CALCULATION ALGORITHM
450 * ============================================================
452 * Calculates Easter Sunday date using the Meeus/Jones/Butcher algorithm.
453 * This is a refinement of Gauss's original algorithm.
455 * WHY IS EASTER COMPLICATED?
456 * - Based on lunar calendar (first Sunday after first full moon after vernal equinox)
457 * - Different churches use different calculations
458 * - This algorithm works for Gregorian calendar years 1583-4099
461 * Jean Meeus, "Astronomical Algorithms", 2nd edition, 1998
463 * @param year - Gregorian calendar year
464 * @returns Date object for Easter Sunday
467 private getEaster(year: number): Date {
468 // Step 1: Golden number - position in 19-year Metonic cycle
471 // Step 2: Century number
472 let b = Math.floor(year / 100);
474 // Step 3: Years within century
477 // Step 4: Leap year corrections
478 let d = Math.floor(b / 4);
481 // Step 5: Correction for 30-year cycle
482 let f = Math.floor((b + 8) / 25);
484 // Step 6: Moon orbit correction
485 let g = Math.floor((b - f + 1) / 3);
487 // Step 7: Epact (age of moon on Jan 1)
488 let h = (19 * a + b - d - g + 15) % 30;
490 // Step 8: Weekday corrections
491 let i = Math.floor(c / 4);
494 // Step 9: Correction for 7-day week
495 let l = (32 + 2 * e + 2 * i - h - k) % 7;
497 // Step 10: Final calculation for full moon
498 let m = Math.floor((a + 11 * h + 22 * l) / 451);
500 // Step 11: Month and day calculation
501 let n = h + l - 7 * m + 114;
502 let month = Math.floor(n / 31);
503 let day = (n % 31) + 1;
505 // Return Easter Sunday date (month is 1-indexed in algorithm)
506 return new Date(year, month - 1, day);
510/* ================================================================
511 * HYBRID EXPORT SYSTEM
512 * ================================================================
514 * CRITICAL DUAL EXPORT PATTERN:
516 * Cinnamon applets require BOTH export styles:
518 * 1. CommonJS/ES6 Export (Development):
519 * - Used by TypeScript compiler
520 * - Used by module bundlers
521 * - Enables IDE auto-completion
523 * 2. Global Export (Production):
524 * - Cinnamon uses requireModule() which expects global assignment
525 * - No module system at runtime in GJS
526 * - Other files access via global.CalendarLogic
528 * WARNING: Removing either export will break the applet!
531/* ----------------------------------------------------------------
532 * CommonJS/ES6 Export (Development & TypeScript)
533 * ----------------------------------------------------------------
535if (typeof exports !== 'undefined') {
536 exports.CalendarLogic = CalendarLogic;
539/* ----------------------------------------------------------------
540 * Global Export (Cinnamon Runtime)
541 * ----------------------------------------------------------------
543(global as any).CalendarLogic = CalendarLogic;
545/* ================================================================
546 * TODOs AND FUTURE ENHANCEMENTS
547 * ================================================================
549 * TODO: Add support for relative date holidays (k='r'):
550 * - "first Monday in September" (Labor Day in US)
551 * - "last Monday in May" (Memorial Day in US)
553 * TODO: Add lunar calendar calculations for:
554 * - Islamic holidays (Hijri calendar)
555 * - Hebrew holidays (Jewish calendar)
556 * - Chinese holidays (Lunisolar calendar)
558 * TODO: Implement holiday caching by year for performance:
559 * - Pre-calculate all holidays for current year
560 * - Cache results for frequently accessed dates
562 * TODO: Add support for user-defined holiday regions:
563 * - Allow users to select region in settings
564 * - Support custom holiday JSON files
566 * TODO: Extend condition system with logical operators:
567 * - Support AND/OR in condition strings
568 * - Add support for day-of-week conditions