Files
CapaKraken/packages/db/src/holiday-calendar-seed-data.ts
T

326 lines
11 KiB
TypeScript

import { getPublicHolidays } from "@capakraken/shared";
export type HolidayCalendarSeedScope = "COUNTRY" | "STATE" | "CITY";
export type HolidayCalendarSeedEntry = {
date: string;
name: string;
isRecurringAnnual: boolean;
};
export type HolidayCalendarSeedDefinition = {
name: string;
scopeType: HolidayCalendarSeedScope;
countryCode: string;
stateCode?: string;
cityName?: string;
priority: number;
entries: HolidayCalendarSeedEntry[];
};
type SeedContext = {
availableCountryCodes: string[];
availableCitiesByCountry: Record<string, string[]>;
activeGermanStates?: string[];
years: number[];
};
type SimpleHolidayDefinition = {
name: string;
resolveDate: (year: number) => string;
};
const COUNTRY_PRIORITY = 10;
const STATE_PRIORITY = 20;
const CITY_PRIORITY = 30;
function toIsoDate(date: Date): string {
return date.toISOString().slice(0, 10);
}
function dateUtc(year: number, month: number, day: number): Date {
return new Date(Date.UTC(year, month - 1, day));
}
function addDays(date: Date, amount: number): Date {
const next = new Date(date);
next.setUTCDate(next.getUTCDate() + amount);
return next;
}
function computeEasterSunday(year: number): Date {
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);
const day = ((h + l - 7 * m + 114) % 31) + 1;
return dateUtc(year, month, day);
}
function nthWeekdayOfMonth(year: number, month: number, weekday: number, occurrence: number): string {
const firstDay = dateUtc(year, month, 1);
const delta = (weekday - firstDay.getUTCDay() + 7) % 7;
const day = 1 + delta + ((occurrence - 1) * 7);
return toIsoDate(dateUtc(year, month, day));
}
function lastWeekdayOfMonth(year: number, month: number, weekday: number): string {
const lastDay = dateUtc(year, month + 1, 0);
const delta = (lastDay.getUTCDay() - weekday + 7) % 7;
lastDay.setUTCDate(lastDay.getUTCDate() - delta);
return toIsoDate(lastDay);
}
function observedUsFixedHoliday(year: number, month: number, day: number): string {
const holiday = dateUtc(year, month, day);
const weekday = holiday.getUTCDay();
if (weekday === 6) {
return toIsoDate(addDays(holiday, -1));
}
if (weekday === 0) {
return toIsoDate(addDays(holiday, 1));
}
return toIsoDate(holiday);
}
function buildEntries(years: number[], definitions: SimpleHolidayDefinition[]): HolidayCalendarSeedEntry[] {
const entries = new Map<string, HolidayCalendarSeedEntry>();
for (const year of years) {
for (const definition of definitions) {
const date = definition.resolveDate(year);
entries.set(date, {
date,
name: definition.name,
isRecurringAnnual: false,
});
}
}
return [...entries.values()].sort((left, right) => left.date.localeCompare(right.date));
}
function buildGermanCountryEntries(years: number[]): HolidayCalendarSeedEntry[] {
return buildEntries(
years,
years.flatMap((year) =>
getPublicHolidays(year)
.filter((holiday) => holiday.federal)
.map((holiday) => ({
name: holiday.name,
resolveDate: () => holiday.date,
})),
),
);
}
function buildGermanStateEntries(years: number[], stateCode: string): HolidayCalendarSeedEntry[] {
return buildEntries(
years,
years.flatMap((year) =>
getPublicHolidays(year, stateCode)
.filter((holiday) => !holiday.federal)
.map((holiday) => ({
name: holiday.name,
resolveDate: () => holiday.date,
})),
),
);
}
function buildSpanishCountryEntries(years: number[]): HolidayCalendarSeedEntry[] {
return buildEntries(years, [
{ name: "Ano Nuevo", resolveDate: (year) => toIsoDate(dateUtc(year, 1, 1)) },
{ name: "Epifania del Senor", resolveDate: (year) => toIsoDate(dateUtc(year, 1, 6)) },
{ name: "Viernes Santo", resolveDate: (year) => toIsoDate(addDays(computeEasterSunday(year), -2)) },
{ name: "Fiesta del Trabajo", resolveDate: (year) => toIsoDate(dateUtc(year, 5, 1)) },
{ name: "Asuncion de la Virgen", resolveDate: (year) => toIsoDate(dateUtc(year, 8, 15)) },
{ name: "Fiesta Nacional de Espana", resolveDate: (year) => toIsoDate(dateUtc(year, 10, 12)) },
{ name: "Todos los Santos", resolveDate: (year) => toIsoDate(dateUtc(year, 11, 1)) },
{ name: "Dia de la Constitucion", resolveDate: (year) => toIsoDate(dateUtc(year, 12, 6)) },
{ name: "Inmaculada Concepcion", resolveDate: (year) => toIsoDate(dateUtc(year, 12, 8)) },
{ name: "Navidad", resolveDate: (year) => toIsoDate(dateUtc(year, 12, 25)) },
]);
}
function buildIndianCountryEntries(years: number[]): HolidayCalendarSeedEntry[] {
return buildEntries(years, [
{ name: "Republic Day", resolveDate: (year) => toIsoDate(dateUtc(year, 1, 26)) },
{ name: "Good Friday", resolveDate: (year) => toIsoDate(addDays(computeEasterSunday(year), -2)) },
{ name: "Independence Day", resolveDate: (year) => toIsoDate(dateUtc(year, 8, 15)) },
{ name: "Gandhi Jayanti", resolveDate: (year) => toIsoDate(dateUtc(year, 10, 2)) },
]);
}
function buildUsCountryEntries(years: number[]): HolidayCalendarSeedEntry[] {
return buildEntries(years, [
{ name: "New Year's Day", resolveDate: (year) => observedUsFixedHoliday(year, 1, 1) },
{ name: "Martin Luther King Jr. Day", resolveDate: (year) => nthWeekdayOfMonth(year, 1, 1, 3) },
{ name: "Memorial Day", resolveDate: (year) => lastWeekdayOfMonth(year, 5, 1) },
{ name: "Independence Day", resolveDate: (year) => observedUsFixedHoliday(year, 7, 4) },
{ name: "Labor Day", resolveDate: (year) => nthWeekdayOfMonth(year, 9, 1, 1) },
{ name: "Thanksgiving Day", resolveDate: (year) => nthWeekdayOfMonth(year, 11, 4, 4) },
{ name: "Christmas Day", resolveDate: (year) => observedUsFixedHoliday(year, 12, 25) },
]);
}
function normalizeCountryCodes(countryCodes: string[]): Set<string> {
return new Set(countryCodes.map((countryCode) => countryCode.trim().toUpperCase()));
}
function normalizeCityLookup(availableCitiesByCountry: Record<string, string[]>): Map<string, Set<string>> {
const lookup = new Map<string, Set<string>>();
for (const [countryCode, cityNames] of Object.entries(availableCitiesByCountry)) {
lookup.set(countryCode.trim().toUpperCase(), new Set(cityNames));
}
return lookup;
}
function hasCity(cityLookup: Map<string, Set<string>>, countryCode: string, cityName: string): boolean {
return cityLookup.get(countryCode)?.has(cityName) ?? false;
}
function germanStateDisplayName(stateCode: string): string {
switch (stateCode) {
case "BW":
return "Baden-Wuerttemberg";
case "BY":
return "Bayern";
case "HH":
return "Hamburg";
case "NW":
return "Nordrhein-Westfalen";
default:
return stateCode;
}
}
function buildCityEntries(years: number[], definitions: SimpleHolidayDefinition[]): HolidayCalendarSeedEntry[] {
return buildEntries(years, definitions);
}
export function buildHolidayCalendarSeedDefinitions(
context: SeedContext,
): HolidayCalendarSeedDefinition[] {
const availableCountries = normalizeCountryCodes(context.availableCountryCodes);
const cityLookup = normalizeCityLookup(context.availableCitiesByCountry);
const definitions: HolidayCalendarSeedDefinition[] = [];
if (availableCountries.has("DE")) {
definitions.push({
name: "Referenzfeiertage Deutschland 2026-2027",
scopeType: "COUNTRY",
countryCode: "DE",
priority: COUNTRY_PRIORITY,
entries: buildGermanCountryEntries(context.years),
});
const germanStates = new Set(
[...(context.activeGermanStates ?? []), "BY", "NW"]
.map((stateCode) => stateCode.trim().toUpperCase())
.filter((stateCode) => ["BW", "BY", "HH", "NW"].includes(stateCode)),
);
for (const stateCode of [...germanStates].sort()) {
const entries = buildGermanStateEntries(context.years, stateCode);
if (entries.length === 0) {
continue;
}
definitions.push({
name: `Referenzfeiertage Deutschland - ${germanStateDisplayName(stateCode)} 2026-2027`,
scopeType: "STATE",
countryCode: "DE",
stateCode,
priority: STATE_PRIORITY,
entries,
});
}
if (hasCity(cityLookup, "DE", "Augsburg")) {
definitions.push({
name: "Referenzfeiertage Deutschland - Augsburg 2026-2027",
scopeType: "CITY",
countryCode: "DE",
cityName: "Augsburg",
priority: CITY_PRIORITY,
entries: buildCityEntries(context.years, [
{ name: "Augsburger Friedensfest", resolveDate: (year) => toIsoDate(dateUtc(year, 8, 8)) },
]),
});
}
}
if (availableCountries.has("ES")) {
definitions.push({
name: "Referenzfeiertage Spanien 2026-2027",
scopeType: "COUNTRY",
countryCode: "ES",
priority: COUNTRY_PRIORITY,
entries: buildSpanishCountryEntries(context.years),
});
if (hasCity(cityLookup, "ES", "Madrid")) {
definitions.push({
name: "Referenzfeiertage Spanien - Madrid 2026-2027",
scopeType: "CITY",
countryCode: "ES",
cityName: "Madrid",
priority: CITY_PRIORITY,
entries: buildEntries(context.years, [
{ name: "San Isidro", resolveDate: (year) => toIsoDate(dateUtc(year, 5, 15)) },
{ name: "Nuestra Senora de la Almudena", resolveDate: (year) => toIsoDate(dateUtc(year, 11, 9)) },
]),
});
}
if (hasCity(cityLookup, "ES", "Barcelona")) {
definitions.push({
name: "Referenzfeiertage Spanien - Barcelona 2026-2027",
scopeType: "CITY",
countryCode: "ES",
cityName: "Barcelona",
priority: CITY_PRIORITY,
entries: buildEntries(context.years, [
{ name: "La Merce", resolveDate: (year) => toIsoDate(dateUtc(year, 9, 24)) },
{ name: "Santa Eulalia", resolveDate: (year) => toIsoDate(dateUtc(year, 2, 12)) },
]),
});
}
}
if (availableCountries.has("IN")) {
definitions.push({
name: "Referenzfeiertage Indien 2026-2027",
scopeType: "COUNTRY",
countryCode: "IN",
priority: COUNTRY_PRIORITY,
entries: buildIndianCountryEntries(context.years),
});
}
if (availableCountries.has("US")) {
definitions.push({
name: "Referenzfeiertage USA 2026-2027",
scopeType: "COUNTRY",
countryCode: "US",
priority: COUNTRY_PRIORITY,
entries: buildUsCountryEntries(context.years),
});
}
return definitions;
}