Files
TaroTime/app/ui-calendar-dates.js
2026-03-07 05:17:50 -08:00

651 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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
};
})();