fuzzyworld

when-am-i / commit

feat: modern global Gregorian default for recent dates

b92a4280cd18ec05fa87132d538be1fc10a6e7d0 · Pavel Safronov · 2026-06-20 12:15:11 -0700 · parents 091a00cbe3a2

Claude Code usage: this commit ~164.7K out / ~24.3M cached · cost unknown (no per-model trailer) · session 2 tally 28.7K in / 459.5K out / 53.7M cached · usage by commit and session

feat: modern global Gregorian default for recent dates

For dates from 1927 on (just after the last major national adoption, Turkey
1926), resolve() appends a synthetic "most of the world" Gregorian answer
anywhere on Earth — so most modern clicks resolve even where the dataset has no
specific region, and a place with a local calendar shows both (e.g. modern Iran
-> Persian and Gregorian).

- Engine-only (src/utils/resolve.ts): not a region polygon, so it never shades
  the map; deduped so a region that already has a Gregorian era doesn't double up.
- Mirrors a Gregorian band onto the year timeline (App.tsx).
- A single global cutoff is a deliberate simplification (adoption was staggered:
  1582/1752/1918/1926/1949/2016...), hence "most of the world"; the cutoff is one
  constant, MODERN_DEFAULT_START_YEAR. Recorded as decision Q5 in plan.md.

Usage: 28.7K in / 459.5K out / 53.7M cached (session tally)
rtk:   88.1K saved, 86.5% over 182 cmds (global/lifetime)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
 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 };
 }