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:
2026-03-08 22:32:38 +00:00
commit a6b3f039d8
48 changed files with 7324 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
// Decimal time & French Republican Calendar
//
// Decimal time: 1 day = 10 decimal hours
// 1 decimal hour = 100 decimal minutes
// 1 decimal minute = 100 decimal seconds
//
// French Republican Calendar epoch: 22 September 1792 (Gregorian) = 1 Vendémiaire An I
// Each year has 12 months of 30 days + 5 (or 6 in sextile years) complementary days.
// Sextile (leap) years: the Republican leap rule approximates the Gregorian one —
// years 3, 7, 11, 15, 20 of each 20-year cycle were designated sextile in the original
// Romme proposal. For simplicity we use the widely accepted rule:
// A Republican year is sextile if the *following* Gregorian year is a Gregorian leap year.
const REPUBLICAN_EPOCH_JD = 2375840; // Julian Day Number of 1 Vendémiaire An I (22 Sep 1792)
const MONTH_NAMES = [
"Vendémiaire", "Brumaire", "Frimaire",
"Nivôse", "Pluviôse", "Ventôse",
"Germinal", "Floréal", "Prairial",
"Messidor", "Thermidor", "Fructidor"
];
const MONTH_NAMES_EN = [
"Vintage", "Mist", "Frost",
"Snowy", "Rainy", "Windy",
"Budding", "Flowery", "Meadow",
"Harvest", "Heat", "Fruit"
];
const COMPLEMENTARY_NAMES = [
"Jour de la Vertu", // 1
"Jour du Génie", // 2
"Jour du Travail", // 3
"Jour de l'Opinion", // 4
"Jour des Récompenses", // 5
"Jour de la Révolution", // 6 (sextile only)
];
const COMPLEMENTARY_NAMES_EN = [
"Day of Virtue",
"Day of Genius",
"Day of Labour",
"Day of Opinion",
"Day of Rewards",
"Day of the Revolution",
];
const WEEKDAY_NAMES = [
"Primidi", "Duodi", "Tridi", "Quartidi", "Quintidi",
"Sextidi", "Septidi", "Octidi", "Nonidi", "Décadi"
];
const WEEKDAY_NAMES_EN = [
"First day", "Second day", "Third day", "Fourth day", "Fifth day",
"Sixth day", "Seventh day", "Eighth day", "Ninth day", "Tenth day"
];
// --- Julian Day Number helpers ---
function gregorianToJD(year, month, day) {
// Algorithm from Jean Meeus, "Astronomical Algorithms"
if (month <= 2) { year -= 1; month += 12; }
const A = Math.floor(year / 100);
const B = 2 - A + Math.floor(A / 4);
return Math.floor(365.25 * (year + 4716)) +
Math.floor(30.6001 * (month + 1)) +
day + B - 1524.5;
}
function isGregorianLeap(year) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
function isSextile(republicanYear) {
// The year following An N is Gregorian year 1792 + N
return isGregorianLeap(1792 + republicanYear);
}
function toRepublicanDate(utcDate) {
// JD at midnight UTC for the given date
const jd = Math.floor(gregorianToJD(
utcDate.getUTCFullYear(),
utcDate.getUTCMonth() + 1,
utcDate.getUTCDate()
) + 0.5); // +0.5 because JD starts at noon; floor gives previous midnight
const daysSinceEpoch = jd - REPUBLICAN_EPOCH_JD;
if (daysSinceEpoch < 0) {
return null; // Before the Republic
}
// Walk years: each year is 365 or 366 days
let year = 1;
let remaining = daysSinceEpoch;
while (true) {
const yearLen = isSextile(year) ? 366 : 365;
if (remaining < yearLen) break;
remaining -= yearLen;
year++;
}
let month, day, weekday, complementary;
if (remaining < 360) {
// Normal months (0-indexed day within year)
month = Math.floor(remaining / 30) + 1; // 1..12
day = (remaining % 30) + 1; // 1..30
weekday = ((remaining % 10)); // 0..9 → Primidi..Décadi
complementary = null;
} else {
// Complementary days (jours complémentaires / sans-culottides)
const compIdx = remaining - 360; // 0..4 (or 0..5 sextile)
month = null;
day = compIdx + 1;
weekday = null;
complementary = COMPLEMENTARY_NAMES[compIdx] || "Jour inconnu";
}
return { year, month, day, weekday, complementary };
}
function toRomanNumeral(n) {
const vals = [1000,900,500,400,100,90,50,40,10,9,5,4,1];
const syms = ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"];
let result = "";
for (let i = 0; i < vals.length; i++) {
while (n >= vals[i]) { result += syms[i]; n -= vals[i]; }
}
return result;
}
function formatRepublicanDate(r) {
if (!r) return { full: "Before the Republic", translation: "", dayOfYear: "-", year: "-" };
let full, translation;
if (r.complementary) {
const compIdx = r.day - 1;
full = `${r.complementary}, An ${toRomanNumeral(r.year)}`;
translation = `${COMPLEMENTARY_NAMES_EN[compIdx]}, Year ${r.year}`;
} else {
const weekdayName = WEEKDAY_NAMES[r.weekday];
const weekdayNameEn = WEEKDAY_NAMES_EN[r.weekday];
const monthName = MONTH_NAMES[r.month - 1];
const monthNameEn = MONTH_NAMES_EN[r.month - 1];
full = `${weekdayName}, ${r.day} ${monthName} An ${toRomanNumeral(r.year)}`;
translation = `${weekdayNameEn}, ${r.day} ${monthNameEn}, Year ${r.year}`;
}
const dayOfYear = r.complementary
? `360 + ${r.day} (Jour complémentaire)`
: `${(r.month - 1) * 30 + r.day}`;
return { full, translation, dayOfYear, year: `An ${toRomanNumeral(r.year)}` };
}
// --- Decimal time ---
// Decimal time counts from local midnight in the selected time zone.
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"
});
const parts = fmt.formatToParts(date).reduce((acc, p) => {
if (p.type !== "literal") acc[p.type] = parseInt(p.value, 10);
return acc;
}, {});
return parts; // { year, month, day, hour, minute, second }
}
function toDecimalTime(date, zone) {
const p = getLocalParts(date, zone);
// ms since local midnight (ignoring DST transitions within the day — good enough)
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; // 10h * 100m * 100s
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 {
str: `${dHour}:${String(dMin).padStart(2,"0")}:${String(dSecInt).padStart(2,"0")}.${String(dCenti).padStart(2,"0")}`
};
}
function toRepublicanDateLocal(date, zone) {
const p = getLocalParts(date, zone);
return toRepublicanDate(new Date(Date.UTC(p.year, p.month - 1, p.day)));
}
// --- DOM & sync ---
const zoneSelect = document.getElementById("zoneSelect");
const zoneLabel = document.getElementById("zoneLabel");
const dateTrans = document.getElementById("dateTrans");
const offsetLabel = document.getElementById("offsetLabel");
const decimalUtc = document.getElementById("decimalUtc");
const dayOfYear = document.getElementById("dayOfYear");
const repYear = document.getElementById("repYear");
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 render() {
const now = new Date(Date.now() + serverOffsetMs);
const dt = toDecimalTime(now, selectedZone);
const rep = toRepublicanDateLocal(now, selectedZone);
const fmt = formatRepublicanDate(rep);
digital.textContent = dt.str;
dateLine.textContent = fmt.full;
dateTrans.textContent = fmt.translation;
decimalUtc.textContent = dt.str;
dayOfYear.textContent = fmt.dayOfYear;
repYear.textContent = fmt.year;
zoneLabel.textContent = selectedZone;
utcLabel.textContent = now.toISOString().replace("T", " ").replace("Z", " UTC");
requestAnimationFrame(render);
}
syncWithServer();
setInterval(syncWithServer, 20_000);
render();