refraction almost completed
This commit is contained in:
472
app/ui-holidays-data.js
Normal file
472
app/ui-holidays-data.js
Normal file
@@ -0,0 +1,472 @@
|
||||
/* ui-holidays-data.js - Holiday data and date resolution helpers */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const HEBREW_MONTH_ALIAS_BY_ID = {
|
||||
nisan: ["nisan"],
|
||||
iyar: ["iyar"],
|
||||
sivan: ["sivan"],
|
||||
tammuz: ["tamuz", "tammuz"],
|
||||
av: ["av"],
|
||||
elul: ["elul"],
|
||||
tishrei: ["tishri", "tishrei"],
|
||||
cheshvan: ["heshvan", "cheshvan", "marcheshvan"],
|
||||
kislev: ["kislev"],
|
||||
tevet: ["tevet"],
|
||||
shvat: ["shevat", "shvat"],
|
||||
adar: ["adar", "adar i", "adar 1"],
|
||||
"adar-ii": ["adar ii", "adar 2"]
|
||||
};
|
||||
|
||||
const MONTH_NAME_TO_INDEX = {
|
||||
january: 0,
|
||||
february: 1,
|
||||
march: 2,
|
||||
april: 3,
|
||||
may: 4,
|
||||
june: 5,
|
||||
july: 6,
|
||||
august: 7,
|
||||
september: 8,
|
||||
october: 9,
|
||||
november: 10,
|
||||
december: 11
|
||||
};
|
||||
|
||||
const GREGORIAN_MONTH_ID_TO_ORDER = {
|
||||
january: 1,
|
||||
february: 2,
|
||||
march: 3,
|
||||
april: 4,
|
||||
may: 5,
|
||||
june: 6,
|
||||
july: 7,
|
||||
august: 8,
|
||||
september: 9,
|
||||
october: 10,
|
||||
november: 11,
|
||||
december: 12
|
||||
};
|
||||
|
||||
function normalizeCalendarText(value) {
|
||||
return String(value || "")
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/['`]/g, "")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function readNumericPart(parts, partType) {
|
||||
const raw = parts.find((part) => part.type === partType)?.value;
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const digits = String(raw).replace(/[^0-9]/g, "");
|
||||
if (!digits) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = Number(digits);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function getGregorianMonthOrderFromId(monthId) {
|
||||
if (!monthId) {
|
||||
return null;
|
||||
}
|
||||
const key = String(monthId).trim().toLowerCase();
|
||||
const value = GREGORIAN_MONTH_ID_TO_ORDER[key];
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function parseMonthDayStartToken(token) {
|
||||
const match = String(token || "").match(/(\d{2})-(\d{2})/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const month = Number(match[1]);
|
||||
const day = Number(match[2]);
|
||||
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { month, day };
|
||||
}
|
||||
|
||||
function createDateAtNoon(year, monthIndex, dayOfMonth) {
|
||||
return new Date(Math.trunc(year), monthIndex, Math.trunc(dayOfMonth), 12, 0, 0, 0);
|
||||
}
|
||||
|
||||
function computeWesternEasterDate(year) {
|
||||
const y = Math.trunc(Number(year));
|
||||
if (!Number.isFinite(y)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const a = y % 19;
|
||||
const b = Math.floor(y / 100);
|
||||
const c = y % 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 createDateAtNoon(y, month - 1, day);
|
||||
}
|
||||
|
||||
function computeNthWeekdayOfMonth(year, monthIndex, weekday, ordinal) {
|
||||
const y = Math.trunc(Number(year));
|
||||
if (!Number.isFinite(y)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const first = createDateAtNoon(y, monthIndex, 1);
|
||||
const firstWeekday = first.getDay();
|
||||
const offset = (weekday - firstWeekday + 7) % 7;
|
||||
const dayOfMonth = 1 + offset + (Math.trunc(ordinal) - 1) * 7;
|
||||
const daysInMonth = new Date(y, monthIndex + 1, 0).getDate();
|
||||
if (dayOfMonth > daysInMonth) {
|
||||
return null;
|
||||
}
|
||||
return createDateAtNoon(y, monthIndex, dayOfMonth);
|
||||
}
|
||||
|
||||
function resolveGregorianDateRule(rule, selectedYear) {
|
||||
const key = String(rule || "").trim().toLowerCase();
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key === "gregorian-easter-sunday") {
|
||||
return computeWesternEasterDate(selectedYear);
|
||||
}
|
||||
|
||||
if (key === "gregorian-good-friday") {
|
||||
const easter = computeWesternEasterDate(selectedYear);
|
||||
if (!(easter instanceof Date) || Number.isNaN(easter.getTime())) {
|
||||
return null;
|
||||
}
|
||||
return createDateAtNoon(easter.getFullYear(), easter.getMonth(), easter.getDate() - 2);
|
||||
}
|
||||
|
||||
if (key === "gregorian-thanksgiving-us") {
|
||||
return computeNthWeekdayOfMonth(selectedYear, 10, 4, 4);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseFirstMonthDayFromText(dateText) {
|
||||
const text = String(dateText || "").replace(/~/g, " ");
|
||||
const firstSegment = text.split("/")[0] || text;
|
||||
const match = firstSegment.match(/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2})/i);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const monthIndex = MONTH_NAME_TO_INDEX[String(match[1]).toLowerCase()];
|
||||
const day = Number(match[2]);
|
||||
if (!Number.isFinite(monthIndex) || !Number.isFinite(day)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { monthIndex, day };
|
||||
}
|
||||
|
||||
function findHebrewMonthDayInGregorianYear(monthId, day, year) {
|
||||
const aliases = HEBREW_MONTH_ALIAS_BY_ID[String(monthId || "").toLowerCase()] || [];
|
||||
const targetDay = Number(day);
|
||||
if (!aliases.length || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedAliases = aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean);
|
||||
const formatter = new Intl.DateTimeFormat("en-u-ca-hebrew", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric"
|
||||
});
|
||||
|
||||
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||||
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||||
|
||||
while (cursor.getTime() <= end.getTime()) {
|
||||
const parts = formatter.formatToParts(cursor);
|
||||
const currentDay = readNumericPart(parts, "day");
|
||||
const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value);
|
||||
if (currentDay === Math.trunc(targetDay) && normalizedAliases.includes(monthName)) {
|
||||
return new Date(cursor);
|
||||
}
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getIslamicMonthOrderById(monthId, calendarData) {
|
||||
const month = (calendarData?.islamic || []).find((item) => item?.id === monthId);
|
||||
const order = Number(month?.order);
|
||||
return Number.isFinite(order) ? Math.trunc(order) : null;
|
||||
}
|
||||
|
||||
function findIslamicMonthDayInGregorianYear(monthId, day, year, calendarData) {
|
||||
const monthOrder = getIslamicMonthOrderById(monthId, calendarData);
|
||||
const targetDay = Number(day);
|
||||
if (!Number.isFinite(monthOrder) || !Number.isFinite(targetDay) || !Number.isFinite(year)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatter = new Intl.DateTimeFormat("en-u-ca-islamic", {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric"
|
||||
});
|
||||
|
||||
const cursor = new Date(Math.trunc(year), 0, 1, 12, 0, 0, 0);
|
||||
const end = new Date(Math.trunc(year), 11, 31, 12, 0, 0, 0);
|
||||
|
||||
while (cursor.getTime() <= end.getTime()) {
|
||||
const parts = formatter.formatToParts(cursor);
|
||||
const currentDay = readNumericPart(parts, "day");
|
||||
const currentMonth = readNumericPart(parts, "month");
|
||||
if (currentDay === Math.trunc(targetDay) && currentMonth === monthOrder) {
|
||||
return new Date(cursor);
|
||||
}
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveHolidayGregorianDate(holiday, options = {}) {
|
||||
if (!holiday || typeof holiday !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedYear = Number(options.selectedYear);
|
||||
const calendarData = options.calendarData || {};
|
||||
const calendarId = String(holiday.calendarId || "").trim().toLowerCase();
|
||||
const monthId = String(holiday.monthId || "").trim().toLowerCase();
|
||||
const day = Number(holiday.day);
|
||||
|
||||
if (calendarId === "gregorian") {
|
||||
if (holiday?.dateRule) {
|
||||
const ruledDate = resolveGregorianDateRule(holiday.dateRule, selectedYear);
|
||||
if (ruledDate) {
|
||||
return ruledDate;
|
||||
}
|
||||
}
|
||||
|
||||
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseMonthDayStartToken(holiday.dateText);
|
||||
if (monthDay) {
|
||||
return new Date(selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
||||
}
|
||||
const order = getGregorianMonthOrderFromId(monthId);
|
||||
if (Number.isFinite(order) && Number.isFinite(day)) {
|
||||
return new Date(selectedYear, order - 1, Math.trunc(day), 12, 0, 0, 0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (calendarId === "hebrew") {
|
||||
return findHebrewMonthDayInGregorianYear(monthId, day, selectedYear);
|
||||
}
|
||||
|
||||
if (calendarId === "islamic") {
|
||||
return findIslamicMonthDayInGregorianYear(monthId, day, selectedYear, calendarData);
|
||||
}
|
||||
|
||||
if (calendarId === "wheel-of-year") {
|
||||
const monthDay = parseMonthDayStartToken(holiday.monthDayStart) || parseFirstMonthDayFromText(holiday.dateText);
|
||||
if (monthDay?.month && monthDay?.day) {
|
||||
return new Date(selectedYear, monthDay.month - 1, monthDay.day, 12, 0, 0, 0);
|
||||
}
|
||||
if (monthDay?.monthIndex != null && monthDay?.day) {
|
||||
return new Date(selectedYear, monthDay.monthIndex, monthDay.day, 12, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatGregorianReferenceDate(date) {
|
||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
return date.toLocaleDateString(undefined, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
});
|
||||
}
|
||||
|
||||
function formatCalendarDateFromGregorian(date, calendarId) {
|
||||
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
||||
return "--";
|
||||
}
|
||||
|
||||
const locale = calendarId === "hebrew"
|
||||
? "en-u-ca-hebrew"
|
||||
: (calendarId === "islamic" ? "en-u-ca-islamic" : "en");
|
||||
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function buildPlanetMap(planetsObj) {
|
||||
const map = new Map();
|
||||
if (!planetsObj || typeof planetsObj !== "object") {
|
||||
return map;
|
||||
}
|
||||
|
||||
Object.values(planetsObj).forEach((planet) => {
|
||||
if (!planet?.id) {
|
||||
return;
|
||||
}
|
||||
map.set(planet.id, planet);
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function buildSignsMap(signs) {
|
||||
const map = new Map();
|
||||
if (!Array.isArray(signs)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
signs.forEach((sign) => {
|
||||
if (!sign?.id) {
|
||||
return;
|
||||
}
|
||||
map.set(sign.id, sign);
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function buildGodsMap(magickDataset) {
|
||||
const gods = magickDataset?.grouped?.gods?.gods;
|
||||
const map = new Map();
|
||||
|
||||
if (!Array.isArray(gods)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
gods.forEach((god) => {
|
||||
if (!god?.id) {
|
||||
return;
|
||||
}
|
||||
map.set(god.id, god);
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function buildHebrewMap(magickDataset) {
|
||||
const map = new Map();
|
||||
const letters = magickDataset?.grouped?.alphabets?.hebrew;
|
||||
if (!Array.isArray(letters)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
letters.forEach((letter) => {
|
||||
if (!letter?.hebrewLetterId) {
|
||||
return;
|
||||
}
|
||||
map.set(letter.hebrewLetterId, letter);
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function buildCalendarData(referenceData) {
|
||||
return {
|
||||
gregorian: Array.isArray(referenceData?.calendarMonths) ? referenceData.calendarMonths : [],
|
||||
hebrew: Array.isArray(referenceData?.hebrewCalendar?.months) ? referenceData.hebrewCalendar.months : [],
|
||||
islamic: Array.isArray(referenceData?.islamicCalendar?.months) ? referenceData.islamicCalendar.months : [],
|
||||
"wheel-of-year": Array.isArray(referenceData?.wheelOfYear?.months) ? referenceData.wheelOfYear.months : []
|
||||
};
|
||||
}
|
||||
|
||||
function calendarLabel(calendarId) {
|
||||
const key = String(calendarId || "").trim().toLowerCase();
|
||||
if (key === "hebrew") return "Hebrew";
|
||||
if (key === "islamic") return "Islamic";
|
||||
if (key === "wheel-of-year") return "Wheel of the Year";
|
||||
return "Gregorian";
|
||||
}
|
||||
|
||||
function monthLabelForCalendar(calendarData, calendarId, monthId) {
|
||||
const months = calendarData?.[calendarId];
|
||||
if (!Array.isArray(months)) {
|
||||
return monthId || "--";
|
||||
}
|
||||
|
||||
const month = months.find((entry) => String(entry?.id || "").toLowerCase() === String(monthId || "").toLowerCase());
|
||||
return month?.name || monthId || "--";
|
||||
}
|
||||
|
||||
function normalizeSourceFilter(value) {
|
||||
const key = String(value || "").trim().toLowerCase();
|
||||
if (key === "gregorian" || key === "hebrew" || key === "islamic" || key === "wheel-of-year") {
|
||||
return key;
|
||||
}
|
||||
return "all";
|
||||
}
|
||||
|
||||
function buildAllHolidays(referenceData) {
|
||||
if (Array.isArray(referenceData?.calendarHolidays) && referenceData.calendarHolidays.length) {
|
||||
return [...referenceData.calendarHolidays].sort((left, right) => {
|
||||
const calCmp = calendarLabel(left?.calendarId).localeCompare(calendarLabel(right?.calendarId));
|
||||
if (calCmp !== 0) return calCmp;
|
||||
|
||||
const leftDay = Number(left?.day);
|
||||
const rightDay = Number(right?.day);
|
||||
if (Number.isFinite(leftDay) && Number.isFinite(rightDay) && leftDay !== rightDay) {
|
||||
return leftDay - rightDay;
|
||||
}
|
||||
|
||||
return String(left?.name || "").localeCompare(String(right?.name || ""));
|
||||
});
|
||||
}
|
||||
|
||||
const legacy = Array.isArray(referenceData?.celestialHolidays) ? referenceData.celestialHolidays : [];
|
||||
return legacy.map((holiday) => ({
|
||||
...holiday,
|
||||
calendarId: "gregorian",
|
||||
dateText: holiday?.date || holiday?.dateRange || ""
|
||||
}));
|
||||
}
|
||||
|
||||
window.HolidayDataUi = {
|
||||
buildAllHolidays,
|
||||
buildCalendarData,
|
||||
buildGodsMap,
|
||||
buildHebrewMap,
|
||||
buildPlanetMap,
|
||||
buildSignsMap,
|
||||
calendarLabel,
|
||||
formatCalendarDateFromGregorian,
|
||||
formatGregorianReferenceDate,
|
||||
monthLabelForCalendar,
|
||||
normalizeSourceFilter,
|
||||
resolveHolidayGregorianDate
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user