fuzzyworld

when-am-i / files / src / types / index.ts

index.ts

Blame: the commit that last changed each line · file · history

26a02b71 2026-06-19 Pavel Safronov1// Shared types for "When am I?" (the calendar app).
a167bf48 2026-06-17 Pavel Safronov2//
3// Data flow (see plan.md): click (lat,lng) + date
4// → find all regions containing the point
5// → find all eras (may overlap) covering the date
6// → convert the date into each era's calendar (or fall back to descriptive-only)
7// → render the full list of Answers.
8
9// ── GeoJSON geometry ────────────────────────────────────────────────────────
10// We only need area geometries for point-in-region tests. GeoJSON coordinates
11// are [longitude, latitude] (note the order — easy to get backwards).
12
13/** A single linear ring: an array of [lng, lat] positions. */
14export type LinearRing = [number, number][];
15
16export interface Polygon {
17 type: 'Polygon';
18 /** First ring is the outer boundary; any further rings are holes. */
19 coordinates: LinearRing[];
20}
21
22export interface MultiPolygon {
23 type: 'MultiPolygon';
24 /** An array of polygons, each itself an array of rings. */
25 coordinates: LinearRing[][];
26}
27
28export type Geometry = Polygon | MultiPolygon;
29
30// ── Region & era (the on-disk JSON shape) ─────────────────────────────────────
31// One JSON file in src/data/calendars/ deserializes to a CalendarRegion.
32
33export interface Region {
34 name: string;
35 geo: Geometry;
36}
37
38/**
26a02b71 2026-06-19 Pavel Safronov39 * One calendar era used in a region over some span of time. Eras MAY overlap in
40 * time — parallel calendars coexisting at the same place and time is expected
41 * (e.g. the colonial Julian and the Massachusett lunar reckoning both apply in
42 * 1700 Massachusetts Bay), whether the overlap is within one region's eras or
43 * across two overlapping regions. Not an error.
a167bf48 2026-06-17 Pavel Safronov44 */
45export interface Era {
46 name: string;
47 /** Proleptic-Gregorian YYYY-MM-DD, or null for "since the earliest time". */
48 start: string | null;
49 /** Proleptic-Gregorian YYYY-MM-DD, or null for "up to the present". */
50 end: string | null;
51 /** Calendar system name, e.g. "Gregorian", "Julian", "Islamic". */
52 calendar: string;
53 /** Whose answer this is, e.g. "British colonists", "Massachusett people". */
54 usedBy?: string;
55 description?: string;
56 /** URLs or citations backing this era. */
57 sources?: string[];
5c66fc6a 2026-06-21 Pavel Safronov58 /**
59 * Documented events a local reckons time from (for calendars with no
60 * numbered year). The engine reports the most recent one at/before the date
61 * as "about N winters since <event>". Optional and back-compatible.
62 */
63 anchorEvents?: AnchorEvent[];
cedd4e9c 2026-06-23 Pavel Safronov64 /**
65 * When true, this era opts its region out of the modern global Gregorian
66 * default (see resolve.ts's DEFAULT_MODERN_ERA) for any date this era
67 * covers — for the rare place where no outside civil calendar applies at
68 * all (e.g. North Sentinel Island), not just one with its own different
69 * calendar. Defaults to false; almost no region should set this.
70 */
71 suppressModernDefault?: boolean;
5c66fc6a 2026-06-21 Pavel Safronov72}
73
74/**
75 * A documented, dateable event a local could reckon time *from* — e.g. "the
76 * great dying" or a remembered eclipse. Used by calendars with no continuous
77 * numbered year to answer "When am I?" as "about N winters since <event>".
78 * Anchors are per-region curated data: each must be a real, sourced event — the
79 * engine only counts the winters, it never invents the event (see plan.md).
80 */
81export interface AnchorEvent {
82 /** Proleptic-Gregorian YYYY-MM-DD the event happened (or began). */
83 date: string;
84 /** How a local would name it, e.g. "the great dying". */
85 name: string;
86 /** URLs or citations documenting the event. Required: anchors must be sourced. */
87 sources: string[];
a167bf48 2026-06-17 Pavel Safronov88}
89
90export interface CalendarRegion {
91 region: Region;
92 eras: Era[];
34c4dd59 2026-06-18 Pavel Safronov93 /** Repo-relative path of the JSON file this region was loaded from. */
94 sourceFile?: string;
a167bf48 2026-06-17 Pavel Safronov95}
96
97// ── Engine input ──────────────────────────────────────────────────────────────
98
99/**
100 * A date in the proleptic Gregorian calendar using astronomical year numbering:
101 * year 0 = 1 BCE, year -1 = 2 BCE, etc. This is the engine-internal form; the
102 * UI translates a BCE/CE picker into it. Comparisons go through a Julian Day
103 * Number so BCE/CE and pre-1582 dates order correctly (see plan.md, Q2).
104 */
105export interface HistoricalDate {
106 year: number;
107 /** 1–12. */
108 month: number;
109 /** 1–31. */
110 day: number;
111}
112
113// ── Engine output ─────────────────────────────────────────────────────────────
114
115/** The result of converting a date into one calendar system. */
116export interface CalendarConversion {
117 /** The calendar system name (echoes Era.calendar). */
118 system: string;
119 /** Human-readable local year, e.g. "1700 AD", "AH 1111", "5460". */
120 localYearLabel: string;
121 /** Optional fuller local date when meaningful. */
122 localDateLabel?: string;
123 /**
124 * Whether a deterministic epoch+rule produced a numeric local year.
125 * `false` ⇒ descriptive-only: no continuous year count exists (e.g. an
126 * observational lunar calendar). The UI then shows description + sources +
127 * the Gregorian reference year instead of a converted number.
128 */
129 convertible: boolean;
130 /** Optional clarifying note, esp. for descriptive-only calendars. */
131 note?: string;
5c66fc6a 2026-06-21 Pavel Safronov132 /**
133 * Optional "about N winters since <event>" reckoning, computed from the
134 * era's `anchorEvents`. Present only when an anchor event precedes the date.
135 * Mainly enriches descriptive-only calendars, which have no numbered year.
136 */
137 reckoning?: { label: string; sources: string[] };
a167bf48 2026-06-17 Pavel Safronov138}
139
26a02b71 2026-06-19 Pavel Safronov140/** One possible reply a local could give to "When am I?". */
a167bf48 2026-06-17 Pavel Safronov141export interface Answer {
142 region: Region;
143 era: Era;
144 conversion: CalendarConversion;
34c4dd59 2026-06-18 Pavel Safronov145 /** Repo-relative path of the source JSON file, for citation links. */
146 sourceFile?: string;
a167bf48 2026-06-17 Pavel Safronov147}
148
149/** Why the engine produced no answers. */
150export type MissReason = 'no-region' | 'no-era';
151
152/**
153 * The engine's full result: every valid answer a local could give. `answers`
154 * may be empty, in which case `missReason` explains why (no region covers the
155 * point, or a region matched but no era covers the date).
156 */
157export interface Resolution {
158 answers: Answer[];
159 missReason?: MissReason;
160}