--- Hebrew calendar conversions and holidays. -- 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-hebrew -- @release 0.1 2026-07-19 local M = {} local basic = require("calendrica-basic") local julian = require("calendrica-julian") local gregorian = require("calendrica-gregorian") local coptic = require("calendrica-coptic-ethiopic") -- === Month constants === local NISAN = 1 -- Iyyar month number local IYYAR = 2 -- Sivan month number local SIVAN = 3 -- Tammuz month number local TAMMUZ = 4 -- Av month number local AV = 5 -- Elul month number local ELUL = 6 local TISHRI = 7 -- Marheshvan month number local MARHESHVAN = 8 -- Kislev month number local KISLEV = 9 -- Tevet month number local TEVET = 10 -- Shevat month number local SHEVAT = 11 -- Adar month number local ADAR = 12 -- Adar II month number local ADARII = 13 -- === Epoch === -- Fixed date of start of the Hebrew calendar (Tishri 1, 1 AM). local HEBREW_EPOCH = julian.fixed_from_julian( julian.julian_date(julian.bce(3761), julian.OCTOBER, 7) ) -- === Date constructor and accessors === --- Construct a Hebrew date {year, month, day}. -- @tparam number year Hebrew year. -- @tparam number month Hebrew month. -- @tparam number day Hebrew day. -- @treturn table {year, month, day} function M.hebrew_date(year, month, day) return {year, month, day} end local hebrew_date = M.hebrew_date -- === Year predicates === -- True if h_year is a Hebrew leap year. local function hebrew_leap_year(h_year) return ((1 + 7 * h_year) % 19) < 7 end -- True if h_year is a sabbatical year. local function hebrew_sabbatical_year(h_year) return h_year % 7 == 0 end -- === Month helpers === -- Last month of Hebrew year h_year. local function last_month_of_hebrew_year(h_year) if hebrew_leap_year(h_year) then return ADARII else return ADAR end end -- Forward declarations needed because long_marheshvan/short_kislev -- reference days_in_hebrew_year which is defined later. local days_in_hebrew_year -- True if Marheshvan is long in Hebrew year h_year. local function long_marheshvan(h_year) local d = days_in_hebrew_year(h_year) return d == 355 or d == 385 end -- True if Kislev is short in Hebrew year h_year. local function short_kislev(h_year) local d = days_in_hebrew_year(h_year) return d == 353 or d == 383 end -- Last day of month h_month in Hebrew year h_year. local function last_day_of_hebrew_month(h_year, h_month) local short_months = { [IYYAR] = true, [TAMMUZ] = true, [ELUL] = true, [TEVET] = true, [ADARII] = true, } if short_months[h_month] or (h_month == ADAR and not hebrew_leap_year(h_year)) or (h_month == MARHESHVAN and not long_marheshvan(h_year)) or (h_month == KISLEV and short_kislev(h_year)) then return 29 else return 30 end end -- === Molad === -- Moment of mean conjunction (molad) of h_month in Hebrew h_year. local function molad(h_year, h_month) -- Treat Nisan as start of year. local y if h_month < TISHRI then y = h_year + 1 else y = h_year end local months_elapsed = (h_month - TISHRI) + basic.quotient(235 * y - 234, 19) return HEBREW_EPOCH - 876/25920 + months_elapsed * (29 + 12/24 + 793/25920) end -- === Calendar arithmetic === -- Days elapsed from noon prior to epoch to molad of Tishri of h_year. local function hebrew_calendar_elapsed_days(h_year) local months_elapsed = basic.quotient(235 * h_year - 234, 19) local parts_elapsed = 12084 + 13753 * months_elapsed local days = 29 * months_elapsed + basic.quotient(parts_elapsed, 25920) if (3 * (days + 1)) % 7 < 3 then -- Sunday, Wednesday, or Friday return days + 1 -- Delay one day. else return days end end -- Delays to start of Hebrew year h_year to keep year length in range. local function hebrew_year_length_correction(h_year) local ny0 = hebrew_calendar_elapsed_days(h_year - 1) local ny1 = hebrew_calendar_elapsed_days(h_year) local ny2 = hebrew_calendar_elapsed_days(h_year + 1) if ny2 - ny1 == 356 then -- Next year would be too long. return 2 elseif ny1 - ny0 == 382 then -- Previous year too short. return 1 else return 0 end end -- Fixed date of Hebrew new year h_year. local function hebrew_new_year(h_year) return HEBREW_EPOCH + hebrew_calendar_elapsed_days(h_year) + hebrew_year_length_correction(h_year) end -- Number of days in Hebrew year h_year. days_in_hebrew_year = function(h_year) return hebrew_new_year(h_year + 1) - hebrew_new_year(h_year) end -- === Conversion === --- Fixed date of Hebrew date `h_date`. -- @tparam table h_date Hebrew date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_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 days = hebrew_new_year(year) + day - 1 if month < TISHRI then -- Add days in prior months this year before and after Nisan. days = days + basic.sum( function(m) return last_day_of_hebrew_month(year, m) end, TISHRI, function(m) return m <= last_month_of_hebrew_year(year) end ) days = days + basic.sum( function(m) return last_day_of_hebrew_month(year, m) end, NISAN, function(m) return m < month end ) else -- Add days in prior months this year. days = days + basic.sum( function(m) return last_day_of_hebrew_month(year, m) end, TISHRI, function(m) return m < month end ) end return days end local fixed_from_hebrew = M.fixed_from_hebrew --- Hebrew date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.hebrew_from_fixed(date) -- Approximate year (average Hebrew year length = 35975351/98496 ≈ 365.25). local approx = 1 + basic.quotient( (date - HEBREW_EPOCH) * 98496, 35975351 ) -- Search forward for the actual year. local year = basic.final( approx - 1, function(y) return hebrew_new_year(y) <= date end ) -- Starting month: Tishri or Nisan depending on position in year. local start if date < fixed_from_hebrew(hebrew_date(year, NISAN, 1)) then start = TISHRI else start = NISAN end -- Search forward from start month. local month = basic.next( start, function(m) return date <= fixed_from_hebrew( hebrew_date(year, m, last_day_of_hebrew_month(year, m)) ) end ) -- Calculate day by subtraction. local day = 1 + date - fixed_from_hebrew(hebrew_date(year, month, 1)) return hebrew_date(year, month, day) end local hebrew_from_fixed = M.hebrew_from_fixed -- Fixed date of the molad that occurs moon days into the week. local function fixed_from_molad(moon) -- luacheck: ignore local r = (74377 * moon - 2879/2160) % 7 return basic.fixed_from_moment( molad(1, TISHRI) + r * 765433 ) end -- === Jewish holidays === -- Forward declaration for hebrew_in_gregorian (used by hanukkah). local hebrew_in_gregorian --- Fixed date of Yom Kippur occurring in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.yom_kippur(g_year) local h_year = 1 + g_year - gregorian.gregorian_year_from_fixed(HEBREW_EPOCH) return fixed_from_hebrew(hebrew_date(h_year, TISHRI, 10)) end --- Fixed dates of Rosh Hashanah (Hebrew New Year, Tishri 1) in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.rosh_hashanah(g_year) return hebrew_in_gregorian(TISHRI, 1, g_year) end --- Fixed dates of Sukkot (Tishri 15) in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.sukkot(g_year) return hebrew_in_gregorian(TISHRI, 15, g_year) end --- Fixed dates of Shavuot (Sivan 6) in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.shavuot(g_year) return hebrew_in_gregorian(SIVAN, 6, g_year) end --- Fixed date of Passover occurring in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.passover(g_year) local h_year = g_year - gregorian.gregorian_year_from_fixed(HEBREW_EPOCH) return fixed_from_hebrew(hebrew_date(h_year, NISAN, 15)) end local passover = M.passover -- Elapsed weeks and days in the omer at date. Returns BOGUS if not in omer. local function omer(date) local c = date - passover(gregorian.gregorian_year_from_fixed(date)) if c >= 1 and c <= 49 then return {basic.quotient(c, 7), c % 7} else return basic.BOGUS end end --- Fixed date of Purim occurring in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.purim(g_year) local h_year = g_year - gregorian.gregorian_year_from_fixed(HEBREW_EPOCH) local last_month = last_month_of_hebrew_year(h_year) return fixed_from_hebrew(hebrew_date(h_year, last_month, 14)) end local purim = M.purim --- Fixed date of Ta'anit Esther occurring in Gregorian year `g_year`. -- If Purim is on Sunday, returns the prior Thursday; otherwise the day before. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.ta_anit_esther(g_year) local purim_date = purim(g_year) if basic.day_of_week_from_fixed(purim_date) == basic.SUNDAY then return purim_date - 3 -- Prior Thursday. else return purim_date - 1 end end --- Fixed date of Tishah be-Av occurring in Gregorian year `g_year`. -- If the ninth of Av is Saturday, returns the next day. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.tishah_be_av(g_year) local h_year = g_year - gregorian.gregorian_year_from_fixed(HEBREW_EPOCH) local av9 = fixed_from_hebrew(hebrew_date(h_year, AV, 9)) if basic.day_of_week_from_fixed(av9) == basic.SATURDAY then return av9 + 1 else return av9 end end --- Fixed dates of Hanukkah (first day) occurring in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn table List of fixed dates. function M.hanukkah(g_year) return hebrew_in_gregorian(KISLEV, 25, g_year) end --- Fixed date of Yom ha-Zikkaron occurring in Gregorian year `g_year`. -- Observance is moved if Iyyar 4 falls on Thursday, Friday, or Sunday. -- @tparam number g_year Gregorian year. -- @treturn number Fixed date. function M.yom_ha_zikkaron(g_year) local h_year = g_year - gregorian.gregorian_year_from_fixed(HEBREW_EPOCH) local iyyar4 = fixed_from_hebrew(hebrew_date(h_year, IYYAR, 4)) local dow = basic.day_of_week_from_fixed(iyyar4) if dow == basic.THURSDAY or dow == basic.FRIDAY then -- Move to Wednesday. return gregorian.kday_before(basic.WEDNESDAY, iyyar4) elseif dow == basic.SUNDAY then return iyyar4 + 1 else return iyyar4 end end -- === Hebrew date in Gregorian year === -- Fixed dates of Hebrew h_month/h_day that fall in Gregorian year g_year. hebrew_in_gregorian = function(h_month, h_day, g_year) local jan1 = gregorian.gregorian_new_year(g_year) local y = basic.standard_year(hebrew_from_fixed(jan1)) local date0 = fixed_from_hebrew(hebrew_date(y, h_month, h_day)) local date1 = fixed_from_hebrew(hebrew_date(y + 1, h_month, h_day)) local date2 = fixed_from_hebrew(hebrew_date(y + 2, h_month, h_day)) return basic.list_range( {date0, date1, date2}, gregorian.gregorian_year_range(g_year) ) end -- === Birkath ha-Hama === -- Fixed dates of Birkath ha-Hama in Gregorian year g_year, if it occurs. local function birkath_ha_hama(g_year) local dates = coptic.coptic_in_gregorian(7, 30, g_year) if #dates > 0 and basic.standard_year(coptic.coptic_from_fixed(dates[1])) % 28 == 17 then return dates else return {} end end -- Fixed dates of Sh'ela in Gregorian year g_year. local function sh_ela(g_year) return coptic.coptic_in_gregorian(3, 26, g_year) end -- === Hebrew birthday and yahrzeit === -- Fixed date of anniversary of Hebrew birthdate in Hebrew year h_year. local function hebrew_birthday(birthdate, h_year) local birth_day = basic.standard_day(birthdate) local birth_month = basic.standard_month(birthdate) local birth_year = basic.standard_year(birthdate) -- If born in Adar of a normal year or Adar II of a leap year, -- use the same day in the last month of the target year. if birth_month == last_month_of_hebrew_year(birth_year) then return fixed_from_hebrew( hebrew_date(h_year, last_month_of_hebrew_year(h_year), birth_day) ) else return fixed_from_hebrew( hebrew_date(h_year, birth_month, 1) ) + birth_day - 1 end end -- Fixed dates of birthdate anniversary in Gregorian year g_year. local function hebrew_birthday_in_gregorian(birthdate, g_year) local jan1 = gregorian.gregorian_new_year(g_year) local y = basic.standard_year(hebrew_from_fixed(jan1)) local date0 = hebrew_birthday(birthdate, y) local date1 = hebrew_birthday(birthdate, y + 1) local date2 = hebrew_birthday(birthdate, y + 2) return basic.list_range( {date0, date1, date2}, gregorian.gregorian_year_range(g_year) ) end -- Fixed date of yahrzeit of death_date in Hebrew year h_year. local function yahrzeit(death_date, h_year) local death_day = basic.standard_day(death_date) local death_month = basic.standard_month(death_date) local death_year = basic.standard_year(death_date) if death_month == MARHESHVAN and death_day == 30 and not long_marheshvan(death_year + 1) then -- Marheshvan 30: use the day before Kislev 1. return fixed_from_hebrew(hebrew_date(h_year, KISLEV, 1)) - 1 elseif death_month == KISLEV and death_day == 30 and short_kislev(death_year + 1) then -- Kislev 30: use the day before Tevet 1. return fixed_from_hebrew(hebrew_date(h_year, TEVET, 1)) - 1 elseif death_month == ADARII then -- Adar II: use same day in last month of target year. return fixed_from_hebrew( hebrew_date(h_year, last_month_of_hebrew_year(h_year), death_day) ) elseif death_day == 30 and death_month == ADAR and not hebrew_leap_year(h_year) then -- 30 Adar I in a non-leap year: use last day of Shevat. return fixed_from_hebrew(hebrew_date(h_year, SHEVAT, 30)) else -- Normal case. return fixed_from_hebrew( hebrew_date(h_year, death_month, 1) ) + death_day - 1 end end -- Fixed dates of yahrzeit of death_date in Gregorian year g_year. local function yahrzeit_in_gregorian(death_date, g_year) local jan1 = gregorian.gregorian_new_year(g_year) local y = basic.standard_year(hebrew_from_fixed(jan1)) local date0 = yahrzeit(death_date, y) local date1 = yahrzeit(death_date, y + 1) local date2 = yahrzeit(death_date, y + 2) return basic.list_range( {date0, date1, date2}, gregorian.gregorian_year_range(g_year) ) end -- === Possible days of week === -- Shift each weekday in list l by cap_delta days. local function shift_days(l, cap_delta) local result = {} for _, d in ipairs(l) do result[#result + 1] = (d + cap_delta) % 7 end return result end -- Possible days of week on which Hebrew h_month/h_day can fall. local function possible_hebrew_days(h_month, h_day) -- luacheck: ignore local h_date0 = hebrew_date(5, NISAN, 1) -- Use a leap year with full pattern. local h_year if h_month > ELUL then h_year = 6 else h_year = 5 end local h_date = hebrew_date(h_year, h_month, h_day) local n = fixed_from_hebrew(h_date) - fixed_from_hebrew(h_date0) local basic_days = {basic.TUESDAY, basic.THURSDAY, basic.SATURDAY} local extra if h_month == MARHESHVAN and h_day == 30 then extra = {} elseif h_month == KISLEV and h_day < 30 then extra = {basic.MONDAY, basic.WEDNESDAY, basic.FRIDAY} elseif h_month == KISLEV and h_day == 30 then extra = {basic.MONDAY} elseif h_month == TEVET or h_month == SHEVAT then extra = {basic.SUNDAY, basic.MONDAY} elseif h_month == ADAR and h_day < 30 then extra = {basic.SUNDAY, basic.MONDAY} else extra = {basic.SUNDAY} end local combined = {} for _, d in ipairs(basic_days) do combined[#combined + 1] = d end for _, d in ipairs(extra) do combined[#combined + 1] = d end return shift_days(combined, n) end -- === Exports === --- Fixed dates of Hebrew month/day `h_month`/`h_day` falling in Gregorian year `g_year`. -- @function hebrew_in_gregorian -- @tparam number h_month Hebrew month. -- @tparam number h_day Hebrew day. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. M.hebrew_in_gregorian = hebrew_in_gregorian --- True if `h_year` is a Hebrew leap year. -- @function hebrew_leap_year -- @tparam number h_year Hebrew year. -- @treturn boolean M.hebrew_leap_year = hebrew_leap_year --- True if `h_year` is a Hebrew sabbatical (shmita) year. -- @function hebrew_sabbatical_year -- @tparam number h_year Hebrew year. -- @treturn boolean M.hebrew_sabbatical_year = hebrew_sabbatical_year --- Day of the Omer for fixed `date` (1..49), or BOGUS outside the Omer period. -- Returns {weeks, days} where weeks is complete weeks and days is remaining days. -- @function omer -- @tparam number date Fixed date. -- @treturn table|number {weeks, days} or BOGUS. M.omer = omer --- Fixed dates of Birkath HaHama in Gregorian year `g_year`. -- Occurs when Nisan 1 falls on Wednesday in a year divisible by 28 (solar cycle). -- @function birkath_ha_hama -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates (empty if not occurring this year). M.birkath_ha_hama = birkath_ha_hama --- Fixed dates of Sh'ela (prayer for rain) in Gregorian year `g_year`. -- @function sh_ela -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. M.sh_ela = sh_ela --- Fixed dates of Hebrew `birthdate` anniversary falling in Gregorian year `g_year`. -- @function hebrew_birthday_in_gregorian -- @tparam table birthdate Hebrew date {year, month, day}. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. M.hebrew_birthday_in_gregorian = hebrew_birthday_in_gregorian --- Fixed dates of yahrzeit for Hebrew `death_date` falling in Gregorian year `g_year`. -- @function yahrzeit_in_gregorian -- @tparam table death_date Hebrew date {year, month, day}. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. M.yahrzeit_in_gregorian = yahrzeit_in_gregorian --- Nisan month number (1). M.NISAN = NISAN --- Iyyar month number (2). M.IYYAR = IYYAR --- Sivan month number (3). M.SIVAN = SIVAN --- Tammuz month number (4). M.TAMMUZ = TAMMUZ --- Av month number (5). M.AV = AV --- Elul month number (6). M.ELUL = ELUL --- Tishri month number (7). M.TISHRI = TISHRI --- Marheshvan month number (8). M.MARHESHVAN = MARHESHVAN --- Kislev month number (9). M.KISLEV = KISLEV --- Tevet month number (10). M.TEVET = TEVET --- Shevat month number (11). M.SHEVAT = SHEVAT --- Adar month number (12). M.ADAR = ADAR --- Adar II month number (13, leap years only). M.ADARII = ADARII return M