--- Astronomical lunar calendar conversions (Babylonian, Observational Islamic/Hebrew, Samaritan). -- 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-astronomical-lunar -- @release 0.1 2026-07-19 local M = {} local basic = require("calendrica-basic") local gregorian = require("calendrica-gregorian") local julian = require("calendrica-julian") local astro = require("calendrica-astro") local islamic = require("calendrica-islamic") local hebrew = require("calendrica-hebrew") -- === Locations === -- Location of Babylon. local BABYLON = astro.location(32.4794, 44.4328, astro.mt(26), astro.hr(3 + 1/2)) -- Sample location for Observational Islamic calendar (Cairo, Egypt). local ISLAMIC_LOCATION = astro.location(30.1, 31.3, astro.mt(200), astro.hr(2)) -- Location of Ankara, Turkey (used for Turkish Diyanet crescent sighting). local ANKARA = astro.location(39.93, 32.85, astro.mt(938), astro.hr(3)) -- Location of Jerusalem. local JERUSALEM = astro.location(31.78, 35.24, astro.mt(740), astro.hr(2)) -- Location of Acre. local ACRE = astro.location(32.94, 35.09, astro.mt(22), astro.hr(2)) -- luacheck: ignore -- Sample location for Observational Hebrew calendar (Haifa, Israel). local HEBREW_LOCATION = astro.location(32.82, 35, astro.mt(0), astro.hr(2)) -- Location of Mt. Gerizim (Samaritan location). local SAMARITAN_LOCATION = astro.location(32.1994, 35.2728, astro.mt(881), astro.hr(2)) -- Forward declaration (used by phasis_on_or_before/after before definition). local visible_crescent -- === Babylonian calendar === -- Fixed date of start of the Babylonian calendar (Seleucid era; April 3, 311 BCE Julian). local BABYLONIAN_EPOCH = julian.fixed_from_julian( julian.julian_date(julian.bce(311), julian.APRIL, 3) ) -- Babylonian date constructor. local function babylonian_date(year, month, leap, day) return {year, month, leap, day} end -- Babylonian date accessors. local function babylonian_year(date) return date[1] end local function babylonian_month(date) return date[2] end local function babylonian_leap(date) return date[3] end local function babylonian_day(date) return date[4] end -- True if b_year is a leap year on the Babylonian calendar. local function babylonian_leap_year(b_year) return (7 * b_year + 13) % 19 < 7 end -- Time between sunset and moonset on date at location; bogus if no sunset. local function moonlag(date, location) local sun = astro.sunset(date, location) local moon = astro.moonset(date, location) if sun == basic.BOGUS then return basic.BOGUS elseif moon == basic.BOGUS then return astro.hr(24) else return moon - sun end end -- Moonlag criterion for visibility of crescent moon on eve of date in Babylon. local function babylonian_criterion(date) local set = astro.sunset(date - 1, BABYLON) local tee = astro.universal_from_standard(set, BABYLON) local phase = astro.lunar_phase(tee) return phase > astro.NEW and phase < astro.FIRST_QUARTER and astro.new_moon_before(tee) <= tee - astro.hr(24) and moonlag(date - 1, BABYLON) > astro.mn(48) end -- Fixed date of start of Babylonian month on or before date (moonset lag criterion). local function babylonian_new_month_on_or_before(date) local moon = basic.fixed_from_moment( astro.lunar_phase_at_or_before(astro.NEW, date) ) local age = date - moon local tau if age <= 3 and not babylonian_criterion(date) then tau = moon - 30 else tau = moon end return basic.next(tau, function(d) return babylonian_criterion(d) end) end --- Fixed date equivalent to Babylonian date `b_date`. -- @tparam table b_date Babylonian date {year, month, leap, day}. -- @treturn number Fixed date. function M.fixed_from_babylonian(b_date) local month = babylonian_month(b_date) local leap = babylonian_leap(b_date) local day = babylonian_day(b_date) local year = babylonian_year(b_date) local month1 if leap or (year % 19 == 18 and month > 6) then month1 = month else month1 = month - 1 end local months = basic.quotient((year - 1) * 235 + 13, 19) + month1 local midmonth = BABYLONIAN_EPOCH + basic.round_half_to_even(astro.MEAN_SYNODIC_MONTH * months) + 15 return babylonian_new_month_on_or_before(midmonth) + day - 1 end --- Babylonian date {year, month, leap, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, leap, day} function M.babylonian_from_fixed(date) local crescent = babylonian_new_month_on_or_before(date) local months = basic.round_half_to_even( (crescent - BABYLONIAN_EPOCH) / astro.MEAN_SYNODIC_MONTH ) local year = 1 + basic.quotient(19 * months + 5, 235) local approx = BABYLONIAN_EPOCH + basic.round_half_to_even( basic.quotient((year - 1) * 235 + 13, 19) * astro.MEAN_SYNODIC_MONTH ) local new_year = babylonian_new_month_on_or_before(approx + 15) local month1 = 1 + basic.round_half_to_even( (crescent - new_year) / 29.5 ) local special = year % 19 == 18 local leap if special then leap = month1 == 7 else leap = month1 == 13 end local month = (leap or (special and month1 > 6)) and month1 - 1 or month1 local day = date - crescent + 1 return babylonian_date(year, month, leap, day) end -- === Crescent visibility helpers === -- Closest fixed date on or before date when crescent first became visible at location. local function phasis_on_or_before(date, location) local moon = basic.fixed_from_moment( astro.lunar_phase_at_or_before(astro.NEW, date) ) local age = date - moon local tau if age <= 3 and not visible_crescent(date, location) then tau = moon - 30 else tau = moon end return basic.next(tau, function(d) return visible_crescent(d, location) end) end -- Closest fixed date on or after date when crescent first became visible at location. local function phasis_on_or_after(date, location) local moon = basic.fixed_from_moment( astro.lunar_phase_at_or_before(astro.NEW, date) ) local age = date - moon local tau if age >= 4 or visible_crescent(date - 1, location) then tau = moon + 29 else tau = date end return basic.next(tau, function(d) return visible_crescent(d, location) end) end -- === Solar altitude (belongs conceptually in calendrica-astro but defined here) === -- Geocentric altitude of sun in degrees at tee at location, ignoring parallax/refraction. local function solar_altitude(tee, location) local phi = astro.latitude(location) local psi = astro.longitude(location) local lambda = astro.solar_longitude(tee) local alpha = astro.right_ascension(tee, 0, lambda) local delta = astro.declination(tee, 0, lambda) local theta0 = astro.sidereal_from_moment(tee) local cap_H = (theta0 + psi - alpha) % 360 local altitude = astro.arcsin_degrees( astro.sin_degrees(phi) * astro.sin_degrees(delta) + astro.cos_degrees(phi) * astro.cos_degrees(delta) * astro.cos_degrees(cap_H) ) return basic.mod3(altitude, -180, 180) end -- === Crescent visibility criteria === -- Angular separation of sun and moon at moment tee. local function arc_of_light(tee) return astro.arccos_degrees( astro.cos_degrees(astro.lunar_latitude(tee)) * astro.cos_degrees(astro.lunar_phase(tee)) ) end -- Angular difference in altitudes of sun and moon at tee at location. local function arc_of_vision(tee, location) return astro.lunar_altitude(tee, location) - solar_altitude(tee, location) end -- Topocentric lunar semi-diameter at moment tee at location. local function lunar_semi_diameter(tee, location) local h = astro.lunar_altitude(tee, location) local p = astro.lunar_parallax(tee, location) return 0.27245 * p * (1 + astro.sin_degrees(h) * astro.sin_degrees(p)) end -- Best viewing time (UT) in the evening, simple version. local function simple_best_view(date, location) local dark = astro.dusk(date, location, 4.5) local best = dark == basic.BOGUS and date + 1 or dark return astro.universal_from_standard(best, location) end -- Best viewing time (UT) in the evening, Yallop/Bruin (1977) version. local function bruin_best_view(date, location) local sun = astro.sunset(date, location) local moon = astro.moonset(date, location) local best if sun == basic.BOGUS or moon == basic.BOGUS then best = date + 1 else best = (5/9) * sun + (4/9) * moon end return astro.universal_from_standard(best, location) end -- Shaukat criterion for likely crescent visibility on eve of date at location. local function shaukat_criterion(date, location) local tee = simple_best_view(date - 1, location) local phase = astro.lunar_phase(tee) local h = astro.lunar_altitude(tee, location) local cap_ARCL = arc_of_light(tee) return phase > astro.NEW and phase < astro.FIRST_QUARTER and cap_ARCL >= 10.6 and cap_ARCL <= 90 and h > 4.1 end -- Yallop criterion for possible crescent visibility on eve of date at location. local function yallop_criterion(date, location) local tee = bruin_best_view(date - 1, location) local phase = astro.lunar_phase(tee) local cap_D = lunar_semi_diameter(tee, location) local cap_ARCL = arc_of_light(tee) local cap_W = cap_D * (1 - astro.cos_degrees(cap_ARCL)) local cap_ARCV = arc_of_vision(tee, location) local e = -0.14 local q1 = basic.poly(cap_W, {11.8371, -6.3226, 0.7319, -0.1018}) return phase > astro.NEW and phase < astro.FIRST_QUARTER and cap_ARCV > q1 + e end -- Criterion for possible crescent visibility on eve of date at location (Shaukat's rule). visible_crescent = function(date, location) return shaukat_criterion(date, location) end -- === Observational Islamic calendar === --- Fixed date equivalent to Observational Islamic date `i_date`. -- @tparam table i_date Islamic date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_observational_islamic(i_date) local month = basic.standard_month(i_date) local day = basic.standard_day(i_date) local year = basic.standard_year(i_date) local midmonth = islamic.ISLAMIC_EPOCH + math.floor( ((year - 1) * 12 + month - 1/2) * astro.MEAN_SYNODIC_MONTH ) return phasis_on_or_before(midmonth, ISLAMIC_LOCATION) + day - 1 end --- Observational Islamic date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.observational_islamic_from_fixed(date) local crescent = phasis_on_or_before(date, ISLAMIC_LOCATION) local elapsed_months = basic.round_half_to_even( (crescent - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH ) local year = 1 + basic.quotient(elapsed_months, 12) local month = 1 + elapsed_months % 12 local day = 1 + date - crescent return islamic.islamic_date(year, month, day) end -- Saudi visibility criterion on eve of fixed date in Mecca. local function saudi_criterion(date) local set = astro.sunset(date - 1, astro.MECCA) local tee = astro.universal_from_standard(set, astro.MECCA) local phase = astro.lunar_phase(tee) return phase > astro.NEW and phase < astro.FIRST_QUARTER and moonlag(date - 1, astro.MECCA) > 0 end -- Closest fixed date on or before date when Saudi visibility criterion held. local function saudi_new_month_on_or_before(date) local moon = basic.fixed_from_moment( astro.lunar_phase_at_or_before(astro.NEW, date) ) local age = date - moon local tau if age <= 3 and not saudi_criterion(date) then tau = moon - 30 else tau = moon end return basic.next(tau, function(d) return saudi_criterion(d) end) end --- Fixed date equivalent to Saudi Islamic date `s_date`. -- @tparam table s_date Islamic date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_saudi_islamic(s_date) local month = basic.standard_month(s_date) local day = basic.standard_day(s_date) local year = basic.standard_year(s_date) local midmonth = islamic.ISLAMIC_EPOCH + math.floor( ((year - 1) * 12 + month - 1/2) * astro.MEAN_SYNODIC_MONTH ) return saudi_new_month_on_or_before(midmonth) + day - 1 end --- Saudi Islamic date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.saudi_islamic_from_fixed(date) local crescent = saudi_new_month_on_or_before(date) local elapsed_months = basic.round_half_to_even( (crescent - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH ) local year = 1 + basic.quotient(elapsed_months, 12) local month = 1 + elapsed_months % 12 local day = 1 + date - crescent return islamic.islamic_date(year, month, day) end -- === Turkish Islamic calendar (Diyanet) === -- The Turkish Directorate of Religious Affairs (Diyanet İşleri Başkanlığı) -- declares the start of each Islamic month based on crescent visibility as -- seen from Turkey. Modelled here using the Shaukat criterion at Ankara. --- Fixed date equivalent to Turkish Islamic date `i_date`. -- @tparam table i_date Islamic date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_turkish_islamic(i_date) local month = basic.standard_month(i_date) local day = basic.standard_day(i_date) local year = basic.standard_year(i_date) local midmonth = islamic.ISLAMIC_EPOCH + math.floor( ((year - 1) * 12 + month - 1/2) * astro.MEAN_SYNODIC_MONTH ) return phasis_on_or_before(midmonth, ANKARA) + day - 1 end --- Turkish Islamic date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.turkish_islamic_from_fixed(date) local crescent = phasis_on_or_before(date, ANKARA) local elapsed_months = basic.round_half_to_even( (crescent - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH ) local year = 1 + basic.quotient(elapsed_months, 12) local month = 1 + elapsed_months % 12 local day = 1 + date - crescent return islamic.islamic_date(year, month, day) end -- === Astronomical Umm al-Qura calendar === -- A month starts on the day after the astronomical conjunction (new moon) occurs -- before sunset at Mecca. This matches the official Saudi definition for -- the Umm al-Qura calendar more closely than the precomputed table variant. -- True if the astronomical new moon (conjunction) occurred before sunset in -- Mecca on the evening preceding fixed date. -- "Before sunset" is evaluated in Mecca standard time: the conjunction must -- fall on Mecca civil day (date-1) and before Mecca sunset on that day. local function umalqura_astronomical_criterion(date) local set_std = astro.sunset(date - 1, astro.MECCA) -- Mecca standard time local set_ut = astro.universal_from_standard(set_std, astro.MECCA) local conj_ut = astro.new_moon_before(set_ut + astro.hr(1)) local conj_std = astro.standard_from_universal(conj_ut, astro.MECCA) -- Conjunction must be on Mecca civil day (date-1), before Mecca sunset return math.floor(conj_std) == date - 1 and conj_std < set_std end -- Closest fixed date on or before date when an Umm al-Qura month started. local function umalqura_astronomical_new_month_on_or_before(date) local moon = basic.fixed_from_moment( astro.lunar_phase_at_or_before(astro.NEW, date) ) local age = date - moon -- The UmQ month starts 1 or 2 days after the conjunction. Only go back -- to the previous lunation when we are on day 0 or 1 past the conjunction -- and the criterion has not yet fired; for age >= 2 the month start is -- always between moon and date. local tau if age <= 1 and not umalqura_astronomical_criterion(date) then tau = moon - 30 else tau = moon end return basic.next(tau, function(d) return umalqura_astronomical_criterion(d) end) end --- Fixed date equivalent to astronomical Umm al-Qura date `i_date`. -- @tparam table i_date Islamic date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_astronomical_islamic_umalqura(i_date) local month = basic.standard_month(i_date) local day = basic.standard_day(i_date) local year = basic.standard_year(i_date) local midmonth = islamic.ISLAMIC_EPOCH + math.floor( ((year - 1) * 12 + month - 1/2) * astro.MEAN_SYNODIC_MONTH ) return umalqura_astronomical_new_month_on_or_before(midmonth) + day - 1 end --- Astronomical Umm al-Qura date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.astronomical_islamic_umalqura_from_fixed(date) local crescent = umalqura_astronomical_new_month_on_or_before(date) local elapsed_months = basic.round_half_to_even( (crescent - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH ) local year = 1 + basic.quotient(elapsed_months, 12) local month = 1 + elapsed_months % 12 local day = 1 + date - crescent return islamic.islamic_date(year, month, day) end -- === Astronomical Easter === --- Date of (proposed) astronomical Easter in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.astronomical_easter(g_year) local equinox = astro.season_in_gregorian(astro.SPRING, g_year) local paschal_moon = math.floor( astro.apparent_from_universal( astro.lunar_phase_at_or_after(astro.FULL, equinox), JERUSALEM ) ) return gregorian.kday_after(basic.SUNDAY, paschal_moon) end -- === Observational Hebrew calendar === -- Fixed date of Observational Nisan 1 occurring in Gregorian year g_year. local function observational_hebrew_first_of_nisan(g_year) local equinox = astro.season_in_gregorian(astro.SPRING, g_year) local set = astro.universal_from_standard( astro.sunset(math.floor(equinox), HEBREW_LOCATION), HEBREW_LOCATION ) return phasis_on_or_after( math.floor(equinox) - (equinox < set and 14 or 13), HEBREW_LOCATION ) end --- Fixed date equivalent to Observational Hebrew date `h_date`. -- @tparam table h_date Hebrew date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_observational_hebrew(h_date) local month = basic.standard_month(h_date) local day = basic.standard_day(h_date) local year = basic.standard_year(h_date) local year1 = month >= hebrew.TISHRI and year - 1 or year local start = hebrew.fixed_from_hebrew( hebrew.hebrew_date(year1, hebrew.NISAN, 1) ) local g_year = gregorian.gregorian_year_from_fixed(start + 60) local new_year = observational_hebrew_first_of_nisan(g_year) local midmonth = new_year + basic.round_half_to_even(29.5 * (month - 1)) + 15 return phasis_on_or_before(midmonth, HEBREW_LOCATION) + day - 1 end --- Observational Hebrew date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.observational_hebrew_from_fixed(date) local crescent = phasis_on_or_before(date, HEBREW_LOCATION) local g_year = gregorian.gregorian_year_from_fixed(date) local ny = observational_hebrew_first_of_nisan(g_year) local new_year = date < ny and observational_hebrew_first_of_nisan(g_year - 1) or ny local month = 1 + basic.round_half_to_even( (crescent - new_year) / 29.5 ) local year = basic.standard_year(hebrew.hebrew_from_fixed(new_year)) + (month >= hebrew.TISHRI and 1 or 0) local day = date - crescent + 1 return hebrew.hebrew_date(year, month, day) end -- Fixed date of Classical Passover Eve (Nisan 14) in Gregorian year g_year. local function classical_passover_eve(g_year) -- luacheck: ignore return observational_hebrew_first_of_nisan(g_year) + 13 end -- === Month length and early-month helpers === -- Length of lunar month based on observability at location, which includes date. local function month_length(date, location) local moon = phasis_on_or_after(date + 1, location) local prev = phasis_on_or_before(date, location) return moon - prev end -- True if date at location is in a month that was forced to start early. local function early_month(date, location) local start = phasis_on_or_before(date, location) local prev = start - 15 return (date - start) >= 30 or month_length(prev, location) > 30 or (month_length(prev, location) == 30 and early_month(prev, location)) end -- Fixed date equivalent to Observational Islamic i_date; months never longer than 30 days. local function alt_fixed_from_observational_islamic(i_date) -- luacheck: ignore local month = basic.standard_month(i_date) local day = basic.standard_day(i_date) local year = basic.standard_year(i_date) local midmonth = islamic.ISLAMIC_EPOCH + math.floor( ((year - 1) * 12 + month - 1/2) * astro.MEAN_SYNODIC_MONTH ) local moon = phasis_on_or_before(midmonth, ISLAMIC_LOCATION) local result = moon + day - 1 return early_month(midmonth, ISLAMIC_LOCATION) and result - 1 or result end -- Observational Islamic date for fixed date; months never longer than 30 days. local function alt_observational_islamic_from_fixed(date) -- luacheck: ignore local early = early_month(date, ISLAMIC_LOCATION) local long = early and month_length(date, ISLAMIC_LOCATION) > 29 local date_prime = long and date + 1 or date local moon = phasis_on_or_before(date_prime, ISLAMIC_LOCATION) local elapsed_months = basic.round_half_to_even( (moon - islamic.ISLAMIC_EPOCH) / astro.MEAN_SYNODIC_MONTH ) local year = 1 + basic.quotient(elapsed_months, 12) local month = 1 + elapsed_months % 12 local day = date_prime - moon + (early and not long and 2 or 1) return islamic.islamic_date(year, month, day) end -- Observational Hebrew date for fixed date; months never longer than 30 days. local function alt_observational_hebrew_from_fixed(date) -- luacheck: ignore local early = early_month(date, HEBREW_LOCATION) local long = early and month_length(date, HEBREW_LOCATION) > 29 local date_prime = long and date + 1 or date local moon = phasis_on_or_before(date_prime, HEBREW_LOCATION) local g_year = gregorian.gregorian_year_from_fixed(date_prime) local ny = observational_hebrew_first_of_nisan(g_year) local new_year = date_prime < ny and observational_hebrew_first_of_nisan(g_year - 1) or ny local month = 1 + basic.round_half_to_even((moon - new_year) / 29.5) local year = basic.standard_year(hebrew.hebrew_from_fixed(new_year)) + (month >= hebrew.TISHRI and 1 or 0) local day = date_prime - moon + (early and not long and 2 or 1) return hebrew.hebrew_date(year, month, day) end -- Fixed date equivalent to Observational Hebrew h_date; months never longer than 30 days. local function alt_fixed_from_observational_hebrew(h_date) -- luacheck: ignore local month = basic.standard_month(h_date) local day = basic.standard_day(h_date) local year = basic.standard_year(h_date) local year1 = month >= hebrew.TISHRI and year - 1 or year local start = hebrew.fixed_from_hebrew( hebrew.hebrew_date(year1, hebrew.NISAN, 1) ) local g_year = gregorian.gregorian_year_from_fixed(start + 60) local new_year = observational_hebrew_first_of_nisan(g_year) local midmonth = new_year + basic.round_half_to_even(29.5 * (month - 1)) + 15 local moon = phasis_on_or_before(midmonth, HEBREW_LOCATION) local result = moon + day - 1 return early_month(midmonth, HEBREW_LOCATION) and result - 1 or result end -- === Samaritan calendar === -- Fixed date of start of the Samaritan Entry Era (March 15, 1639 BCE Julian). local SAMARITAN_EPOCH = julian.fixed_from_julian( julian.julian_date(julian.bce(1639), julian.MARCH, 15) ) -- Universal time of true noon on date at Samaritan location. local function samaritan_noon(date) return astro.midday(date, SAMARITAN_LOCATION) end -- Fixed date of first new moon after UT moment tee (modern calculation). local function samaritan_new_moon_after(tee) return math.ceil( astro.apparent_from_universal( astro.new_moon_at_or_after(tee), SAMARITAN_LOCATION ) - astro.hr(12) ) end -- Fixed date of last new moon before UT moment tee (modern calculation). local function samaritan_new_moon_at_or_before(tee) return math.ceil( astro.apparent_from_universal( astro.new_moon_before(tee), SAMARITAN_LOCATION ) - astro.hr(12) ) end -- Fixed date of Samaritan New Year on or before fixed date. local function samaritan_new_year_on_or_before(date) local g_year = gregorian.gregorian_year_from_fixed(date) local dates = {} for _, d in ipairs(julian.julian_in_gregorian(julian.MARCH, 11, g_year - 1)) do dates[#dates + 1] = d end for _, d in ipairs(julian.julian_in_gregorian(julian.MARCH, 11, g_year)) do dates[#dates + 1] = d end dates[#dates + 1] = date + 1 local n = basic.final( 0, function(i) return samaritan_new_moon_after(samaritan_noon(dates[i + 1])) <= date end ) return samaritan_new_moon_after(samaritan_noon(dates[n + 1])) end --- Samaritan date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.samaritan_from_fixed(date) local moon = samaritan_new_moon_at_or_before(samaritan_noon(date)) local new_year = samaritan_new_year_on_or_before(moon) local month = 1 + basic.round_half_to_even((moon - new_year) / 29.5) local year = basic.round_half_to_even( (new_year - SAMARITAN_EPOCH) / 365.25 ) + math.ceil((month - 5) / 8) local day = date - moon + 1 return hebrew.hebrew_date(year, month, day) end --- Fixed date of Samaritan date `s_date`. -- @tparam table s_date Samaritan date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_samaritan(s_date) local month = basic.standard_month(s_date) local day = basic.standard_day(s_date) local year = basic.standard_year(s_date) local ny = samaritan_new_year_on_or_before( math.floor( SAMARITAN_EPOCH + 50 + 365.25 * (year - math.ceil((month - 5) / 8)) ) ) local nm = samaritan_new_moon_at_or_before(ny + 29.5 * (month - 1) + 15) return nm + day - 1 end -- === Exports === --- True if Babylonian year `b_year` is a leap year. -- @function babylonian_leap_year -- @tparam number b_year Babylonian year. -- @treturn boolean M.babylonian_leap_year = babylonian_leap_year --- Closest fixed date on or before `date` when crescent moon first became visible at `location`. -- @function phasis_on_or_before -- @tparam number date Fixed date. -- @tparam table location Location table. -- @treturn number Fixed date. M.phasis_on_or_before = phasis_on_or_before --- Closest fixed date on or after `date` when crescent moon first became visible at `location`. -- @function phasis_on_or_after -- @tparam number date Fixed date. -- @tparam table location Location table. -- @treturn number Fixed date. M.phasis_on_or_after = phasis_on_or_after --- True if crescent moon is likely visible on the eve of `date` at `location` (Shaukat criterion). -- @function shaukat_criterion -- @tparam number date Fixed date. -- @tparam table location Location table. -- @treturn boolean M.shaukat_criterion = shaukat_criterion --- True if crescent moon is possibly visible on the eve of `date` at `location` (Yallop criterion). -- @function yallop_criterion -- @tparam number date Fixed date. -- @tparam table location Location table. -- @treturn boolean M.yallop_criterion = yallop_criterion return M