--- Balinese Pawukon 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-balinese -- @release 0.1 2026-07-19 local M = {} local basic = require("calendrica-basic") local gregorian = require("calendrica-gregorian") -- === Epoch === -- Fixed date of start of a Balinese Pawukon cycle. local BALI_EPOCH = basic.fixed_from_jd(146) -- === Date constructor and accessors === -- Balinese Pawukon date constructor. local function balinese_date(b1, b2, b3, b4, b5, b6, b7, b8, b9, b0) return {b1, b2, b3, b4, b5, b6, b7, b8, b9, b0} end -- Balinese Pawukon cycle accessors. local function bali_luang(b) return b[1] end -- luacheck: ignore local function bali_dwiwara(b) return b[2] end -- luacheck: ignore local function bali_triwara(b) return b[3] end -- luacheck: ignore local function bali_caturwara(b) return b[4] end -- luacheck: ignore local function bali_pancawara(b) return b[5] end local function bali_sadwara(b) return b[6] end local function bali_saptawara(b) return b[7] end local function bali_asatawara(b) return b[8] end -- luacheck: ignore local function bali_sangawara(b) return b[9] end -- luacheck: ignore local function bali_dasawara(b) return b[10] end -- luacheck: ignore -- === Day position in Pawukon cycle === -- Position of date in 210-day Pawukon cycle. local function bali_day_from_fixed(date) return (date - BALI_EPOCH) % 210 end -- === Individual cycle positions === -- Forward declarations for mutual recursion. local bali_pancawara_from_fixed local bali_saptawara_from_fixed -- Position of date in 10-day Balinese cycle. local function bali_dasawara_from_fixed(date) local i = bali_pancawara_from_fixed(date) - 1 local j = bali_saptawara_from_fixed(date) - 1 local pancawara_offset = ({5, 9, 7, 4, 8})[i + 1] local saptawara_offset = ({5, 4, 3, 7, 8, 6, 9})[j + 1] return (1 + pancawara_offset + saptawara_offset) % 10 end -- True if dasawara position is even. local function bali_luang_from_fixed(date) return bali_dasawara_from_fixed(date) % 2 == 0 end -- Position of date in 2-day Balinese cycle. local function bali_dwiwara_from_fixed(date) return basic.amod(bali_dasawara_from_fixed(date), 2) end -- Position of date in 3-day Balinese cycle. local function bali_triwara_from_fixed(date) return 1 + (bali_day_from_fixed(date) % 3) end -- Position of date in 5-day Balinese cycle. bali_pancawara_from_fixed = function(date) return basic.amod(bali_day_from_fixed(date) + 2, 5) end -- Position of date in 6-day Balinese cycle. local function bali_sadwara_from_fixed(date) return 1 + (bali_day_from_fixed(date) % 6) end -- Position of date in Balinese week (7-day cycle). bali_saptawara_from_fixed = function(date) return 1 + (bali_day_from_fixed(date) % 7) end -- Position of date in 8-day Balinese cycle. local function bali_asatawara_from_fixed(date) local day = bali_day_from_fixed(date) return 1 + (math.max(6, 4 + (day - 70) % 210) % 8) end -- Position of date in 9-day Balinese cycle. local function bali_sangawara_from_fixed(date) return 1 + (math.max(0, bali_day_from_fixed(date) - 3) % 9) end -- Position of date in 4-day Balinese cycle. local function bali_caturwara_from_fixed(date) return basic.amod(bali_asatawara_from_fixed(date), 4) end -- === Full Pawukon date === --- Positions of `date` in ten cycles of the Balinese Pawukon calendar. -- @tparam number date Fixed date. -- @treturn table Balinese date {luang, dwiwara, triwara, caturwara, pancawara, -- sadwara, saptawara, asatawara, sangawara, dasawara} function M.bali_pawukon_from_fixed(date) return balinese_date( bali_luang_from_fixed(date), bali_dwiwara_from_fixed(date), bali_triwara_from_fixed(date), bali_caturwara_from_fixed(date), bali_pancawara_from_fixed(date), bali_sadwara_from_fixed(date), bali_saptawara_from_fixed(date), bali_asatawara_from_fixed(date), bali_sangawara_from_fixed(date), bali_dasawara_from_fixed(date) ) end -- Week number of date in Balinese Pawukon cycle. local function bali_week_from_fixed(date) return 1 + basic.quotient(bali_day_from_fixed(date), 7) end -- === Inverse: find date on or before === -- Last fixed date on or before date with Pawukon b_date. local function bali_on_or_before(b_date, date) local pancawara = bali_pancawara(b_date) local sadwara = bali_sadwara(b_date) local saptawara = bali_saptawara(b_date) local a5 = pancawara - 1 local a6 = sadwara - 1 local b7 = saptawara - 1 local b35 = (a5 + 14 + 15 * (b7 - a5)) % 35 local days = a6 + 36 * (b35 - a6) local cap_delta = bali_day_from_fixed(basic.rd(0)) return date - ((date + cap_delta - days) % 210) end -- === Recurring dates === --- Occurrences of Kajeng Keliwon in Gregorian year `g_year`. -- Kajeng Keliwon falls every 15 days when the 3-day and 5-day weeks coincide. -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.kajeng_keliwon(g_year) local year = gregorian.gregorian_year_range(g_year) local cap_delta = bali_day_from_fixed(basic.rd(0)) return basic.positions_in_range(8, 15, cap_delta, year) end --- Occurrences of Tumpek in Gregorian year `g_year`. -- Tumpek falls every 35 days (intersection of 5-day and 7-day weeks). -- @tparam number g_year Gregorian year. -- @treturn {number,...} Fixed dates. function M.tumpek(g_year) local year = gregorian.gregorian_year_range(g_year) local cap_delta = bali_day_from_fixed(basic.rd(0)) return basic.positions_in_range(13, 35, cap_delta, year) end --- Week number (1..30) of `date` in the 210-day Balinese Pawukon cycle. -- @function bali_week_from_fixed -- @tparam number date Fixed date. -- @treturn number Week number. M.bali_week_from_fixed = bali_week_from_fixed --- Latest fixed date on or before `date` whose Pawukon equals `b_date`. -- @function bali_on_or_before -- @tparam table b_date Balinese Pawukon date. -- @tparam number date Fixed date. -- @treturn number Fixed date. M.bali_on_or_before = bali_on_or_before return M