when-am-i / commit
Add French Republican calendar, with a sourced-lookup-table converter
.claude/skills/new-calendar/SKILL.md | 6 +- README.md | 10 +-- .../revolutionary-france/01-gregorian.png | Bin 0 -> 431073 bytes .../02-french-republican-calendar.png | Bin 0 -> 500536 bytes .../03-french-republican-calendar.png | Bin 0 -> 423215 bytes .../revolutionary-france/04-gregorian.png | Bin 0 -> 426264 bytes src/data/calendars/revolutionary-france.json | 76 +++++++++++++++++++++ src/utils/calendars.test.ts | 17 +++++ src/utils/calendars.ts | 44 ++++++++++++ 9 files changed, 147 insertions(+), 6 deletions(-) diff --git a/.claude/skills/new-calendar/SKILL.md b/.claude/skills/new-calendar/SKILL.md index 398722a..0bd804b 100644 --- a/.claude/skills/new-calendar/SKILL.md +++ b/.claude/skills/new-calendar/SKILL.md @@ -54,8 +54,10 @@ This is **two independent gates** (see plan.md, Q3): year. This is decided by `src/utils/calendars.ts`: - **Convertible** strings hit a converter there: `Gregorian`, `Julian`, and the Temporal-backed `Islamic`, `Hebrew`, `Persian`, `Indian`/`Saka`, `Buddhist`, - `Japanese`, `Ethiopic`, plus the hand-rolled `Chinese sexagenary cycle` and - `Maya Long Count`. + `Japanese`, `Ethiopic`, plus the hand-rolled `Chinese sexagenary cycle`, + `Maya Long Count`, and `French Republican calendar` (year only, via a + sourced lookup table — its year began at the Paris equinox, an + observation, not a fixed rule). Match an existing string **exactly** (the lookup is case/space-insensitive). - **Descriptive-only** — a calendar with no continuous numbered year (e.g. an observational lunar calendar). Use a clear `calendar` name; the engine shows diff --git a/README.md b/README.md index d0dbc11..8b3846e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [](https://gitlab.com/PavelSafronov/when-am-i/-/pipelines) [](https://pavelsafronov.gitlab.io/when-am-i)  - +   @@ -50,11 +50,13 @@ and show the Gregorian year for reference). - **Convertible:** Gregorian, Julian, Islamic (Umm al-Qura), Hebrew, Persian (Solar Hijri), Indian (Saka), Buddhist, Japanese (nengō, Meiji onward), - Ethiopian, Maya Long Count. + Ethiopian, Maya Long Count, French Republican calendar (year only, from a + sourced table of each year's actual equinox-based start date). - **Descriptive-only:** the Chinese sexagenary cycle (the named 60-year stem-branch / zodiac-animal year), the Japanese pre-1873 lunisolar calendar, - the Tzolk'in/Haab' Calendar Round, and observational lunar calendars such as - the Algonquian (Massachusett) reckoning. + the Tzolk'in/Haab' Calendar Round, the Ottoman Rumi fiscal calendar, the + Māori maramataka, and observational lunar calendars such as the Algonquian + (Massachusett) reckoning. ## Tech Stack diff --git a/screenshots/calendars/revolutionary-france/01-gregorian.png b/screenshots/calendars/revolutionary-france/01-gregorian.png new file mode 100644 index 0000000..e3ddcbe Binary files /dev/null and b/screenshots/calendars/revolutionary-france/01-gregorian.png differ diff --git a/screenshots/calendars/revolutionary-france/02-french-republican-calendar.png b/screenshots/calendars/revolutionary-france/02-french-republican-calendar.png new file mode 100644 index 0000000..046078d Binary files /dev/null and b/screenshots/calendars/revolutionary-france/02-french-republican-calendar.png differ diff --git a/screenshots/calendars/revolutionary-france/03-french-republican-calendar.png b/screenshots/calendars/revolutionary-france/03-french-republican-calendar.png new file mode 100644 index 0000000..467e9fe Binary files /dev/null and b/screenshots/calendars/revolutionary-france/03-french-republican-calendar.png differ diff --git a/screenshots/calendars/revolutionary-france/04-gregorian.png b/screenshots/calendars/revolutionary-france/04-gregorian.png new file mode 100644 index 0000000..4bf5858 Binary files /dev/null and b/screenshots/calendars/revolutionary-france/04-gregorian.png differ diff --git a/src/data/calendars/revolutionary-france.json b/src/data/calendars/revolutionary-france.json new file mode 100644 index 0000000..fbb5144 --- /dev/null +++ b/src/data/calendars/revolutionary-france.json @@ -0,0 +1,76 @@ +{ + "region": { + "name": "France (mainland)", + "geo": { + "type": "Polygon", + "coordinates": [ + [ + [2.5, 51.0], + [4.8, 49.9], + [8.2, 49.0], + [7.6, 47.6], + [7.6, 45.9], + [6.8, 44.0], + [7.6, 43.7], + [4.6, 43.3], + [3.0, 42.4], + [-1.8, 43.4], + [-1.3, 44.5], + [-2.0, 47.0], + [-4.8, 48.4], + [-1.6, 49.7], + [1.6, 50.9], + [2.5, 51.0] + ] + ] + } + }, + "eras": [ + { + "name": "Pre-revolutionary era", + "start": null, + "end": "1792-09-22", + "calendar": "Gregorian", + "usedBy": "the Kingdom of France", + "description": "France used the Gregorian calendar (adopted by Catholic France in 1582, among the first countries to do so) until the Republican calendar replaced it.", + "sources": [ + "https://en.wikipedia.org/wiki/Adoption_of_the_Gregorian_calendar", + "https://en.wikipedia.org/wiki/French_Republican_calendar" + ] + }, + { + "name": "French Republican calendar", + "start": "1792-09-22", + "end": "1806-01-01", + "calendar": "French Republican calendar", + "usedBy": "the First French Republic", + "description": "Adopted by the National Convention on 24 October 1793 and applied retroactively to the establishment of the Republic on 22 September 1792 (epoch of An I). Twelve 30-day months grouped by season (Vendemiaire, Brumaire, Frimaire; Nivose, Pluviose, Ventose; Germinal, Floreal, Prairial; Messidor, Thermidor, Fructidor) plus five or six extra 'Sansculottides' days. The year was meant to begin at the Paris autumnal equinox, determined by observation rather than a fixed rule, so leap years (an extra sixth day) were irregular — only Years III, VII and XI were observed as leap years before Napoleon abolished the calendar effective 1 January 1806.", + "sources": [ + "https://en.wikipedia.org/wiki/French_Republican_calendar" + ] + }, + { + "name": "Paris Commune revival", + "start": "1871-05-06", + "end": "1871-05-24", + "calendar": "French Republican calendar", + "usedBy": "the Paris Commune", + "description": "The Republican calendar was briefly revived for some dates in the Journal officiel during the Paris Commune, 6-23 May 1871 (16 Floreal-3 Prairial, An LXXIX), continuing the original year count from 1792.", + "sources": [ + "https://en.wikipedia.org/wiki/French_Republican_calendar", + "https://en.wikipedia.org/wiki/Paris_Commune" + ] + }, + { + "name": "Post-revolutionary era", + "start": "1806-01-01", + "end": null, + "calendar": "Gregorian", + "usedBy": "France", + "description": "Napoleon restored the Gregorian calendar effective 1 January 1806, and France has used it since (alongside the brief 1871 Commune revival of the Republican calendar).", + "sources": [ + "https://en.wikipedia.org/wiki/French_Republican_calendar" + ] + } + ] +} diff --git a/src/utils/calendars.test.ts b/src/utils/calendars.test.ts index 9316402..b3adc1d 100644 --- a/src/utils/calendars.test.ts +++ b/src/utils/calendars.test.ts @@ -99,6 +99,23 @@ describe('convert', () => { expect(c.localYearLabel).toBe('1500'); // Gregorian reference }); + it('converts to the French Republican year (hand-rolled lookup table)', () => { + // Year I began 1792-09-22; the Thermidorian Reaction was 9 Thermidor An II = 1794-07-27. + expect(convert({ year: 1792, month: 9, day: 22 }, 'French Republican calendar').localYearLabel).toBe('An I'); + expect(convert({ year: 1794, month: 7, day: 27 }, 'French Republican calendar')).toMatchObject({ + convertible: true, + localYearLabel: 'An II', + }); + // The brief 1871 Paris Commune revival. + expect(convert({ year: 1871, month: 5, day: 10 }, 'French Republican calendar').localYearLabel).toBe('An LXXIX'); + }); + + it('French Republican calendar falls back to descriptive-only outside its periods of use', () => { + const c = convert({ year: 1850, month: 1, day: 1 }, 'French Republican calendar'); + expect(c.convertible).toBe(false); + expect(c.localYearLabel).toBe('1850'); // Gregorian reference + }); + it('falls back to descriptive-only for unknown calendars', () => { const c = convert({ year: 1700, month: 4, day: 1 }, 'Some Unknown Calendar'); expect(c.convertible).toBe(false); diff --git a/src/utils/calendars.ts b/src/utils/calendars.ts index 02d15e8..51cbe33 100644 --- a/src/utils/calendars.ts +++ b/src/utils/calendars.ts @@ -268,6 +268,49 @@ function longCountConverter(date: HistoricalDate): CalendarConversion { }; } +// ── French Republican calendar (hand-rolled, lookup table) ────────────────── +// The Republican year began at the Paris autumnal equinox — an astronomical +// observation, not a fixed arithmetic rule (years III, VII and XI were the +// only leap years actually observed before Napoleon abolished the calendar on +// 1 January 1806). Rather than invent an equinox-prediction formula, we use +// the sourced table of each year's actual Gregorian start date (Wikipedia, +// "French Republican calendar") to determine the year number; we do not +// attempt the month/day (those depend on the same per-year equinox timing we +// aren't computing). Year LXXIX is the brief 1871 Paris Commune revival. +const FRENCH_REPUBLICAN_YEAR_STARTS: { start: string; end: string; label: string }[] = [ + { start: '1792-09-22', end: '1793-09-22', label: 'An I' }, + { start: '1793-09-22', end: '1794-09-22', label: 'An II' }, + { start: '1794-09-22', end: '1795-09-23', label: 'An III' }, + { start: '1795-09-23', end: '1796-09-22', label: 'An IV' }, + { start: '1796-09-22', end: '1797-09-22', label: 'An V' }, + { start: '1797-09-22', end: '1798-09-22', label: 'An VI' }, + { start: '1798-09-22', end: '1799-09-23', label: 'An VII' }, + { start: '1799-09-23', end: '1800-09-23', label: 'An VIII' }, + { start: '1800-09-23', end: '1801-09-23', label: 'An IX' }, + { start: '1801-09-23', end: '1802-09-23', label: 'An X' }, + { start: '1802-09-23', end: '1803-09-24', label: 'An XI' }, + { start: '1803-09-24', end: '1804-09-23', label: 'An XII' }, + { start: '1804-09-23', end: '1805-09-23', label: 'An XIII' }, + { start: '1805-09-23', end: '1806-01-01', label: 'An XIV' }, + { start: '1870-09-23', end: '1871-05-24', label: 'An LXXIX' }, +]; + +function frenchRepublicanConverter(date: HistoricalDate): CalendarConversion { + const jd = gregorianToJulianDay(date); + const hit = FRENCH_REPUBLICAN_YEAR_STARTS.find( + ({ start, end }) => jd >= gregorianToJulianDay(parseGregorianDate(start)) && jd < gregorianToJulianDay(parseGregorianDate(end)), + ); + if (!hit) { + return descriptiveOnly(date, 'French Republican calendar', 'Outside the calendar’s documented periods of use; showing the Gregorian year for reference.'); + } + return { + system: 'French Republican calendar', + localYearLabel: hit.label, + convertible: true, + note: 'The Republican year began at the autumnal equinox observed at the Paris Observatory, not a fixed arithmetic date, so only the year (from a sourced table of each year’s actual start date) is computed here — not the month or day.', + }; +} + const CONVERTERS: Record<string, Converter> = { gregorian: (date) => ({ system: 'Gregorian', @@ -290,6 +333,7 @@ const CONVERTERS: Record<string, Converter> = { 'chinese sexagenary cycle': sexagenaryConverter, chinese: sexagenaryConverter, 'maya long count': longCountConverter, + 'french republican calendar': frenchRepublicanConverter, }; for (const { names, def } of TEMPORAL_CALENDARS) {