a6b3f039d8
Pages: /, /simple, /decimal, /seth, /calendar, /astro, /convert, /timegov Features: Seth Calendar (10×36 + holidays), decimal time, moon phases, astronomy (sun/moon), bidirectional time converter, Seth date display, leap day split cell in calendar grid.
219 lines
7.3 KiB
JavaScript
219 lines
7.3 KiB
JavaScript
// Seth Calendar
|
||
//
|
||
// Structure:
|
||
// - Same year numbers and Jan 1 epoch as Gregorian
|
||
// - 10 months of 36 days (6 weeks of 6 days each)
|
||
// - 5 holiday days at year end (6 on leap years)
|
||
// - Total: 365 or 366 days
|
||
//
|
||
// Gregorian alignment (non-leap year):
|
||
// Month 1 Day 1 = Jan 1
|
||
// Month 10 Day 35 = Dec 25 (Christmas)
|
||
// Month 10 Day 36 = Dec 26 (Boxing Day)
|
||
// Holiday 1 = Dec 27
|
||
// Holiday 5 = Dec 31 (New Year's Eve)
|
||
//
|
||
// Gregorian alignment (leap year):
|
||
// Month 10 Day 35 = Dec 24 (Christmas Eve)
|
||
// Month 10 Day 36 = Dec 25 (Christmas)
|
||
// Holiday 1 = Dec 26 (Boxing Day)
|
||
// Holiday 6 = Dec 31 (New Year's Eve)
|
||
|
||
function isLeapYear(year) {
|
||
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
||
}
|
||
|
||
function toSethDate(year, dayOfYear) {
|
||
// dayOfYear: 1-indexed (Jan 1 = 1)
|
||
const leap = isLeapYear(year);
|
||
|
||
// Leap Day: DOY 60 in a leap year — sits between M1 W3 D4 and M1 W3 D5
|
||
if (leap && dayOfYear === 60) {
|
||
return { type: "leapday", year, dayOfYear };
|
||
}
|
||
|
||
// After leap day, subtract 1 so the rest of the calendar stays pegged
|
||
const idx = (leap && dayOfYear > 60 ? dayOfYear - 1 : dayOfYear) - 1;
|
||
|
||
if (idx < 360) {
|
||
const month = Math.floor(idx / 36); // 0..9
|
||
const dayInM = idx % 36; // 0..35
|
||
const week = Math.floor(dayInM / 6); // 0..5
|
||
const dayInW = dayInM % 6; // 0..5
|
||
return { type: "month", year, month, day: dayInM, week, weekDay: dayInW, dayOfYear };
|
||
} else {
|
||
const holiday = idx - 360; // 0..4 (or 0..5 leap)
|
||
return { type: "holiday", year, holiday, leap, dayOfYear };
|
||
}
|
||
}
|
||
|
||
function getDayOfYear(date, zone) {
|
||
const fmt = new Intl.DateTimeFormat("en-US", {
|
||
timeZone: zone,
|
||
year: "numeric", month: "numeric", day: "numeric"
|
||
});
|
||
const parts = fmt.formatToParts(date).reduce((acc, p) => {
|
||
if (p.type !== "literal") acc[p.type] = parseInt(p.value, 10);
|
||
return acc;
|
||
}, {});
|
||
|
||
const { year, month, day } = parts;
|
||
|
||
// Day of year calculation
|
||
const cumDays = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
||
const leap = isLeapYear(year);
|
||
let doy = cumDays[month - 1] + day;
|
||
if (leap && month > 2) doy++;
|
||
|
||
return { year, doy };
|
||
}
|
||
|
||
function formatSethDate(sd) {
|
||
if (sd.type === "leapday") return "Leap Day";
|
||
if (sd.type === "holiday") {
|
||
const total = sd.leap ? 6 : 5;
|
||
return `Holiday ${sd.holiday} of ${total}`;
|
||
}
|
||
return `Month ${sd.month}, Week ${sd.week}, Day ${sd.weekDay}`;
|
||
}
|
||
|
||
function formatSethLong(sd) {
|
||
if (sd.type === "leapday") return `${sd.year} Leap Day — Feb 29`;
|
||
if (sd.type === "holiday") {
|
||
const lastHoliday = sd.leap ? 5 : 4;
|
||
const isNYE = sd.holiday === lastHoliday;
|
||
const isBoxingDay = sd.leap && sd.holiday === 0;
|
||
let note = "";
|
||
if (isNYE) note = " — New Year's Eve";
|
||
else if (isBoxingDay) note = " — Boxing Day";
|
||
return `${sd.year} Holiday ${sd.holiday}${note}`;
|
||
}
|
||
return `${sd.year} M${sd.month} W${sd.week} D${sd.weekDay} (Month ${sd.month}, Day ${sd.day})`;
|
||
}
|
||
|
||
// --- Decimal time (same as decimal page) ---
|
||
|
||
function getLocalParts(date, zone) {
|
||
const fmt = new Intl.DateTimeFormat("en-US", {
|
||
timeZone: zone,
|
||
hour12: false,
|
||
year: "numeric", month: "2-digit", day: "2-digit",
|
||
hour: "2-digit", minute: "2-digit", second: "2-digit"
|
||
});
|
||
return fmt.formatToParts(date).reduce((acc, p) => {
|
||
if (p.type !== "literal") acc[p.type] = parseInt(p.value, 10);
|
||
return acc;
|
||
}, {});
|
||
}
|
||
|
||
function toDecimalTime(date, zone) {
|
||
const p = getLocalParts(date, zone);
|
||
const msIntoDay =
|
||
p.hour * 3_600_000 +
|
||
p.minute * 60_000 +
|
||
p.second * 1_000 +
|
||
date.getMilliseconds();
|
||
|
||
const dayFraction = msIntoDay / 86_400_000;
|
||
const totalDecimalSeconds = dayFraction * 100_000;
|
||
|
||
const dHour = Math.floor(totalDecimalSeconds / 10_000);
|
||
const rem1 = totalDecimalSeconds % 10_000;
|
||
const dMin = Math.floor(rem1 / 100);
|
||
const dSec = rem1 % 100;
|
||
const dCenti = Math.floor((dSec % 1) * 100);
|
||
const dSecInt = Math.floor(dSec);
|
||
|
||
return `\u00a0${dHour}:${String(dMin).padStart(2,"0")}:${String(dSecInt).padStart(2,"0")}.${String(dCenti).padStart(2,"0")}`;
|
||
}
|
||
|
||
// --- DOM & sync ---
|
||
|
||
const zoneSelect = document.getElementById("zoneSelect");
|
||
const zoneLabel = document.getElementById("zoneLabel");
|
||
const offsetLabel = document.getElementById("offsetLabel");
|
||
const decimalTime = document.getElementById("decimalTime");
|
||
const gregorianTime = document.getElementById("gregorianTime");
|
||
const dayOfYear = document.getElementById("dayOfYear");
|
||
const weekOfYear = document.getElementById("weekOfYear");
|
||
const utcLabel = document.getElementById("utcLabel");
|
||
const dateLine = document.getElementById("dateLine");
|
||
const digital = document.getElementById("digital");
|
||
|
||
const zones = [
|
||
"America/New_York",
|
||
"America/Chicago",
|
||
"America/Denver",
|
||
"America/Los_Angeles",
|
||
"America/Anchorage",
|
||
"Pacific/Honolulu",
|
||
"UTC"
|
||
];
|
||
|
||
let selectedZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "America/New_York";
|
||
if (!zones.includes(selectedZone)) selectedZone = "America/New_York";
|
||
|
||
for (const zone of zones) {
|
||
const option = document.createElement("option");
|
||
option.value = zone;
|
||
option.textContent = zone;
|
||
if (zone === selectedZone) option.selected = true;
|
||
zoneSelect.appendChild(option);
|
||
}
|
||
|
||
zoneSelect.addEventListener("change", () => {
|
||
selectedZone = zoneSelect.value;
|
||
zoneLabel.textContent = selectedZone;
|
||
});
|
||
|
||
let serverOffsetMs = 0;
|
||
|
||
async function syncWithServer() {
|
||
const start = Date.now();
|
||
try {
|
||
const response = await fetch(`/api/time?ts=${start}`, { cache: "no-store" });
|
||
const end = Date.now();
|
||
const data = await response.json();
|
||
const rtt = end - start;
|
||
serverOffsetMs = (data.epoch_ms + rtt / 2) - end;
|
||
offsetLabel.textContent = `Synchronized (${serverOffsetMs >= 0 ? "+" : ""}${serverOffsetMs.toFixed(0)} ms, RTT ${rtt} ms)`;
|
||
} catch (_) {
|
||
offsetLabel.textContent = "Sync failed";
|
||
}
|
||
}
|
||
|
||
function toGregorianTime(date, zone) {
|
||
const p = getLocalParts(date, zone);
|
||
// Format as HH:MM:SS.cc to match decimal time width
|
||
const cs = String(Math.floor(date.getMilliseconds() / 10)).padStart(2, "0");
|
||
return `${String(p.hour).padStart(2,"0")}:${String(p.minute).padStart(2,"0")}:${String(p.second).padStart(2,"0")}.${cs}`;
|
||
}
|
||
|
||
function render() {
|
||
const now = new Date(Date.now() + serverOffsetMs);
|
||
const { year, doy } = getDayOfYear(now, selectedZone);
|
||
const sd = toSethDate(year, doy);
|
||
const dt = toDecimalTime(now, selectedZone);
|
||
const gt = toGregorianTime(now, selectedZone);
|
||
|
||
dateLine.textContent = formatSethLong(sd);
|
||
digital.textContent = dt;
|
||
decimalTime.textContent = dt;
|
||
gregorianTime.textContent = gt;
|
||
dayOfYear.textContent = `${doy} of ${isLeapYear(year) ? 366 : 365}`;
|
||
const totalWeeks = 60; // 10 months × 6 weeks
|
||
const woy = sd.type === "month" ? sd.month * 6 + sd.week : "—";
|
||
const woyTotal = sd.type === "month" ? `${woy} of ${totalWeeks}`
|
||
: sd.type === "leapday" ? "— (leap day)"
|
||
: `— (holiday days)`;
|
||
weekOfYear.textContent = woyTotal;
|
||
zoneLabel.textContent = selectedZone;
|
||
utcLabel.textContent = now.toISOString().replace("T", " ").replace("Z", " UTC");
|
||
|
||
requestAnimationFrame(render);
|
||
}
|
||
|
||
syncWithServer();
|
||
setInterval(syncWithServer, 20_000);
|
||
render();
|