// 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();