651 lines
19 KiB
JavaScript
651 lines
19 KiB
JavaScript
|
|
(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
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let config = {};
|
|||
|
|
|
|||
|
|
function getSelectedYear() {
|
|||
|
|
return Number(config.getSelectedYear?.()) || new Date().getFullYear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getSelectedCalendar() {
|
|||
|
|
return String(config.getSelectedCalendar?.() || "gregorian").trim().toLowerCase();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getIslamicMonths() {
|
|||
|
|
return config.getIslamicMonths?.() || [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseMonthDayToken(token) {
|
|||
|
|
const [month, day] = String(token || "").split("-").map((part) => Number(part));
|
|||
|
|
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return { month, day };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function monthDayDate(monthDay, year) {
|
|||
|
|
const parsed = parseMonthDayToken(monthDay);
|
|||
|
|
if (!parsed) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return new Date(year, parsed.month - 1, parsed.day);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildSignDateBounds(sign) {
|
|||
|
|
const start = monthDayDate(sign?.start, 2025);
|
|||
|
|
const endBase = monthDayDate(sign?.end, 2025);
|
|||
|
|
if (!start || !endBase) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const wrapsYear = endBase.getTime() < start.getTime();
|
|||
|
|
const end = wrapsYear ? monthDayDate(sign?.end, 2026) : endBase;
|
|||
|
|
if (!end) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { start, end };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addDays(date, days) {
|
|||
|
|
const next = new Date(date);
|
|||
|
|
next.setDate(next.getDate() + days);
|
|||
|
|
return next;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatDateLabel(date) {
|
|||
|
|
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function monthDayOrdinal(month, day) {
|
|||
|
|
if (!Number.isFinite(month) || !Number.isFinite(day)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
const base = new Date(2025, Math.trunc(month) - 1, Math.trunc(day), 12, 0, 0, 0);
|
|||
|
|
if (Number.isNaN(base.getTime())) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
const start = new Date(2025, 0, 1, 12, 0, 0, 0);
|
|||
|
|
const diff = base.getTime() - start.getTime();
|
|||
|
|
return Math.floor(diff / (24 * 60 * 60 * 1000)) + 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isMonthDayInRange(targetMonth, targetDay, startMonth, startDay, endMonth, endDay) {
|
|||
|
|
const target = monthDayOrdinal(targetMonth, targetDay);
|
|||
|
|
const start = monthDayOrdinal(startMonth, startDay);
|
|||
|
|
const end = monthDayOrdinal(endMonth, endDay);
|
|||
|
|
if (!Number.isFinite(target) || !Number.isFinite(start) || !Number.isFinite(end)) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (end >= start) {
|
|||
|
|
return target >= start && target <= end;
|
|||
|
|
}
|
|||
|
|
return target >= start || target <= end;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseMonthDayTokensFromText(value) {
|
|||
|
|
const text = String(value || "");
|
|||
|
|
const matches = [...text.matchAll(/(\d{2})-(\d{2})/g)];
|
|||
|
|
return matches
|
|||
|
|
.map((match) => ({ month: Number(match[1]), day: Number(match[2]) }))
|
|||
|
|
.filter((token) => Number.isFinite(token.month) && Number.isFinite(token.day));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseDayRangeFromText(value) {
|
|||
|
|
const text = String(value || "");
|
|||
|
|
const range = text.match(/\b(\d{1,2})\s*[–-]\s*(\d{1,2})\b/);
|
|||
|
|
if (!range) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const startDay = Number(range[1]);
|
|||
|
|
const endDay = Number(range[2]);
|
|||
|
|
if (!Number.isFinite(startDay) || !Number.isFinite(endDay)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { startDay, endDay };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function isoToDateAtNoon(iso) {
|
|||
|
|
const text = String(iso || "").trim();
|
|||
|
|
if (!text) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
const parsed = new Date(`${text}T12:00:00`);
|
|||
|
|
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getDaysInMonth(year, monthOrder) {
|
|||
|
|
if (!Number.isFinite(year) || !Number.isFinite(monthOrder)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return new Date(year, monthOrder, 0).getDate();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getMonthStartWeekday(year, monthOrder) {
|
|||
|
|
const date = new Date(year, monthOrder - 1, 1);
|
|||
|
|
return date.toLocaleDateString(undefined, { weekday: "long" });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseMonthRange(month) {
|
|||
|
|
const startText = String(month?.start || "").trim();
|
|||
|
|
const endText = String(month?.end || "").trim();
|
|||
|
|
if (!startText || !endText) {
|
|||
|
|
return "--";
|
|||
|
|
}
|
|||
|
|
return `${startText} to ${endText}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 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 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 getGregorianMonthStartDate(monthOrder, year = getSelectedYear()) {
|
|||
|
|
if (!Number.isFinite(monthOrder) || !Number.isFinite(year)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return new Date(Math.trunc(year), Math.trunc(monthOrder) - 1, 1, 12, 0, 0, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getHebrewMonthAliases(month) {
|
|||
|
|
const aliases = [];
|
|||
|
|
const idAliases = HEBREW_MONTH_ALIAS_BY_ID[String(month?.id || "").toLowerCase()] || [];
|
|||
|
|
aliases.push(...idAliases);
|
|||
|
|
|
|||
|
|
const nameAlias = normalizeCalendarText(month?.name);
|
|||
|
|
if (nameAlias) {
|
|||
|
|
aliases.push(nameAlias);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return Array.from(new Set(aliases.map((alias) => normalizeCalendarText(alias)).filter(Boolean)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function findHebrewMonthStartInGregorianYear(month, year) {
|
|||
|
|
const aliases = getHebrewMonthAliases(month);
|
|||
|
|
if (!aliases.length || !Number.isFinite(year)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 day = readNumericPart(parts, "day");
|
|||
|
|
const monthName = normalizeCalendarText(parts.find((part) => part.type === "month")?.value);
|
|||
|
|
if (day === 1 && aliases.includes(monthName)) {
|
|||
|
|
return new Date(cursor);
|
|||
|
|
}
|
|||
|
|
cursor.setDate(cursor.getDate() + 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function findIslamicMonthStartInGregorianYear(month, year) {
|
|||
|
|
const targetOrder = Number(month?.order);
|
|||
|
|
if (!Number.isFinite(targetOrder) || !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 day = readNumericPart(parts, "day");
|
|||
|
|
const monthNo = readNumericPart(parts, "month");
|
|||
|
|
if (day === 1 && monthNo === Math.trunc(targetOrder)) {
|
|||
|
|
return new Date(cursor);
|
|||
|
|
}
|
|||
|
|
cursor.setDate(cursor.getDate() + 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 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, year = getSelectedYear()) {
|
|||
|
|
const key = String(rule || "").trim().toLowerCase();
|
|||
|
|
if (!key) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (key === "gregorian-easter-sunday") {
|
|||
|
|
return computeWesternEasterDate(year);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (key === "gregorian-good-friday") {
|
|||
|
|
const easter = computeWesternEasterDate(year);
|
|||
|
|
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(year, 10, 4, 4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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) {
|
|||
|
|
const month = getIslamicMonths().find((item) => item?.id === monthId);
|
|||
|
|
const order = Number(month?.order);
|
|||
|
|
return Number.isFinite(order) ? Math.trunc(order) : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function findIslamicMonthDayInGregorianYear(monthId, day, year) {
|
|||
|
|
const monthOrder = getIslamicMonthOrderById(monthId);
|
|||
|
|
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) {
|
|||
|
|
if (!holiday || typeof holiday !== "object") {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const calendarId = String(holiday.calendarId || "").trim().toLowerCase();
|
|||
|
|
const monthId = String(holiday.monthId || "").trim().toLowerCase();
|
|||
|
|
const day = Number(holiday.day);
|
|||
|
|
const selectedYear = getSelectedYear();
|
|||
|
|
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 findWheelMonthStartInGregorianYear(month, year) {
|
|||
|
|
const parsed = parseFirstMonthDayFromText(month?.date);
|
|||
|
|
if (!parsed || !Number.isFinite(year)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return new Date(Math.trunc(year), parsed.monthIndex, parsed.day, 12, 0, 0, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getGregorianReferenceDateForCalendarMonth(month) {
|
|||
|
|
const calId = getSelectedCalendar();
|
|||
|
|
const selectedYear = getSelectedYear();
|
|||
|
|
if (calId === "gregorian") {
|
|||
|
|
return getGregorianMonthStartDate(Number(month?.order), selectedYear);
|
|||
|
|
}
|
|||
|
|
if (calId === "hebrew") {
|
|||
|
|
return findHebrewMonthStartInGregorianYear(month, selectedYear);
|
|||
|
|
}
|
|||
|
|
if (calId === "islamic") {
|
|||
|
|
return findIslamicMonthStartInGregorianYear(month, selectedYear);
|
|||
|
|
}
|
|||
|
|
if (calId === "wheel-of-year") {
|
|||
|
|
return findWheelMonthStartInGregorianYear(month, selectedYear);
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function formatIsoDate(date) {
|
|||
|
|
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|||
|
|
return "";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const year = date.getFullYear();
|
|||
|
|
const month = `${date.getMonth() + 1}`.padStart(2, "0");
|
|||
|
|
const day = `${date.getDate()}`.padStart(2, "0");
|
|||
|
|
return `${year}-${month}-${day}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveCalendarDayToGregorian(month, dayNumber) {
|
|||
|
|
const calId = getSelectedCalendar();
|
|||
|
|
const selectedYear = getSelectedYear();
|
|||
|
|
const day = Math.trunc(Number(dayNumber));
|
|||
|
|
if (!Number.isFinite(day) || day <= 0) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (calId === "gregorian") {
|
|||
|
|
const monthOrder = Number(month?.order);
|
|||
|
|
if (!Number.isFinite(monthOrder)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
return new Date(selectedYear, monthOrder - 1, day, 12, 0, 0, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (calId === "hebrew") {
|
|||
|
|
return findHebrewMonthDayInGregorianYear(month?.id, day, selectedYear);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (calId === "islamic") {
|
|||
|
|
return findIslamicMonthDayInGregorianYear(month?.id, day, selectedYear);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function intersectDateRanges(startA, endA, startB, endB) {
|
|||
|
|
const start = startA.getTime() > startB.getTime() ? startA : startB;
|
|||
|
|
const end = endA.getTime() < endB.getTime() ? endA : endB;
|
|||
|
|
return start.getTime() <= end.getTime() ? { start, end } : null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function init(nextConfig = {}) {
|
|||
|
|
config = {
|
|||
|
|
...config,
|
|||
|
|
...nextConfig
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
window.TarotCalendarDates = {
|
|||
|
|
...(window.TarotCalendarDates || {}),
|
|||
|
|
init,
|
|||
|
|
parseMonthDayToken,
|
|||
|
|
buildSignDateBounds,
|
|||
|
|
addDays,
|
|||
|
|
formatDateLabel,
|
|||
|
|
isMonthDayInRange,
|
|||
|
|
parseMonthDayTokensFromText,
|
|||
|
|
parseDayRangeFromText,
|
|||
|
|
isoToDateAtNoon,
|
|||
|
|
getDaysInMonth,
|
|||
|
|
getMonthStartWeekday,
|
|||
|
|
parseMonthRange,
|
|||
|
|
normalizeCalendarText,
|
|||
|
|
formatGregorianReferenceDate,
|
|||
|
|
formatCalendarDateFromGregorian,
|
|||
|
|
getGregorianMonthStartDate,
|
|||
|
|
findHebrewMonthStartInGregorianYear,
|
|||
|
|
findIslamicMonthStartInGregorianYear,
|
|||
|
|
parseFirstMonthDayFromText,
|
|||
|
|
parseMonthDayStartToken,
|
|||
|
|
resolveHolidayGregorianDate,
|
|||
|
|
findWheelMonthStartInGregorianYear,
|
|||
|
|
getGregorianReferenceDateForCalendarMonth,
|
|||
|
|
formatIsoDate,
|
|||
|
|
resolveCalendarDayToGregorian,
|
|||
|
|
intersectDateRanges
|
|||
|
|
};
|
|||
|
|
})();
|