--- Tibetan 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-tibetan -- @release 0.1 2026-07-19 local M = {} local basic = require("calendrica-basic") local gregorian = require("calendrica-gregorian") local astro = require("calendrica-astro") -- === Epoch === -- Fixed date of start of the Tibetan calendar (December 7, -127 Gregorian). local TIBETAN_EPOCH = gregorian.fixed_from_gregorian( gregorian.gregorian_date(-127, gregorian.DECEMBER, 7) ) -- === Date constructor and accessors === -- Tibetan date constructor. local function tibetan_date(year, month, leap_month, day, leap_day) return {year, month, leap_month, day, leap_day} end -- Tibetan date accessors. local function tibetan_year(date) return date[1] end local function tibetan_month(date) return date[2] end local function tibetan_leap_month(date) return date[3] end local function tibetan_day(date) return date[4] end local function tibetan_leap_day(date) return date[5] end -- === Equation tables === -- Interpolated tabular sine of solar anomaly alpha (range 0..12). local function tibetan_sun_equation(alpha) if alpha > 6 then return -tibetan_sun_equation(alpha - 6) elseif alpha > 3 then return tibetan_sun_equation(6 - alpha) elseif alpha % 1 == 0 then local table = {astro.mins(0), astro.mins(6), astro.mins(10), astro.mins(11)} return table[alpha + 1] else return (alpha % 1) * tibetan_sun_equation(math.ceil(alpha)) + ((-alpha) % 1) * tibetan_sun_equation(math.floor(alpha)) end end -- Interpolated tabular sine of lunar anomaly alpha (range 0..14). local function tibetan_moon_equation(alpha) if alpha > 14 then return -tibetan_moon_equation(alpha - 14) elseif alpha > 7 then return tibetan_moon_equation(14 - alpha) elseif alpha % 1 == 0 then local table = { astro.mins(0), astro.mins(5), astro.mins(10), astro.mins(15), astro.mins(19), astro.mins(22), astro.mins(24), astro.mins(25), } return table[alpha + 1] else return (alpha % 1) * tibetan_moon_equation(math.ceil(alpha)) + ((-alpha) % 1) * tibetan_moon_equation(math.floor(alpha)) end end -- === Conversion === --- Fixed date corresponding to Tibetan lunar date `t_date`. -- @tparam table t_date Tibetan date {year, month, leap_month, day, leap_day}. -- @treturn number Fixed date. function M.fixed_from_tibetan(t_date) local year = tibetan_year(t_date) local month = tibetan_month(t_date) local leap_month = tibetan_leap_month(t_date) local day = tibetan_day(t_date) local leap_day = tibetan_leap_day(t_date) local months = math.floor( 804/65 * (year - 1) + 67/65 * month + (leap_month and -1 or 0) + 64/65 ) local days = 30 * months + day local mean = days * 11135/11312 - 30 + (leap_day and 0 or -1) + 1071/1616 local solar_anomaly = (days * 13/4824 + 2117/4824) % 1 local lunar_anomaly = (days * 3781/105840 + 2837/15120) % 1 local sun = -tibetan_sun_equation(12 * solar_anomaly) local moon = tibetan_moon_equation(28 * lunar_anomaly) return math.floor(TIBETAN_EPOCH + mean + sun + moon) end local fixed_from_tibetan = M.fixed_from_tibetan --- Tibetan lunar date {year,month,leap_month,day,leap_day} corresponding to fixed `date`. -- @tparam number date Fixed date. -- @treturn table {year, month, leap_month, day, leap_day} function M.tibetan_from_fixed(date) local cap_Y = 365 + 4975/18382 local years = math.ceil((date - TIBETAN_EPOCH) / cap_Y) local year0 = basic.final( years, function(y) return date >= fixed_from_tibetan(tibetan_date(y, 1, false, 1, false)) end ) local month0 = basic.final( 1, function(m) return date >= fixed_from_tibetan( tibetan_date(year0, m, false, 1, false) ) end ) local est = date - fixed_from_tibetan( tibetan_date(year0, month0, false, 1, false) ) local day0 = basic.final( est - 2, function(d) return date >= fixed_from_tibetan( tibetan_date(year0, month0, false, d, false) ) end ) local leap_month = day0 > 30 local day = basic.amod(day0, 30) local month_raw if day > day0 then month_raw = month0 - 1 elseif leap_month then month_raw = month0 + 1 else month_raw = month0 end local month = basic.amod(month_raw, 12) local year if day > day0 and month0 == 1 then year = year0 - 1 elseif leap_month and month0 == 12 then year = year0 + 1 else year = year0 end local leap_day = date == fixed_from_tibetan( tibetan_date(year, month, leap_month, day, true) ) return tibetan_date(year, month, leap_month, day, leap_day) end local tibetan_from_fixed = M.tibetan_from_fixed -- === Leap month and day predicates === -- True if t_month is a leap month in Tibetan year t_year. local function tibetan_leap_month_p(t_year, t_month) return t_month == tibetan_month( tibetan_from_fixed( fixed_from_tibetan( tibetan_date(t_year, t_month, true, 2, false) ) ) ) end -- True if t_day is a leap day in Tibetan month t_month and year t_year. local function tibetan_leap_day_p(t_year, t_month, t_day) return t_day == tibetan_day( tibetan_from_fixed( fixed_from_tibetan( tibetan_date(t_year, t_month, false, t_day, true) ) ) ) or t_day == tibetan_day( tibetan_from_fixed( fixed_from_tibetan( tibetan_date( t_year, t_month, tibetan_leap_month_p(t_year, t_month), t_day, true ) ) ) ) end -- === Tibetan New Year === -- Fixed date of Tibetan New Year (Losar) in Tibetan year t_year. local function losar(t_year) local t_leap = tibetan_leap_month_p(t_year, 1) return fixed_from_tibetan(tibetan_date(t_year, 1, t_leap, 1, false)) end -- List of fixed dates of Tibetan New Year in Gregorian year g_year. --- Fixed dates of Losar (Tibetan New Year) falling in Gregorian year `g_year`. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.tibetan_new_year(g_year) local dec31 = gregorian.gregorian_year_end(g_year) local t_year = tibetan_year(tibetan_from_fixed(dec31)) return basic.list_range( {losar(t_year - 1), losar(t_year)}, gregorian.gregorian_year_range(g_year) ) end --- True if month `t_month` is a leap month in Tibetan year `t_year`. -- @function tibetan_leap_month_p -- @tparam number t_year Tibetan year. -- @tparam number t_month Tibetan month. -- @treturn boolean M.tibetan_leap_month_p = tibetan_leap_month_p --- True if day `t_day` is a leap day in Tibetan month `t_month` of year `t_year`. -- @function tibetan_leap_day_p -- @tparam number t_year Tibetan year. -- @tparam number t_month Tibetan month. -- @tparam number t_day Tibetan day. -- @treturn boolean M.tibetan_leap_day_p = tibetan_leap_day_p return M