Module:IriniaCalendar

From DSRPG
Revision as of 19:54, 4 May 2025 by Dubhghlas (talk | contribs)

Documentation for this module may be created at Module:IriniaCalendar/doc

-- Module:IriniaCalendar
-- A module for handling the custom calendar of Irinia

local p = {}

-- Normalize arguments for any #invoke call
local function getArgs(frame)
    -- Prefer named args on this frame
    if frame.args and next(frame.args) then
        return frame.args
    end
    -- Otherwise fall back to parent frame args
    if frame.getParent then
        local parent = frame:getParent()
        if parent and parent.args then
            return parent.args
        end
    end
    return {}
end

-- Configurable variables for the calendar system
local calendar = {
    -- Current date in the world
    current = { day = 4, month = 5, year = 1236 },

    -- Month names in order
    months = {
        [1] = "Frost Moon", [2] = "Shadow Moon", [3] = "Storm Moon",
        [4] = "Mist Moon",  [5] = "Bloom Moon",  [6] = "Sun Moon",
        [7] = "Thunder Moon", [8] = "Harvest Moon", [9] = "Ember Moon",
        [10] = "Frost Moon", [11] = "Twilight Moon", [12] = "Star Moon"
    },

    -- Days in each month
    days_in_month = {
        [1] = 30, [2] = 28, [3] = 30, [4] = 29,
        [5] = 31, [6] = 31, [7] = 30, [8] = 31,
        [9] = 30, [10] = 31, [11] = 28, [12] = 30
    },

    -- Weekday names
    weekdays = {
        [1] = "Silverday", [2] = "Brassday", [3] = "Woodday",
        [4] = "Stoneday", [5] = "Glassday", [6] = "Steamday",
        [7] = "Gearsday"
    },

    -- Seasons by month
    seasons = {
        [1] = "Winter", [2] = "Winter", [3] = "Spring",
        [4] = "Spring", [5] = "Spring", [6] = "Summer",
        [7] = "Summer", [8] = "Summer", [9] = "Autumn",
        [10] = "Autumn", [11] = "Autumn", [12] = "Winter"
    },

    era = "AE",

    -- Total days per year
    days_in_year = 359
}

-- Update the current in-world date
function p.updateCurrentDate(frame)
    local args = getArgs(frame)
    calendar.current.day   = tonumber(args.day)   or calendar.current.day
    calendar.current.month = tonumber(args.month) or calendar.current.month
    calendar.current.year  = tonumber(args.year)  or calendar.current.year
    return "Current date updated to " .. p.formatDate(calendar.current)
end

-- Get the formatted current date
function p.getCurrentDate(frame)
    local args = getArgs(frame)
    local fmt  = args.format or "full"
    return p.formatDate(calendar.current, fmt)
end

-- Convert numeric month to name
function p.getMonthName(frame)
    local args = getArgs(frame)
    local m    = tonumber(args.month) or 1
    local cnt  = #calendar.months
    if m < 1 or m > cnt then m = ((m-1)%cnt)+1 end
    return calendar.months[m]
end

-- Format a date (either a table or via #invoke frame)
function p.formatDate(dateOrFrame, format)
    local date, fmt
    if dateOrFrame and dateOrFrame.args then
        -- invoked via #invoke, grab named args
        local args = getArgs(dateOrFrame)
        date = {
            day   = tonumber(args.day)   or calendar.current.day,
            month = tonumber(args.month) or calendar.current.month,
            year  = tonumber(args.year)  or calendar.current.year
        }
        fmt = args.format or "full"
    else
        -- called programmatically
        date = dateOrFrame or {}
        fmt  = format or "full"
    end

    local day   = date.day   or 1
    local month = date.month or 1
    local year  = date.year  or 0
    if month < 1 or month > #calendar.months then
        month = ((month-1)%#calendar.months)+1
    end
    local monthName = calendar.months[month]

    if fmt == "full" then
        return day .. " " .. monthName .. ", " .. year .. " " .. calendar.era
    elseif fmt == "short" then
        return day .. " " .. monthName .. ", " .. year
    elseif fmt == "month-year" then
        return monthName .. ", " .. year .. " " .. calendar.era
    elseif fmt == "day-month" then
        return day .. " " .. monthName
    elseif fmt == "numeric" then
        return day .. "/" .. month .. "/" .. year .. " " .. calendar.era
    else
        return day .. " " .. monthName .. ", " .. year .. " " .. calendar.era
    end
end

-- Parse a date string back into a table
function p.parseDate(dateStr)
    local day, monName, year = string.match(dateStr, "(%d+)%s+(%a+%s*%a*),%s*(%d+)")
    if not day or not monName or not year then return nil end
    local month
    for i,name in ipairs(calendar.months) do
        if string.lower(name) == string.lower(monName) then month = i break end
    end
    if not month then return nil end
    return { day = tonumber(day), month = month, year = tonumber(year) }
end

-- Total days since year 0
function p.totalDays(d)
    if not d or not d.day or not d.month or not d.year then return 0 end
    local total = d.year * calendar.days_in_year
    for i=1, d.month-1 do total = total + calendar.days_in_month[i] end
    total = total + d.day
    return total
end

-- Compare two dates
function p.compareDate(a,b)
    if a.year ~= b.year then return a.year < b.year and -1 or 1 end
    if a.month ~= b.month then return a.month < b.month and -1 or 1 end
    if a.day ~= b.day then return a.day < b.day and -1 or 1 end
    return 0
end

-- Compute difference between two dates
function p.dateDifference(d1, d2)
    local negative = false
    if p.compareDate(d1,d2) > 0 then d1,d2 = d2,d1; negative = true end

    local years  = d2.year - d1.year
    local months = 0
    local days   = 0

    if d2.month >= d1.month then
        months = d2.month - d1.month
    else
        years = years - 1
        months = 12 - d1.month + d2.month
    end

    if d2.day >= d1.day then
        days = d2.day - d1.day
    else
        if months > 0 then
            months = months - 1
        else
            years = years - 1
            months = 11
        end
        local prevMon = d2.month - 1
        if prevMon < 1 then prevMon = 12 end
        days = calendar.days_in_month[prevMon] - d1.day + d2.day
    end

    local parts = {}
    if years ~= 0 then table.insert(parts, years .. " year" .. (years~=1 and "s" or "")) end
    if months~= 0 then table.insert(parts, months .. " month" .. (months~=1 and "s" or "")) end
    if days   ~= 0 or #parts == 0 then table.insert(parts, days   .. " day"   .. (days~=1   and "s" or "")) end

    local result = table.concat(parts, ", ")
    if #parts > 1 then
        result = string.gsub(result, ", ([^,]*)$", " and %1")
    end
    if negative then result = result .. " ago" end
    return result
end

-- Time since a given date
function p.timeSince(frame)
    local args = getArgs(frame)
    local d = { day=tonumber(args.day) or 1, month=tonumber(args.month) or 1, year=tonumber(args.year) or 0 }
    return p.dateDifference(d, calendar.current)
end

-- Time until a given date
function p.timeUntil(frame)
    local args = getArgs(frame)
    local d = { day=tonumber(args.day) or 1, month=tonumber(args.month) or 1, year=tonumber(args.year) or 0 }
    return p.dateDifference(calendar.current, d)
end

-- Timeline entry formatter
function p.timelineEntry(frame)
    local args   = getArgs(frame)
    local d      = { day=tonumber(args.day) or 1, month=tonumber(args.month) or 1, year=tonumber(args.year) or 0 }
    local fmt    = args.format or "full"
    local dateStr= p.formatDate(d, fmt)
    local ago    = p.dateDifference(d, calendar.current)
    local text   = args.text or ""
    return string.format("<div class='timeline-entry'><span class='timeline-date'>%s</span> <span class='timeline-ago'>(%s ago)</span>: <span class='timeline-text'>%s</span></div>", dateStr, ago, text)
end

-- Single-month calendar display
function p.calendarDisplay(frame)
    local args = getArgs(frame)
    local m    = tonumber(args.month) or calendar.current.month
    local y    = tonumber(args.year)  or calendar.current.year
    if m<1 or m>#calendar.months then m = ((m-1)%#calendar.months)+1 end

    local daysInM = calendar.days_in_month[m]
    local monName = calendar.months[m]
    local out = { string.format("<table class='calendar-table'><caption>%s %d %s</caption>", monName, y, calendar.era), "<tr class='calendar-header'>" }
    for _,wd in ipairs(calendar.weekdays) do table.insert(out, "<th>"..wd.."</th>") end
    table.insert(out, "</tr>")

    local firstWeekday = (p.totalDays({ day=1, month=m, year=y }) - 1) % #calendar.weekdays + 1
    table.insert(out, "<tr>")
    for i=1, firstWeekday-1 do table.insert(out, "<td class='calendar-empty'></td>") end

    local cw = firstWeekday
    for d=1, daysInM do
        local cls = (d==calendar.current.day and m==calendar.current.month and y==calendar.current.year)
                    and 'calendar-current-day' or 'calendar-day'
        table.insert(out, string.format("<td class='%s'>%d</td>", cls, d))
        cw = cw + 1
        if cw > #calendar.weekdays and d < daysInM then table.insert(out, "</tr><tr>"); cw = 1 end
    end
    for i=cw,#calendar.weekdays do table.insert(out, "<td class='calendar-empty'></td>") end
    table.insert(out, "</tr></table>")

    return table.concat(out)
end

-- Full-year calendar display
function p.yearCalendar(frame)
    local args = getArgs(frame)
    local y    = tonumber(args.year) or calendar.current.year
    local out = { string.format("<div class='year-calendar'><h2>Calendar for Year %d %s</h2>", y, calendar.era) }
    for m=1,#calendar.months do
        local subArgs = { args={ month=tostring(m), year=tostring(y) } }
        table.insert(out, "<div class='month-calendar'>" .. p.calendarDisplay(subArgs) .. "</div>")
    end
    table.insert(out, "</div>")
    return table.concat(out)
end

-- Season lookup
function p.getSeason(frame)
    local args = getArgs(frame)
    local m    = tonumber(args.month) or calendar.current.month
    if m<1 or m>#calendar.months then m = ((m-1)%#calendar.months)+1 end
    return calendar.seasons[m]
end

-- Calculate age in years
function p.calculateAge(frame)
    local args = getArgs(frame)
    local bd   = tonumber(args.day)   or 1
    local bm   = tonumber(args.month) or 1
    local by   = tonumber(args.year)  or 0
    local ageStr = p.dateDifference({ day=bd, month=bm, year=by }, calendar.current)
    local y     = string.match(ageStr, "^(%d+) years?")
    return y or "0"
end

-- Event time ago with optional prefix/suffix
function p.eventTimeAgo(frame)
    local args   = getArgs(frame)
    local d      = { day=tonumber(args.day) or 1, month=tonumber(args.month) or 1, year=tonumber(args.year) or 0 }
    local prefix = args.prefix or ""
    local suffix = args.suffix or " ago"
    return prefix .. p.dateDifference(d, calendar.current) .. suffix
end

return p