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:
+280
@@ -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();
|
||||
Reference in New Issue
Block a user