--- Islamic 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-islamic -- @release 0.1 2026-07-19 local M = {} local basic = require("calendrica-basic") local julian = require("calendrica-julian") local gregorian = require("calendrica-gregorian") -- === Epochs === -- Friday epoch: CE 622 July 16 (Julian) — used by arithmetic (civil) calendar. local ISLAMIC_EPOCH = julian.fixed_from_julian( julian.julian_date(julian.ce(622), julian.JULY, 16) ) -- Thursday epoch: CE 622 July 15 (Julian) — used by TBLA variant. local ISLAMIC_EPOCH_TBLA = julian.fixed_from_julian( julian.julian_date(julian.ce(622), julian.JULY, 15) ) -- === Date constructor === --- Construct an Islamic date {year, month, day}. -- @tparam number year Islamic year. -- @tparam number month Islamic month. -- @tparam number day Islamic day. -- @treturn table {year, month, day} function M.islamic_date(year, month, day) return {year, month, day} end local islamic_date = M.islamic_date -- === Leap year === -- True if i_year is an Islamic leap year. local function islamic_leap_year(i_year) return (14 + 11 * i_year) % 30 < 11 end -- === Conversion === --- Fixed date equivalent to Islamic date `i_date`. -- @tparam table i_date Islamic date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_islamic(i_date) local month = basic.standard_month(i_date) local day = basic.standard_day(i_date) local year = basic.standard_year(i_date) return (ISLAMIC_EPOCH - 1) -- Days before start of calendar. + (year - 1) * 354 -- Ordinary days since epoch. + basic.quotient(3 + 11 * year, 30) -- Leap days since epoch. + 29 * (month - 1) -- Days in prior months this year. + basic.quotient(month, 2) + day -- Days so far this month. end local fixed_from_islamic = M.fixed_from_islamic --- Islamic date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.islamic_from_fixed(date) local year = basic.quotient( 30 * (date - ISLAMIC_EPOCH) + 10646, 10631 ) local prior_days = date - fixed_from_islamic(islamic_date(year, 1, 1)) local month = basic.quotient(11 * prior_days + 330, 325) local day = 1 + date - fixed_from_islamic(islamic_date(year, month, 1)) return islamic_date(year, month, day) end local islamic_from_fixed = M.islamic_from_fixed -- === TBLA variant (Thursday epoch) === --- Fixed date equivalent to Islamic TBLA date `i_date`. -- Identical to the civil calendar but uses the Thursday epoch (one day earlier). -- @tparam table i_date Islamic date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_islamic_tbla(i_date) local month = basic.standard_month(i_date) local day = basic.standard_day(i_date) local year = basic.standard_year(i_date) return (ISLAMIC_EPOCH_TBLA - 1) + (year - 1) * 354 + basic.quotient(3 + 11 * year, 30) + 29 * (month - 1) + basic.quotient(month, 2) + day end local fixed_from_islamic_tbla = M.fixed_from_islamic_tbla --- Islamic TBLA date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.islamic_tbla_from_fixed(date) local year = basic.quotient( 30 * (date - ISLAMIC_EPOCH_TBLA) + 10646, 10631 ) local prior_days = date - fixed_from_islamic_tbla(islamic_date(year, 1, 1)) local month = basic.quotient(11 * prior_days + 330, 325) local day = 1 + date - fixed_from_islamic_tbla(islamic_date(year, month, 1)) return islamic_date(year, month, day) end -- === Umm al-Qura variant === -- Precomputed month lengths for AH 1300-1600 (bit-packed: 1=30 days, 0=29 days). -- Each entry is a 12-bit value, one bit per month (MSB = month 1). -- Source: ICU UMALQURA_MONTHLENGTH table. local UMALQURA_YEAR_START = 1300 local UMALQURA_YEAR_END = 1600 local UMALQURA_DATA = { 0x0AAA,0x0D54,0x0EC9,0x06D4,0x06EA,0x036C,0x0AAD,0x0555,0x06A9,0x0792, 0x0BA9,0x05D4,0x0ADA,0x055C,0x0D2D,0x0695,0x074A,0x0B54,0x0B6A,0x05AD, 0x04AE,0x0A4F,0x0517,0x068B,0x06A5,0x0AD5,0x02D6,0x095B,0x049D,0x0A4D, 0x0D26,0x0D95,0x05AC,0x09B6,0x02BA,0x0A5B,0x052B,0x0A95,0x06CA,0x0AE9, 0x02F4,0x0976,0x02B6,0x0956,0x0ACA,0x0BA4,0x0BD2,0x05D9,0x02DC,0x096D, 0x054D,0x0AA5,0x0B52,0x0BA5,0x05B4,0x09B6,0x0557,0x0297,0x054B,0x06A3, 0x0752,0x0B65,0x056A,0x0AAB,0x052B,0x0C95,0x0D4A,0x0DA5,0x05CA,0x0AD6, 0x0957,0x04AB,0x094B,0x0AA5,0x0B52,0x0B6A,0x0575,0x0276,0x08B7,0x045B, 0x0555,0x05A9,0x05B4,0x09DA,0x04DD,0x026E,0x0936,0x0AAA,0x0D54,0x0DB2, 0x05D5,0x02DA,0x095B,0x04AB,0x0A55,0x0B49,0x0B64,0x0B71,0x05B4,0x0AB5, 0x0A55,0x0D25,0x0E92,0x0EC9,0x06D4,0x0AE9,0x096B,0x04AB,0x0A93,0x0D49, 0x0DA4,0x0DB2,0x0AB9,0x04BA,0x0A5B,0x052B,0x0A95,0x0B2A,0x0B55,0x055C, 0x04BD,0x023D,0x091D,0x0A95,0x0B4A,0x0B5A,0x056D,0x02B6,0x093B,0x049B, 0x0655,0x06A9,0x0754,0x0B6A,0x056C,0x0AAD,0x0555,0x0B29,0x0B92,0x0BA9, 0x05D4,0x0ADA,0x055A,0x0AAB,0x0595,0x0749,0x0764,0x0BAA,0x05B5,0x02B6, 0x0A56,0x0E4D,0x0B25,0x0B52,0x0B6A,0x05AD,0x02AE,0x092F,0x0497,0x064B, 0x06A5,0x06AC,0x0AD6,0x055D,0x049D,0x0A4D,0x0D16,0x0D95,0x05AA,0x05B5, 0x02DA,0x095B,0x04AD,0x0595,0x06CA,0x06E4,0x0AEA,0x04F5,0x02B6,0x0956, 0x0AAA,0x0B54,0x0BD2,0x05D9,0x02EA,0x096D,0x04AD,0x0A95,0x0B4A,0x0BA5, 0x05B2,0x09B5,0x04D6,0x0A97,0x0547,0x0693,0x0749,0x0B55,0x056A,0x0A6B, 0x052B,0x0A8B,0x0D46,0x0DA3,0x05CA,0x0AD6,0x04DB,0x026B,0x094B,0x0AA5, 0x0B52,0x0B69,0x0575,0x0176,0x08B7,0x025B,0x052B,0x0565,0x05B4,0x09DA, 0x04ED,0x016D,0x08B6,0x0AA6,0x0D52,0x0DA9,0x05D4,0x0ADA,0x095B,0x04AB, 0x0653,0x0729,0x0762,0x0BA9,0x05B2,0x0AB5,0x0555,0x0B25,0x0D92,0x0EC9, 0x06D2,0x0AE9,0x056B,0x04AB,0x0A55,0x0D29,0x0D54,0x0DAA,0x09B5,0x04BA, 0x0A3B,0x049B,0x0A4D,0x0AAA,0x0AD5,0x02DA,0x095D,0x045E,0x0A2E,0x0C9A, 0x0D55,0x06B2,0x06B9,0x04BA,0x0A5D,0x052D,0x0A95,0x0B52,0x0BA8,0x0BB4, 0x05B9,0x02DA,0x095A,0x0B4A,0x0DA4,0x0ED1,0x06E8,0x0B6A,0x056D,0x0535, 0x0695,0x0D4A,0x0DA8,0x0DD4,0x06DA,0x055B,0x029D,0x062B,0x0B15,0x0B4A, 0x0B95,0x05AA,0x0AAE,0x092E,0x0C8F,0x0527,0x0695,0x06AA,0x0AD6,0x055D,0x029D, } local function umalqura_month_length(year, month) if year < UMALQURA_YEAR_START or year > UMALQURA_YEAR_END then return 29 + (month % 2 == 1 and 1 or 0) + (month == 12 and islamic_leap_year(year) and 1 or 0) end local bits = UMALQURA_DATA[year - UMALQURA_YEAR_START + 1] local mask = 0x800 >> (month - 1) return (bits & mask ~= 0) and 30 or 29 end --- Fixed date equivalent to Umm al-Qura Islamic date `i_date`. -- @tparam table i_date Islamic date {year, month, day}. -- @treturn number Fixed date. function M.fixed_from_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) -- start from year epoch using civil calendar for years outside table range local d = fixed_from_islamic(islamic_date(year, 1, 1)) -- adjust for umalqura month lengths within the year for m = 1, month - 1 do d = d + umalqura_month_length(year, m) end return d + day - 1 end local fixed_from_islamic_umalqura = M.fixed_from_islamic_umalqura --- Umm al-Qura Islamic date {year, month, day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, day} function M.islamic_umalqura_from_fixed(date) -- estimate year from civil calendar local year = basic.standard_year(islamic_from_fixed(date)) -- adjust year if needed while fixed_from_islamic_umalqura(islamic_date(year + 1, 1, 1)) <= date do year = year + 1 end while fixed_from_islamic_umalqura(islamic_date(year, 1, 1)) > date do year = year - 1 end local month = 1 while month < 12 and fixed_from_islamic_umalqura(islamic_date(year, month + 1, 1)) <= date do month = month + 1 end local day = 1 + date - fixed_from_islamic_umalqura(islamic_date(year, month, 1)) return islamic_date(year, month, day) end -- === Islamic date in Gregorian year === -- Fixed dates of Islamic i_month/i_day that fall in Gregorian year g_year. local function islamic_in_gregorian(i_month, i_day, g_year) local jan1 = gregorian.gregorian_new_year(g_year) local y = basic.standard_year(islamic_from_fixed(jan1)) local date0 = fixed_from_islamic(islamic_date(y, i_month, i_day)) local date1 = fixed_from_islamic(islamic_date(y + 1, i_month, i_day)) local date2 = fixed_from_islamic(islamic_date(y + 2, i_month, i_day)) return basic.list_range( {date0, date1, date2}, gregorian.gregorian_year_range(g_year) ) end -- === Islamic holidays === --- Fixed dates of Ramadan start (month 9, day 1) in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.ramadan(g_year) return islamic_in_gregorian(9, 1, g_year) end --- Fixed dates of Eid al-Fitr (month 10, day 1) in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.eid_al_fitr(g_year) return islamic_in_gregorian(10, 1, g_year) end --- Fixed dates of Eid al-Adha (month 12, day 10) in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.eid_al_adha(g_year) return islamic_in_gregorian(12, 10, g_year) end --- Fixed dates of Islamic New Year (Muharram 1) occurring in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.islamic_new_year(g_year) return islamic_in_gregorian(1, 1, g_year) end --- Fixed dates of Mawlid an-Nabi occurring in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn table List of fixed dates. function M.mawlid(g_year) return islamic_in_gregorian(3, 12, g_year) end -- === Exports === --- Fixed dates of Islamic month/day `i_month`/`i_day` falling in Gregorian year `g_year`. -- @function islamic_in_gregorian -- @tparam number i_month Islamic month. -- @tparam number i_day Islamic day. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. M.islamic_in_gregorian = islamic_in_gregorian --- True if Islamic year `i_year` is a leap year (355 days). -- @function islamic_leap_year -- @tparam number i_year Islamic year. -- @treturn boolean M.islamic_leap_year = islamic_leap_year --- Fixed date of start of the Islamic calendar (July 16, 622 CE Julian). M.ISLAMIC_EPOCH = ISLAMIC_EPOCH --- Fixed date of start of the Islamic TBLA calendar (July 15, 622 CE Julian). M.ISLAMIC_EPOCH_TBLA = ISLAMIC_EPOCH_TBLA return M