Theoma calendar, basic implementation

This commit is contained in:
2026-01-01 13:51:41 -08:00
commit aee0c66c05
9 changed files with 2472 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
# `create-preact`
<h2 align="center">
<img height="256" width="256" src="./src/assets/preact.svg">
</h2>
<h3 align="center">Get started using Preact and Vite!</h3>
## Getting Started
- `npm run dev` - Starts a dev server at http://localhost:5173/
- `npm run build` - Builds for production, emitting to `dist/`
- `npm run preview` - Starts a server at http://localhost:4173/ to test production build locally

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Preact</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

2106
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

18
package.json Normal file
View File

@@ -0,0 +1,18 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"preact": "^10.26.9"
},
"devDependencies": {
"@preact/preset-vite": "^2.10.2",
"typescript": "^5.9.3",
"vite": "^7.0.4",
"vite-plugin-singlefile": "^2.3.0"
}
}

268
src/index.tsx Normal file
View File

@@ -0,0 +1,268 @@
import { render } from "preact";
import { useState } from "preact/hooks";
type Calendar = {
name: string;
periods: Period[];
epoch: RelativeDay;
};
type Period = {
name: string;
duration: number;
};
type RelativeDay = {
cycle: number;
period: string;
index: number;
};
type AbsoluteDay = number;
const StockCalendars: Calendar[] = [
{
name: "Week",
periods: [
{ name: "Niesday", duration: 1 },
{ name: "Tinsday", duration: 1 },
{ name: "Gwensday", duration: 1 },
{ name: "Varsday", duration: 1 },
{ name: "Ryesday", duration: 1 },
{ name: "Pretsday", duration: 1 },
{ name: "Skansday", duration: 1 },
],
epoch: {
cycle: 0,
period: "Niesday",
index: 1,
},
},
{
name: "Kelkaithian",
periods: [
{ name: "Frigidae", duration: 35 },
{ name: "Vesvor", duration: 35 },
{ name: "Inesqua", duration: 35 },
{ name: "Spring Celebration", duration: 7 },
{ name: "Kinish", duration: 35 },
{ name: "Chyran", duration: 35 },
{ name: "Kelit", duration: 35 },
{ name: "Emrak", duration: 35 },
{ name: "Rynawa", duration: 35 },
{ name: "Tanith", duration: 35 },
{ name: "Autumn Celebration", duration: 7 },
{ name: "Skelundy", duration: 35 },
],
epoch: {
cycle: 0,
period: "Frigidae",
index: 1,
},
},
{
name: "Tachamundi",
periods: [
{ name: "Firstmonth", duration: 28 },
{ name: "Secondmonth", duration: 28 },
{ name: "Thirdmonth", duration: 28 },
{ name: "Fourthmonth", duration: 28 },
{ name: "Fifthmonth", duration: 28 },
{ name: "Sixthmonth", duration: 28 },
{ name: "Seventhmonth", duration: 28 },
{ name: "Eighthmonth", duration: 28 },
{ name: "Ninthmonth", duration: 28 },
{ name: "Tenthmonth", duration: 28 },
{ name: "Eleventhmonth", duration: 28 },
{ name: "Twelfthmonth", duration: 28 },
{ name: "Thirteenthmonth", duration: 28 },
],
epoch: {
cycle: 0,
period: "Firstmonth",
index: 1,
},
},
{
name: "Seasonal",
periods: [
{ name: "Winter", duration: 91 },
{ name: "Spring", duration: 91 },
{ name: "Summer", duration: 91 },
{ name: "Autumn", duration: 91 },
],
epoch: {
cycle: 0,
period: "Winter",
index: 14,
},
},
];
function computeDayOffset(calendar: Calendar, period: string, index: number) {
let cumulativeOffset = 0;
for (let candidate of calendar.periods) {
if (candidate.name != period) {
cumulativeOffset += candidate.duration;
} else {
// correct invalid dates
if (index < 1) {
index = 1;
}
if (index > candidate.duration) {
index = candidate.duration;
}
cumulativeOffset += index - 1; // days are 1-indexed
return cumulativeOffset;
}
}
throw new Error(`could not compute day offset for ${period} ${index}`);
}
function epochOffset(calendar: Calendar) {
let epochDayOffset = computeDayOffset(
calendar,
calendar.epoch.period,
calendar.epoch.index,
);
let duration = calendarDuration(calendar);
let fullOffset = epochDayOffset + calendar.epoch.cycle * duration;
return fullOffset;
}
function calendarDuration(calendar: Calendar): number {
let totalLength = 0;
for (let period of calendar.periods) {
totalLength += period.duration;
}
return totalLength;
}
function divmod(i: number, base: number): [number, number] {
// a polite modulus similar to Python's
return [Math.floor(i / base), ((i % base) + base) % base];
}
function relativize(calendar: Calendar, absolute: AbsoluteDay): RelativeDay {
let duration = calendarDuration(calendar);
absolute += epochOffset(calendar);
let [cycle, cycleDay] = divmod(absolute, duration);
let period = null;
let index = null;
for (let candidate of calendar.periods) {
if (cycleDay < candidate.duration) {
period = candidate.name;
index = cycleDay + 1; // 1-indexed
break;
} else {
cycleDay -= candidate.duration;
}
}
if (period == null) {
throw new Error(`could not resolve ${absolute}`);
}
return { cycle, period, index };
}
function absolutize(calendar: Calendar, instant: RelativeDay): AbsoluteDay {
let duration = calendarDuration(calendar);
let cycleDay = computeDayOffset(calendar, instant.period, instant.index);
return cycleDay + instant.cycle * duration - epochOffset(calendar);
}
export function App() {
let [absoluteDay, setAbsoluteDay] = useState(0);
let widgets = [];
for (let calendar of StockCalendars) {
widgets.push(
<CalendarWidget
key={calendar.name}
value={calendar}
absoluteDay={absoluteDay}
setAbsoluteDay={setAbsoluteDay}
/>,
);
}
return <>{widgets}</>;
}
export function CalendarWidget({
value,
absoluteDay,
setAbsoluteDay,
}: {
value: Calendar;
absoluteDay: number;
setAbsoluteDay: (number) => void;
}) {
let rel = relativize(value, absoluteDay);
let abs = absolutize(value, rel);
if (abs != absoluteDay) {
throw new Error(`roundtrip failed: ${absoluteDay} ${rel} ${abs}`);
}
let cycleSelector = (
<input
type="text"
onChange={(t) => {
let cycle = parseInt(t.currentTarget.value);
setAbsoluteDay(absolutize(value, { ...rel, cycle }));
}}
value={rel.cycle}
></input>
);
let periodSelector = (
<select
onChange={(t) => {
let selection = t.currentTarget.value;
setAbsoluteDay(absolutize(value, { ...rel, period: selection }));
}}
>
{value.periods.map((i) => (
<option key={i.name} selected={i.name == rel.period}>
{i.name}
</option>
))}
</select>
);
let relevantPeriod = value.periods.find((p) => p.name == rel.period);
let possibleIndexes = [...Array(relevantPeriod.duration).keys()].map(
(i) => i + 1,
);
let indexSelector = (
<select
onChange={(t) => {
let selection = parseInt(t.currentTarget.value);
setAbsoluteDay(absolutize(value, { ...rel, index: selection }));
}}
>
{possibleIndexes.map((i: number) => (
<option key={i} selected={i == rel.index} value={i}>
{i}
</option>
))}
</select>
);
if (possibleIndexes.length == 1) {
indexSelector = <></>;
}
return (
<div>
<p>{value.name}</p>
<p>
{cycleSelector} {periodSelector} {indexSelector}
</p>
</div>
);
}
render(<App />, document.getElementById("app"));

0
src/style.css Normal file
View File

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"noEmit": true,
"allowJs": true,
"checkJs": true,
/* Preact Config */
"jsx": "react-jsx",
"jsxImportSource": "preact",
"skipLibCheck": true,
"paths": {
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"]
}
},
"include": ["node_modules/vite/client.d.ts", "**/*"]
}

8
vite.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig } from "vite";
import preact from "@preact/preset-vite";
import { viteSingleFile } from "vite-plugin-singlefile"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [preact(), viteSingleFile()],
});