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