Initial commit — Seth Calendar & Decimal Time clock site
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.
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
// 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();
|
||||
Reference in New Issue
Block a user