--- Coptic and Ethiopic calendar conversions. -- 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-coptic-ethiopic -- @release 0.1 2026-07-19 local M = {} local basic = require("calendrica-basic") local julian = require("calendrica-julian") local gregorian = require("calendrica-gregorian") -- === Epochs === -- Coptic epoch: August 29, 284 CE (Julian). local COPTIC_EPOCH = julian.fixed_from_julian( julian.julian_date(julian.ce(284), julian.AUGUST, 29) ) -- Ethiopic epoch: August 29, 8 CE (Julian). local ETHIOPIC_EPOCH = julian.fixed_from_julian( julian.julian_date(julian.ce(8), julian.AUGUST, 29) ) local function coptic_date(year, month, day) return {year, month, day} end --- True if `c_year` is a Coptic leap year. -- @tparam number c_year Coptic year. -- @treturn boolean function M.coptic_leap_year(c_year) return c_year % 4 == 3 end -- === Coptic conversion === --- Fixed date of Coptic date `c_date`. -- @tparam table c_date Coptic date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_coptic(c_date) local month = basic.standard_month(c_date) local day = basic.standard_day(c_date) local year = basic.standard_year(c_date) return COPTIC_EPOCH - 1 -- Days before start of calendar. + 365 * (year - 1) -- Ordinary days in prior years. + basic.quotient(year, 4) -- Leap days in prior years. + 30 * (month - 1) -- Days in prior months this year. + day -- Days so far this month. end local fixed_from_coptic = M.fixed_from_coptic --- Coptic date {year, month, day} equivalent to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.coptic_from_fixed(date) local year = basic.quotient( 4 * (date - COPTIC_EPOCH) + 1463, 1461 ) local month = 1 + basic.quotient( date - fixed_from_coptic(coptic_date(year, 1, 1)), 30 ) local day = date + 1 - fixed_from_coptic(coptic_date(year, month, 1)) return coptic_date(year, month, day) end local coptic_from_fixed = M.coptic_from_fixed -- === Ethiopic date constructor === -- Ethiopic date constructor. local function ethiopic_date(year, month, day) -- luacheck: ignore return {year, month, day} end -- === Ethiopic conversion === --- Fixed date of Ethiopic date `e_date`. -- Same structure as the Coptic calendar with a different epoch. -- @tparam table e_date Ethiopic date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_ethiopic(e_date) local month = basic.standard_month(e_date) local day = basic.standard_day(e_date) local year = basic.standard_year(e_date) return ETHIOPIC_EPOCH + fixed_from_coptic(coptic_date(year, month, day)) - COPTIC_EPOCH end --- Ethiopic date {year, month, day} equivalent to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.ethiopic_from_fixed(date) return coptic_from_fixed(date + COPTIC_EPOCH - ETHIOPIC_EPOCH) end -- === Coptic date in Gregorian year === --- Fixed dates of Coptic month `c_month`, day `c_day` that fall in Gregorian year `g_year`. -- @tparam number c_month Coptic month. -- @tparam number c_day Coptic day. -- @tparam number g_year Gregorian year. -- @treturn table List of fixed dates (0 or 1 entries). function M.coptic_in_gregorian(c_month, c_day, g_year) local jan1 = gregorian.gregorian_new_year(g_year) local y = basic.standard_year(coptic_from_fixed(jan1)) local date0 = fixed_from_coptic(coptic_date(y, c_month, c_day)) local date1 = fixed_from_coptic(coptic_date(y + 1, c_month, c_day)) return basic.list_range( {date0, date1}, gregorian.gregorian_year_range(g_year) ) end local coptic_in_gregorian = M.coptic_in_gregorian -- === Coptic Christmas === --- Fixed dates of Coptic Christmas (Coptic month 4, day 29) in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates of Coptic Christmas in the year. function M.coptic_christmas(g_year) return coptic_in_gregorian(4, 29, g_year) end return M