326 lines
11 KiB
TypeScript
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;
|
|
}
|