when-am-i / commit
feat: modern global Gregorian default for recent dates
README.md | 4 ++++ plan.md | 13 ++++++++++ src/App.tsx | 15 +++++++++++- src/utils/resolve.test.ts | 44 ++++++++++++++++++++++++++++++++++ src/utils/resolve.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index db8c9b1..84b362b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Imagine you're a time traveler who ends up in a random place and time in history - Interactive world map for selecting a location - Date picker for choosing any year in history - Displays the local calendar year and system for the chosen place and time +- Shows **all** valid answers at once — different people at the same place and time + reckoned dates differently (e.g. colonial Julian alongside an Indigenous lunar calendar) +- A global **modern Gregorian default** for dates from 1927 on, so most recent dates + resolve anywhere — shown alongside any local calendar (e.g. modern Iran → Persian *and* Gregorian) - Educational and fun way to explore world calendars and history ## Tech Stack diff --git a/plan.md b/plan.md index e3e3961..56107a9 100644 --- a/plan.md +++ b/plan.md @@ -319,3 +319,16 @@ This is authentic *in structure*. Its pieces split sharply by cost: Geographic region overlap → show all matched regions. Eras within a region **may overlap in time** → show all concurrent calendars, because "When am I?" is asked of a person and different people legitimately answer differently. +- **Q5 — Modern global default.** ✅ (implemented 2026-06-20, [src/utils/resolve.ts](src/utils/resolve.ts)) + The Gregorian calendar is the world's near-universal *civil* calendar today, so for dates + from **1927** onward (chosen cutoff — just after the last major national adoption, Turkey + 1926) the engine appends a synthetic **"most of the world" Gregorian answer** anywhere on + Earth, unless a region already supplies a Gregorian answer for that date (dedup, no double + Gregorian). So most modern clicks resolve even with no specific region, and a place with a + local calendar shows both (e.g. modern Iran → Persian **and** Gregorian). It is engine-only: + **not** a region polygon, so it never shades the map (a world polygon would flood it); a + matching band is mirrored onto the year timeline. A single global cutoff is a deliberate + simplification (adoption was staggered: 1582/1752/1918/1926/1949/2016…), hence "most of + the world", not a universal claim. The cutoff is one constant (`MODERN_DEFAULT_START_YEAR`). + Open follow-up: the default currently applies over oceans too — could be land-bounded once + the land-clipping work (stretch goals) lands. diff --git a/src/App.tsx b/src/App.tsx index ae0adcd..325acad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import { DatePicker, type YearBand } from './components/DatePicker'; import { ResultPanel } from './components/ResultPanel'; import { Attribution } from './components/Attribution'; import { regions } from './data'; -import { resolve } from './utils/resolve'; +import { resolve, MODERN_DEFAULT_START_YEAR } from './utils/resolve'; import { findEras } from './utils/eras'; import { findRegions } from './utils/geo'; import { parseGregorianDate } from './utils/julianDay'; @@ -73,6 +73,19 @@ function App() { }); } } + // Mirror resolve()'s modern global Gregorian default on the timeline, + // unless a region already supplies a Gregorian band reaching modern times. + const hasModernGregorian = bands.some( + (b) => b.calendar === 'Gregorian' && b.end >= MODERN_DEFAULT_START_YEAR, + ); + if (!hasModernGregorian) { + bands.push({ + start: MODERN_DEFAULT_START_YEAR, + end: Infinity, + color: calendarColor('Gregorian'), + calendar: 'Gregorian', + }); + } return bands; }, [picked]); diff --git a/src/utils/resolve.test.ts b/src/utils/resolve.test.ts index 10402f9..7051f20 100644 --- a/src/utils/resolve.test.ts +++ b/src/utils/resolve.test.ts @@ -59,4 +59,48 @@ describe('resolve', () => { expect(answers[0].era.usedBy).toBe('locals'); expect(answers[0].conversion.convertible).toBe(false); }); + + describe('modern global default', () => { + it('adds a Gregorian default anywhere for modern dates with no region', () => { + const { answers, missReason } = resolve(50, 50, { year: 2000, month: 6, day: 1 }, regions); + expect(missReason).toBeUndefined(); + expect(answers).toHaveLength(1); + expect(answers[0].era.name).toBe('Modern (global civil calendar)'); + expect(answers[0].era.usedBy).toBe('most of the world'); + expect(answers[0].conversion).toMatchObject({ system: 'Gregorian', convertible: true }); + expect(answers[0].sourceFile).toBeUndefined(); // not from a data file + }); + + it('still misses (no-region) for pre-modern dates outside every region', () => { + expect(resolve(50, 50, { year: 1700, month: 1, day: 1 }, regions)).toEqual({ + answers: [], + missReason: 'no-region', + }); + }); + + it('does not duplicate Gregorian when a region already covers the date', () => { + const { answers } = resolve(5, 5, { year: 1990, month: 1, day: 1 }, regions); + expect(answers).toHaveLength(1); // the region's own "Modern" era, no extra default + expect(answers[0].era.name).toBe('Modern'); + expect(answers[0].sourceFile).toBe('src/data/calendars/testland.json'); + }); + + it('shows a local calendar AND the Gregorian default together', () => { + const persia: CalendarRegion[] = [ + { region: { name: 'Persia', geo: square }, eras: [{ name: 'Solar Hijri', start: '1925-03-21', end: null, calendar: 'Persian', usedBy: 'Iranians' }] }, + ]; + const { answers } = resolve(5, 5, { year: 2025, month: 6, day: 1 }, persia); + expect(answers.map((a) => a.conversion.system)).toEqual(['Persian (Solar Hijri)', 'Gregorian']); + }); + + it('falls back to the default when a region matches but its eras have lapsed', () => { + const bounded: CalendarRegion[] = [ + { region: { name: 'X', geo: square }, eras: [{ name: 'E', start: '1000-01-01', end: '1100-01-01', calendar: 'Julian' }] }, + ]; + const { answers, missReason } = resolve(5, 5, { year: 2000, month: 1, day: 1 }, bounded); + expect(missReason).toBeUndefined(); + expect(answers).toHaveLength(1); + expect(answers[0].era.name).toBe('Modern (global civil calendar)'); + }); + }); }); diff --git a/src/utils/resolve.ts b/src/utils/resolve.ts index 34ecfe8..c5cd6b7 100644 --- a/src/utils/resolve.ts +++ b/src/utils/resolve.ts @@ -4,10 +4,52 @@ // could give. Pure (regions are passed in) so it is easy to unit-test; the UI // supplies the loaded calendar data. -import type { CalendarRegion, HistoricalDate, Answer, Resolution } from '../types'; +import type { CalendarRegion, HistoricalDate, Answer, Resolution, Region, Era, Geometry } from '../types'; import { findRegions } from './geo'; import { findEras } from './eras'; import { convert } from './calendars'; +import { gregorianToJulianDay } from './julianDay'; + +// ── Modern global default ───────────────────────────────────────────────────── +// The Gregorian calendar became the world's near-universal *civil* calendar in +// the early 20th century (the last major national adoption was Turkey, 1926). +// From this year we add a Gregorian answer anywhere on Earth, so most modern +// clicks resolve to "what almost everyone says today" even where the dataset has +// no specific region. A single global cutoff is a deliberate simplification: +// adoption was staggered (1582 Catholic Europe, 1752 Britain, 1918 Russia, +// 1926 Turkey, 1949 China, 2016 Saudi civil), so this is "most of the world", +// not a precise universal claim. +export const MODERN_DEFAULT_START_YEAR = 1927; +const MODERN_DEFAULT_START_JDN = gregorianToJulianDay({ year: MODERN_DEFAULT_START_YEAR, month: 1, day: 1 }); + +// A whole-Earth geometry. The default is never added to the shaded map overlays +// (that would flood the map), so this only exists to satisfy the Region shape. +const WORLD_GEO: Geometry = { + type: 'Polygon', + coordinates: [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]], +}; + +export const DEFAULT_MODERN_REGION: Region = { name: 'Most of the world', geo: WORLD_GEO }; + +export const DEFAULT_MODERN_ERA: Era = { + name: 'Modern (global civil calendar)', + start: `${MODERN_DEFAULT_START_YEAR}-01-01`, + end: null, + calendar: 'Gregorian', + usedBy: 'most of the world', + description: + 'By the early 20th century the Gregorian calendar had become the world’s ' + + 'near-universal civil calendar; today almost every country uses it for civil ' + + 'dating, often alongside a local religious or traditional calendar. This is a ' + + 'global default, so its start year is a deliberate simplification — adoption ' + + 'was staggered (e.g. 1582 Catholic Europe, 1752 Britain, 1918 Russia, 1926 Turkey).', + sources: ['https://en.wikipedia.org/wiki/Adoption_of_the_Gregorian_calendar'], +}; + +/** Whether `date` is recent enough for the global Gregorian default to apply. */ +export function isModernDefaultDate(date: HistoricalDate): boolean { + return gregorianToJulianDay(date) >= MODERN_DEFAULT_START_JDN; +} export function resolve( lat: number, @@ -16,7 +58,6 @@ export function resolve( regions: CalendarRegion[], ): Resolution { const matched = findRegions(lat, lng, regions); - if (matched.length === 0) return { answers: [], missReason: 'no-region' }; const answers: Answer[] = []; for (const { region, eras, sourceFile } of matched) { @@ -25,6 +66,19 @@ export function resolve( } } - if (answers.length === 0) return { answers: [], missReason: 'no-era' }; + // Append the modern global Gregorian default — but only when no Gregorian + // answer already applies here (a region with its own Gregorian era covering + // this date), so we never show the same Gregorian year twice. + if (isModernDefaultDate(date) && !answers.some((a) => a.conversion.system === 'Gregorian')) { + answers.push({ + region: DEFAULT_MODERN_REGION, + era: DEFAULT_MODERN_ERA, + conversion: convert(date, DEFAULT_MODERN_ERA.calendar), + }); + } + + if (answers.length === 0) { + return { answers: [], missReason: matched.length === 0 ? 'no-region' : 'no-era' }; + } return { answers }; }