when-am-i / files / src / types / index.ts
index.ts
26a02b71 2026-06-19 Pavel Safronov | 1 | // Shared types for "When am I?" (the calendar app). |
a167bf48 2026-06-17 Pavel Safronov | 2 | // |
| 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. */ | |
| 14 | export type LinearRing = [number, number][]; | |
| 15 | ||
| 16 | export interface Polygon { | |
| 17 | type: 'Polygon'; | |
| 18 | /** First ring is the outer boundary; any further rings are holes. */ | |
| 19 | coordinates: LinearRing[]; | |
| 20 | } | |
| 21 | ||
| 22 | export interface MultiPolygon { | |
| 23 | type: 'MultiPolygon'; | |
| 24 | /** An array of polygons, each itself an array of rings. */ | |
| 25 | coordinates: LinearRing[][]; | |
| 26 | } | |
| 27 | ||
| 28 | export 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 | ||
| 33 | export interface Region { | |
| 34 | name: string; | |
| 35 | geo: Geometry; | |
| 36 | } | |
| 37 | ||
| 38 | /** | |
26a02b71 2026-06-19 Pavel Safronov | 39 | * 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 Safronov | 44 | */ |
| 45 | export 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 Safronov | 58 | /** |
| 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 Safronov | 64 | /** |
| 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 Safronov | 72 | } |
| 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 | */ | |
| 81 | export 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 Safronov | 88 | } |
| 89 | ||
| 90 | export interface CalendarRegion { | |
| 91 | region: Region; | |
| 92 | eras: Era[]; | |
34c4dd59 2026-06-18 Pavel Safronov | 93 | /** Repo-relative path of the JSON file this region was loaded from. */ |
| 94 | sourceFile?: string; | |
a167bf48 2026-06-17 Pavel Safronov | 95 | } |
| 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 | */ | |
| 105 | export 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. */ | |
| 116 | export 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 Safronov | 132 | /** |
| 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 Safronov | 138 | } |
| 139 | ||
26a02b71 2026-06-19 Pavel Safronov | 140 | /** One possible reply a local could give to "When am I?". */ |
a167bf48 2026-06-17 Pavel Safronov | 141 | export interface Answer { |
| 142 | region: Region; | |
| 143 | era: Era; | |
| 144 | conversion: CalendarConversion; | |
34c4dd59 2026-06-18 Pavel Safronov | 145 | /** Repo-relative path of the source JSON file, for citation links. */ |
| 146 | sourceFile?: string; | |
a167bf48 2026-06-17 Pavel Safronov | 147 | } |
| 148 | ||
| 149 | /** Why the engine produced no answers. */ | |
| 150 | export 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 | */ | |
| 157 | export interface Resolution { | |
| 158 | answers: Answer[]; | |
| 159 | missReason?: MissReason; | |
| 160 | } |