Theoma calendar, basic implementation
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
15
README.md
Normal 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
13
index.html
Normal 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
2106
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal 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
268
src/index.tsx
Normal 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
0
src/style.css
Normal file
20
tsconfig.json
Normal file
20
tsconfig.json
Normal 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
8
vite.config.ts
Normal 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()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user