--- French Revolutionary calendar conversions (astronomical and arithmetic). -- Ported from "Calendrical Calculations" (4th edition) -- by Nachum Dershowitz and Edward M. Reingold. -- Original Lisp code (CALENDRICA 4.0) is Apache 2.0 licensed. -- @module calendrica-french-revolutionary -- @release 0.1 2026-07-19 local M = {} local basic = require("calendrica-basic") local gregorian = require("calendrica-gregorian") local astro = require("calendrica-astro") -- === Epoch and location === -- French Revolutionary epoch: September 22, 1792 (Gregorian). local FRENCH_EPOCH = gregorian.fixed_from_gregorian( gregorian.gregorian_date(1792, gregorian.SEPTEMBER, 22) ) -- Location of Paris Observatory. local PARIS = astro.location( astro.angle(48, 50, 11), astro.angle(2, 20, 15), astro.mt(27), astro.hr(1) ) -- === Date constructor === -- French Revolutionary date constructor. local function french_date(year, month, day) return {year, month, day} end -- === Astronomical French Revolutionary calendar === -- Universal time of true midnight at end of date in Paris. local function midnight_in_paris(date) return astro.midnight(date + 1, PARIS) end -- French New Year on or before date (autumnal equinox in Paris). local function french_new_year_on_or_before(date) local approx = astro.estimate_prior_solar_longitude( astro.AUTUMN, midnight_in_paris(date) ) return basic.next( math.floor(approx) - 1, function(day) return astro.AUTUMN <= astro.solar_longitude(midnight_in_paris(day)) end ) end --- Fixed date of astronomical French Revolutionary date `f_date`. -- @tparam table f_date French date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_french(f_date) local month = basic.standard_month(f_date) local day = basic.standard_day(f_date) local year = basic.standard_year(f_date) local new_year = french_new_year_on_or_before( math.floor( FRENCH_EPOCH + 180 -- Spring after epoch. + astro.MEAN_TROPICAL_YEAR * (year - 1) ) ) return new_year - 1 -- Days in prior years. + 30 * (month - 1) -- Days in prior months. + day -- Days this month. end local fixed_from_french = M.fixed_from_french --- Astronomical French Revolutionary date {year, month, day} for fixed `date`. -- Uses `round_half_to_even` to match Lisp's `round`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.french_from_fixed(date) local new_year = french_new_year_on_or_before(date) local year = 1 + basic.round_half_to_even( (new_year - FRENCH_EPOCH) / astro.MEAN_TROPICAL_YEAR ) local month = 1 + basic.quotient(date - new_year, 30) local day = 1 + (date - new_year) % 30 return french_date(year, month, day) end --- True if `f_year` is an astronomical French Revolutionary leap year. -- @tparam number f_year French Revolutionary year. -- @treturn boolean function M.french_leap_year(f_year) return fixed_from_french(french_date(f_year + 1, 1, 1)) - fixed_from_french(french_date(f_year, 1, 1)) > 365 end -- === Arithmetic French Revolutionary calendar === --- True if `f_year` is an arithmetic French Revolutionary leap year. -- @tparam number f_year French Revolutionary year. -- @treturn boolean function M.arithmetic_french_leap_year(f_year) return f_year % 4 == 0 and (f_year % 400 == 0 or f_year % 100 ~= 0) and f_year % 4000 ~= 0 end --- Fixed date of Arithmetic French Revolutionary date `f_date`. -- @tparam table f_date Arithmetic French date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_arithmetic_french(f_date) local month = basic.standard_month(f_date) local day = basic.standard_day(f_date) local year = basic.standard_year(f_date) return FRENCH_EPOCH - 1 -- Days before start of calendar. + 365 * (year - 1) -- Ordinary days in prior years. + basic.quotient(year - 1, 4) -- Leap days in prior years. - basic.quotient(year - 1, 100) -- Minus century years. + basic.quotient(year - 1, 400) -- Plus 400-year cycles. - basic.quotient(year - 1, 4000) -- Minus 4000-year cycles. + 30 * (month - 1) -- Days in prior months this year. + day -- Days this month. end local fixed_from_arithmetic_french = M.fixed_from_arithmetic_french --- Arithmetic French Revolutionary date {year, month, day} of fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.arithmetic_french_from_fixed(date) -- Approximate year (may be off by 1). local approx = 1 + basic.quotient( date - FRENCH_EPOCH + 2, 1460969 / 4000 ) local year if date < fixed_from_arithmetic_french(french_date(approx, 1, 1)) then year = approx - 1 else year = approx end local month = 1 + basic.quotient( date - fixed_from_arithmetic_french(french_date(year, 1, 1)), 30 ) local day = 1 + date - fixed_from_arithmetic_french(french_date(year, month, 1)) return french_date(year, month, day) end return M