--- Baha'i calendar conversions (arithmetic and astronomical). -- 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-bahai -- @release 0.1 2026-07-19 local M = {} local basic = require("calendrica-basic") local gregorian = require("calendrica-gregorian") local astro = require("calendrica-astro") -- === Constants === -- Fixed date of start of Baha'i calendar (March 21, 1844 Gregorian). local BAHAI_EPOCH = gregorian.fixed_from_gregorian( gregorian.gregorian_date(1844, gregorian.MARCH, 21) ) -- Signifies the intercalary period of 4 or 5 days. local AYYAM_I_HA = 0 -- Location of Tehran for astronomical Baha'i calendar. local BAHAI_LOCATION = astro.location( 35.696111, 51.423056, astro.mt(0), astro.hr(3 + 1/2) ) -- === Date constructor and accessors === -- Baha'i date constructor. local function bahai_date(major, cycle, year, month, day) return {major, cycle, year, month, day} end -- Baha'i date accessors. local function bahai_major(date) return date[1] end local function bahai_cycle(date) return date[2] end local function bahai_year(date) return date[3] end local function bahai_month(date) return date[4] end local function bahai_day(date) return date[5] end -- === Arithmetic Baha'i calendar === --- Fixed date equivalent to the Baha'i date `b_date`. -- @tparam table b_date Baha'i date {major, cycle, year, month, day}. -- @treturn number Fixed date. function M.fixed_from_bahai(b_date) local major = bahai_major(b_date) local cycle = bahai_cycle(b_date) local year = bahai_year(b_date) local month = bahai_month(b_date) local day = bahai_day(b_date) local g_year = 361 * (major - 1) + 19 * (cycle - 1) + year - 1 + gregorian.gregorian_year_from_fixed(BAHAI_EPOCH) local prior_months if month == AYYAM_I_HA then prior_months = 342 elseif month == 19 then if gregorian.gregorian_leap_year(g_year + 1) then prior_months = 347 else prior_months = 346 end else prior_months = 19 * (month - 1) end return gregorian.fixed_from_gregorian( gregorian.gregorian_date(g_year, gregorian.MARCH, 20) ) + prior_months + day end local fixed_from_bahai = M.fixed_from_bahai --- Baha'i date {major, cycle, year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {major, cycle, year, month, day} function M.bahai_from_fixed(date) local g_year = gregorian.gregorian_year_from_fixed(date) local start = gregorian.gregorian_year_from_fixed(BAHAI_EPOCH) local years = g_year - start - (date <= gregorian.fixed_from_gregorian( gregorian.gregorian_date(g_year, gregorian.MARCH, 20) ) and 1 or 0) local major = 1 + basic.quotient(years, 361) local cycle = 1 + basic.quotient(years % 361, 19) local year = 1 + (years % 19) local days = date - fixed_from_bahai(bahai_date(major, cycle, year, 1, 1)) local month if date >= fixed_from_bahai(bahai_date(major, cycle, year, 19, 1)) then month = 19 elseif date >= fixed_from_bahai( bahai_date(major, cycle, year, AYYAM_I_HA, 1) ) then month = AYYAM_I_HA else month = 1 + basic.quotient(days, 19) end local day = date + 1 - fixed_from_bahai(bahai_date(major, cycle, year, month, 1)) return bahai_date(major, cycle, year, month, day) end --- Fixed date of arithmetic Bahá'í New Year (always March 21) in Gregorian year `g_year`. -- For the astronomical version based on the vernal equinox, use `naw_ruz`. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.bahai_new_year(g_year) return gregorian.fixed_from_gregorian( gregorian.gregorian_date(g_year, gregorian.MARCH, 21) ) end -- === Astronomical Baha'i calendar === -- Universal time of sunset on fixed date in BAHAI_LOCATION. local function bahai_sunset(date) return astro.universal_from_standard( astro.sunset(date, BAHAI_LOCATION), BAHAI_LOCATION ) end -- Fixed date of astronomical Baha'i New Year on or before fixed date. local function astro_bahai_new_year_on_or_before(date) local approx = astro.estimate_prior_solar_longitude( astro.SPRING, bahai_sunset(date) ) return basic.next( math.floor(approx) - 1, function(day) return astro.solar_longitude(bahai_sunset(day)) <= astro.SPRING + 2 end ) end --- Fixed date of astronomical Baha'i date `b_date`. -- @tparam table b_date Baha'i date {major, cycle, year, month, day}. -- @treturn number Fixed date. function M.fixed_from_astro_bahai(b_date) local major = bahai_major(b_date) local cycle = bahai_cycle(b_date) local year = bahai_year(b_date) local month = bahai_month(b_date) local day = bahai_day(b_date) local years = 361 * (major - 1) + 19 * (cycle - 1) + year if month == 19 then return astro_bahai_new_year_on_or_before( BAHAI_EPOCH + math.floor(astro.MEAN_TROPICAL_YEAR * (years + 1/2)) ) - 20 + day elseif month == AYYAM_I_HA then return astro_bahai_new_year_on_or_before( BAHAI_EPOCH + math.floor(astro.MEAN_TROPICAL_YEAR * (years - 1/2)) ) + 341 + day else return astro_bahai_new_year_on_or_before( BAHAI_EPOCH + math.floor(astro.MEAN_TROPICAL_YEAR * (years - 1/2)) ) + 19 * (month - 1) + day - 1 end end local fixed_from_astro_bahai = M.fixed_from_astro_bahai --- Astronomical Baha'i date {major,cycle,year,month,day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {major, cycle, year, month, day} function M.astro_bahai_from_fixed(date) local new_year = astro_bahai_new_year_on_or_before(date) local years = basic.round_half_to_even( (new_year - BAHAI_EPOCH) / astro.MEAN_TROPICAL_YEAR ) local major = 1 + basic.quotient(years, 361) local cycle = 1 + basic.quotient(years % 361, 19) local year = 1 + (years % 19) local days = date - new_year local month if date >= fixed_from_astro_bahai( bahai_date(major, cycle, year, 19, 1) ) then month = 19 elseif date >= fixed_from_astro_bahai( bahai_date(major, cycle, year, AYYAM_I_HA, 1) ) then month = AYYAM_I_HA else month = 1 + basic.quotient(days, 19) end local day = date + 1 - fixed_from_astro_bahai(bahai_date(major, cycle, year, month, 1)) return bahai_date(major, cycle, year, month, day) end -- === Baha'i holidays === --- Fixed date of Bahá'í New Year (Naw-Rúz) in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.naw_ruz(g_year) return astro_bahai_new_year_on_or_before( gregorian.gregorian_new_year(g_year + 1) ) end local naw_ruz = M.naw_ruz --- Fixed date of the Feast of Riḍván in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.feast_of_ridvan(g_year) return naw_ruz(g_year) + 31 end --- Fixed date of the Birthday of the Báb in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.birth_of_the_bab(g_year) local ny = naw_ruz(g_year) local set1 = bahai_sunset(ny) local m1 = astro.new_moon_at_or_after(set1) local m8 = astro.new_moon_at_or_after(m1 + 190) local day = basic.fixed_from_moment(m8) local set8 = bahai_sunset(day) if m8 < set8 then return day + 1 else return day + 2 end end -- === Exports === --- Intercalary month number (0) inserted before the last month of the year. M.AYYAM_I_HA = AYYAM_I_HA return M