fuzzyworld

when-am-i / commit

Add French Republican calendar, with a sourced-lookup-table converter

c571572f1347ab4e6387d098b6fc314bd05d76d5 · Pavel Safronov · 2026-06-20 22:40:53 -0700 · parents 76b2fa1bbf56

Add French Republican calendar, with a sourced-lookup-table converter

Hand-rolled converter for the French Republican calendar's year (Years I
through XIV, plus the brief 1871 Paris Commune revival as Year LXXIX),
using a sourced table of each year's actual equinox-based start date since
the leap-year rule was an astronomical observation, not a fixed formula —
only the year is computed, not month/day. Covers the Kingdom of
France/First Republic's pre- and post-revolutionary Gregorian eras too.

Updates README and the new-calendar skill's convertible-calendar lists for
this plus the Ottoman/Maori additions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 .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 @@
 [![pipeline status](https://gitlab.com/PavelSafronov/when-am-i/badges/main/pipeline.svg)](https://gitlab.com/PavelSafronov/when-am-i/-/pipelines)
 [![live demo](https://img.shields.io/badge/demo-live-success)](https://pavelsafronov.gitlab.io/when-am-i)
 ![version](https://img.shields.io/badge/version-v0.1.0-blue)
-![calendar data](https://img.shields.io/badge/calendar%20data-11-orange)
+![calendar data](https://img.shields.io/badge/calendar%20data-15-orange)
 ![license](https://img.shields.io/badge/license-MIT-green)
 
 ![React](https://img.shields.io/badge/React-20232A?logo=react&logoColor=61DAFB)
@@ -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) {