Module:IriniaCalendar: Difference between revisions

From DSRPG
No edit summary
No edit summary
Line 3: Line 3:


local p = {}
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
-- Configurable variables for the calendar system
local calendar = {
local calendar = {
     -- Current date in the world (can be updated as game progresses)
     -- Current date in the world
     current = {
     current = { day = 4, month = 5, year = 1236 },
        day = 4,
 
        month = 5, -- Bloom Moon
        year = 1236, -- AE (After Establishment)
    },
   
     -- Month names in order
     -- Month names in order
     months = {
     months = {
         [1] = "Frost Moon",
         [1] = "Frost Moon", [2] = "Shadow Moon", [3] = "Storm Moon",
        [2] = "Shadow Moon",
         [4] = "Mist Moon", [5] = "Bloom Moon", [6] = "Sun Moon",
        [3] = "Storm Moon",
         [7] = "Thunder Moon", [8] = "Harvest Moon", [9] = "Ember Moon",
         [4] = "Mist Moon",
         [10] = "Frost Moon", [11] = "Twilight Moon", [12] = "Star 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 (adjust as needed for your calendar)
     -- Days in each month
     days_in_month = {
     days_in_month = {
         [1] = 30, -- Frost Moon
         [1] = 30, [2] = 28, [3] = 30, [4] = 29,
        [2] = 28, -- Shadow Moon
         [5] = 31, [6] = 31, [7] = 30, [8] = 31,
        [3] = 30, -- Storm Moon
         [9] = 30, [10] = 31, [11] = 28, [12] = 30
        [4] = 29, -- Mist Moon
         [5] = 31, -- Bloom Moon
        [6] = 31, -- Sun Moon
        [7] = 30, -- Thunder Moon
        [8] = 31, -- Harvest Moon
         [9] = 30, -- Ember Moon
        [10] = 31, -- Frost Moon
        [11] = 28, -- Twilight Moon
        [12] = 30 -- Star Moon
     },
     },
   
 
     -- Days of the week (if your calendar uses a weekly cycle)
     -- Weekday names
     weekdays = {
     weekdays = {
         [1] = "Silverday",
         [1] = "Silverday", [2] = "Brassday", [3] = "Woodday",
        [2] = "Brassday",
         [4] = "Stoneday", [5] = "Glassday", [6] = "Steamday",
        [3] = "Woodday",
         [4] = "Stoneday",
        [5] = "Glassday",
        [6] = "Steamday",
         [7] = "Gearsday"
         [7] = "Gearsday"
     },
     },
   
 
     -- Seasons (useful for some calendar displays)
     -- Seasons by month
     seasons = {
     seasons = {
         [1] = "Winter", -- Frost Moon, Shadow Moon
         [1] = "Winter", [2] = "Winter", [3] = "Spring",
        [2] = "Winter",
         [4] = "Spring", [5] = "Spring", [6] = "Summer",
        [3] = "Spring", -- Storm Moon, Mist Moon, Bloom Moon
         [7] = "Summer", [8] = "Summer", [9] = "Autumn",
         [4] = "Spring",
         [10] = "Autumn", [11] = "Autumn", [12] = "Winter"
        [5] = "Spring",
        [6] = "Summer", -- Sun Moon, Thunder Moon, Harvest Moon
         [7] = "Summer",
        [8] = "Summer",
        [9] = "Autumn", -- Ember Moon, Frost Moon, Twilight Moon
         [10] = "Autumn",
        [11] = "Autumn",
        [12] = "Winter", -- Star Moon
     },
     },
   
 
    -- Era name (e.g., "AE" for "After Establishment")
     era = "AE",
     era = "AE",
   
 
     -- Days in a year
     -- Total days per year
     days_in_year = 359 -- Sum of all month days above
     days_in_year = 359
}
}


-- Update the current date (can be called from other modules or templates)
-- Update the current in-world date
function p.updateCurrentDate(frame)
function p.updateCurrentDate(frame)
     local args = frame.args
     local args = getArgs(frame)
   
     calendar.current.day   = tonumber(args.day)   or calendar.current.day
    -- If called via #invoke, get the args from the parent frame
    if not args or not args[1] then
        args = frame:getParent().args
    end
   
    -- Get the new current date
     calendar.current.day = tonumber(args.day) or calendar.current.day
     calendar.current.month = tonumber(args.month) or calendar.current.month
     calendar.current.month = tonumber(args.month) or calendar.current.month
     calendar.current.year = tonumber(args.year) or calendar.current.year
     calendar.current.year = tonumber(args.year) or calendar.current.year
   
    -- Return confirmation
     return "Current date updated to " .. p.formatDate(calendar.current)
     return "Current date updated to " .. p.formatDate(calendar.current)
end
end


-- Get the current date
-- Get the formatted current date
function p.getCurrentDate(frame)
function p.getCurrentDate(frame)
     local args = frame.args
     local args = getArgs(frame)
     local format = "full"
     local fmt  = args.format or "full"
   
     return p.formatDate(calendar.current, fmt)
    -- If called via #invoke, get the args from the parent frame
    if not args or not args[1] then
        args = frame:getParent().args
    end
   
    -- Get format parameter if provided
    if args.format then
        format = args.format
    end
   
    -- Format and return the current date
     return p.formatDate(calendar.current, format)
end
end


-- Convert numerical month to name
-- Convert numeric month to name
function p.getMonthName(frame)
function p.getMonthName(frame)
     local args = frame.args
     local args = getArgs(frame)
      
    local m    = tonumber(args.month) or 1
     -- If called via #invoke, get the args from the parent frame
     local cnt  = #calendar.months
     if not args or not args[1] then
     if m < 1 or m > cnt then m = ((m-1)%cnt)+1 end
         args = frame:getParent().args
    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
     end
   
    local month = tonumber(args.month) or 1
   
    -- Ensure month is in valid range
    if month < 1 or month > #calendar.months then
        month = ((month - 1) % #calendar.months) + 1
    end
   
    return calendar.months[month]
end


-- Format a date according to the specified format
     local day   = date.day   or 1
function p.formatDate(date, format)
    format = format or "full"
   
     local day = date.day or 1
     local month = date.month or 1
     local month = date.month or 1
     local year = date.year or 0
     local year = date.year or 0
   
    -- Ensure month is in valid range
     if month < 1 or month > #calendar.months then
     if month < 1 or month > #calendar.months then
         month = ((month - 1) % #calendar.months) + 1
         month = ((month-1)%#calendar.months)+1
     end
     end
   
     local monthName = calendar.months[month]
     local monthName = calendar.months[month]
   
 
    -- Different format options
     if fmt == "full" then
     if format == "full" then
         return day .. " " .. monthName .. ", " .. year .. " " .. calendar.era
         return day .. " " .. monthName .. ", " .. year .. " " .. calendar.era
     elseif format == "short" then
     elseif fmt == "short" then
         return day .. " " .. monthName .. ", " .. year
         return day .. " " .. monthName .. ", " .. year
     elseif format == "month-year" then
     elseif fmt == "month-year" then
         return monthName .. ", " .. year .. " " .. calendar.era
         return monthName .. ", " .. year .. " " .. calendar.era
     elseif format == "day-month" then
     elseif fmt == "day-month" then
         return day .. " " .. monthName
         return day .. " " .. monthName
     elseif format == "numeric" then
     elseif fmt == "numeric" then
         return day .. "/" .. month .. "/" .. year .. " " .. calendar.era
         return day .. "/" .. month .. "/" .. year .. " " .. calendar.era
     else
     else
Line 166: Line 127:
end
end


-- Parse a date string into a date object
-- Parse a date string back into a table
function p.parseDate(dateStr)
function p.parseDate(dateStr)
     local day, monthName, year = string.match(dateStr, "(%d+)%s+(%a+%s*%a*),%s*(%d+)")
     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
     if not day or not monthName or not year then
    local month
         return nil
    for i,name in ipairs(calendar.months) do
         if string.lower(name) == string.lower(monName) then month = i break end
     end
     end
   
     if not month then return nil end
    -- Find the month number from the name
     return { day = tonumber(day), month = month, year = tonumber(year) }
    local month = nil
    for i, name in ipairs(calendar.months) do
        if string.lower(name) == string.lower(monthName) then
            month = i
            break
        end
    end
   
     if not month then
        return nil
    end
   
     return {
        day = tonumber(day),
        month = month,
        year = tonumber(year)
    }
end
end


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


-- Calculate how many years, months, and days between two dates
-- Compare two dates
function p.dateDifference(date1, date2)
function p.compareDate(a,b)
     -- If date2 is before date1, swap them and set a negative flag
     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
     local negative = false
     if p.compareDate(date1, date2) > 0 then
     if p.compareDate(d1,d2) > 0 then d1,d2 = d2,d1; negative = true end
        date1, date2 = date2, date1
 
        negative = true
     local years = d2.year - d1.year
    end
   
    -- Initialize with zeros
     local years = 0
     local months = 0
     local months = 0
     local days = 0
     local days   = 0
   
 
    -- Start with the difference in years
     if d2.month >= d1.month then
    years = date2.year - date1.year
         months = d2.month - d1.month
   
     else
    -- Now calculate months and days, adjusting as needed
     if date2.month > date1.month then
         months = date2.month - date1.month
     elseif date2.month < date1.month then
         years = years - 1
         years = years - 1
         months = 12 - date1.month + date2.month
         months = 12 - d1.month + d2.month
    else -- Same month
        months = 0
     end
     end
   
 
    -- Calculate days
     if d2.day >= d1.day then
     if date2.day >= date1.day then
         days = d2.day - d1.day
         days = date2.day - date1.day
     else
     else
        -- Need to borrow from months
         if months > 0 then
         if months > 0 then
             months = months - 1
             months = months - 1
         else
         else
             -- Need to borrow from years
             years = years - 1
            if years > 0 then
            months = 11
                years = years - 1
                months = 11 -- We're setting to 11 because we'll add one below
            end
         end
         end
       
         local prevMon = d2.month - 1
        -- Add one month (now that we've borrowed)
         if prevMon < 1 then prevMon = 12 end
        months = months + 1
         days = calendar.days_in_month[prevMon] - d1.day + d2.day
       
        -- Calculate the days, considering the length of the previous month
         local prevMonth = date2.month - 1
         if prevMonth < 1 then prevMonth = 12 end
       
         days = calendar.days_in_month[prevMonth] - date1.day + date2.day
     end
     end
   
 
    -- Format the result
     local parts = {}
     local result = {}
     if years ~= 0 then table.insert(parts, years .. " year" .. (years~=1 and "s" or "")) end
     if years ~= 0 then
    if months~= 0 then table.insert(parts, months .. " month" .. (months~=1 and "s" or "")) end
        table.insert(result, years .. " year" .. (years ~= 1 and "s" or ""))
    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
     end
     if months ~= 0 then
     if negative then result = result .. " ago" end
        table.insert(result, months .. " month" .. (months ~= 1 and "s" or ""))
     return result
    end
    if days ~= 0 or #result == 0 then
        table.insert(result, days .. " day" .. (days ~= 1 and "s" or ""))
    end
   
    local output = table.concat(result, ", ")
   
    -- Replace the last comma with "and" if there's more than one element
    if #result > 1 then
        output = string.gsub(output, ", ([^,]*)$", " and %1")
    end
   
    if negative then
        output = output .. " ago"
    end
   
     return output
end
 
-- Compare two dates: returns -1 if date1 < date2, 0 if equal, 1 if date1 > date2
function p.compareDate(date1, date2)
    if date1.year < date2.year then return -1 end
    if date1.year > date2.year then return 1 end
   
    -- Same year, check month
    if date1.month < date2.month then return -1 end
    if date1.month > date2.month then return 1 end
   
    -- Same year and month, check day
    if date1.day < date2.day then return -1 end
    if date1.day > date2.day then return 1 end
   
    -- Dates are equal
    return 0
end
end


-- Calculate time since a given date, relative to the current date
-- Time since a given date
function p.timeSince(frame)
function p.timeSince(frame)
     local args = frame.args
     local args = getArgs(frame)
   
     local d = { day=tonumber(args.day) or 1, month=tonumber(args.month) or 1, year=tonumber(args.year) or 0 }
    -- If called via #invoke, get the args from the parent frame
     return p.dateDifference(d, calendar.current)
    if not args or not args[1] then
        args = frame:getParent().args
    end
   
     local day = tonumber(args.day) or 1
    local month = tonumber(args.month) or 1
    local year = tonumber(args.year) or 0
   
    local date = {
        day = day,
        month = month,
        year = year
    }
   
    -- Calculate the difference between the provided date and the current date
     return p.dateDifference(date, calendar.current)
end
end


-- Calculate time until a given date, relative to the current date
-- Time until a given date
function p.timeUntil(frame)
function p.timeUntil(frame)
     local args = frame.args
     local args = getArgs(frame)
   
     local d = { day=tonumber(args.day) or 1, month=tonumber(args.month) or 1, year=tonumber(args.year) or 0 }
    -- If called via #invoke, get the args from the parent frame
     return p.dateDifference(calendar.current, d)
    if not args or not args[1] then
        args = frame:getParent().args
    end
   
     local day = tonumber(args.day) or 1
    local month = tonumber(args.month) or 1
    local year = tonumber(args.year) or 0
   
    local date = {
        day = day,
        month = month,
        year = year
    }
   
    -- Calculate the difference between the current date and the provided date
     return p.dateDifference(calendar.current, date)
end
end


-- Format a timeline entry with a specific date and event text
-- Timeline entry formatter
function p.timelineEntry(frame)
function p.timelineEntry(frame)
     local args = frame.args
     local args   = getArgs(frame)
   
     local d      = { day=tonumber(args.day) or 1, month=tonumber(args.month) or 1, year=tonumber(args.year) or 0 }
    -- If called via #invoke, get the args from the parent frame
     local fmt    = args.format or "full"
    if not args or not args[1] then
     local dateStr= p.formatDate(d, fmt)
        args = frame:getParent().args
     local ago   = p.dateDifference(d, calendar.current)
    end
     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)
     local day = tonumber(args.day) or 1
    local month = tonumber(args.month) or 1
    local year = tonumber(args.year) or 0
     local text = args.text or ""
    local format = args.format or "full"
   
    local date = {
        day = day,
        month = month,
        year = year
    }
   
    -- Format the date according to the specified format
     local dateStr = p.formatDate(date, format)
      
    -- Calculate how long ago this date was
    local timeAgo = p.dateDifference(date, calendar.current)
      
    -- Format the timeline entry
     return "<div class='timeline-entry'><span class='timeline-date'>" .. dateStr .. "</span> <span class='timeline-ago'>(" .. timeAgo .. " ago)</span>: <span class='timeline-text'>" .. text .. "</span></div>"
end
end


-- Generate a calendar display for a specific month and year
-- Single-month calendar display
function p.calendarDisplay(frame)
function p.calendarDisplay(frame)
     local args = frame.args
     local args = getArgs(frame)
      
    local m    = tonumber(args.month) or calendar.current.month
     -- If called via #invoke, get the args from the parent frame
    local y    = tonumber(args.year)  or calendar.current.year
     if not args or not args[1] then
    if m<1 or m>#calendar.months then m = ((m-1)%#calendar.months)+1 end
         args = frame:getParent().args
 
    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
     end
   
     for i=cw,#calendar.weekdays do table.insert(out, "<td class='calendar-empty'></td>") end
    local month = tonumber(args.month) or calendar.current.month
     table.insert(out, "</tr></table>")
    local year = tonumber(args.year) or calendar.current.year
 
   
     return table.concat(out)
    -- Ensure month is in valid range
    if month < 1 or month > #calendar.months then
        month = ((month - 1) % #calendar.months) + 1
    end
   
    local daysInMonth = calendar.days_in_month[month]
    local monthName = calendar.months[month]
   
    -- Start building the calendar table
    local output = "<table class='calendar-table'>\n"
    output = output .. "<caption>" .. monthName .. " " .. year .. " " .. calendar.era .. "</caption>\n"
   
    -- Add weekday headers
    output = output .. "<tr class='calendar-header'>\n"
     for _, dayName in ipairs(calendar.weekdays) do
        output = output .. "<th>" .. dayName .. "</th>\n"
    end
    output = output .. "</tr>\n"
   
    -- Calculate the weekday of the first day of the month
    -- This is a simplified calculation assuming the first day of year 0 was weekday 1
    local totalDays = p.totalDays({day = 1, month = month, year = year}) - 1
    local firstWeekday = (totalDays % #calendar.weekdays) + 1
   
    -- Start the first week
    output = output .. "<tr>\n"
   
    -- Add empty cells for days before the first of the month
    for i = 1, firstWeekday - 1 do
        output = output .. "<td class='calendar-empty'></td>\n"
    end
   
    -- Add the days of the month
    local currentWeekday = firstWeekday
    for day = 1, daysInMonth do
        -- Check if this day is the current date
        local isCurrentDate = (day == calendar.current.day and month == calendar.current.month and year == calendar.current.year)
        local cellClass = isCurrentDate and "calendar-current-day" or "calendar-day"
       
        output = output .. "<td class='" .. cellClass .. "'>" .. day .. "</td>\n"
       
        -- Move to the next weekday
        currentWeekday = currentWeekday + 1
       
        -- Start a new week if necessary
        if currentWeekday > #calendar.weekdays and day < daysInMonth then
            output = output .. "</tr>\n<tr>\n"
            currentWeekday = 1
        end
    end
   
    -- Add empty cells for days after the end of the month
    for i = currentWeekday, #calendar.weekdays do
        output = output .. "<td class='calendar-empty'></td>\n"
    end
      
    -- Close the final week and the table
    output = output .. "</tr>\n</table>"
   
     return output
end
end


-- Generate an annual calendar display
-- Full-year calendar display
function p.yearCalendar(frame)
function p.yearCalendar(frame)
     local args = frame.args
     local args = getArgs(frame)
   
     local y    = tonumber(args.year) or calendar.current.year
    -- If called via #invoke, get the args from the parent frame
     local out = { string.format("<div class='year-calendar'><h2>Calendar for Year %d %s</h2>", y, calendar.era) }
    if not args or not args[1] then
     for m=1,#calendar.months do
        args = frame:getParent().args
         local subArgs = { args={ month=tostring(m), year=tostring(y) } }
    end
        table.insert(out, "<div class='month-calendar'>" .. p.calendarDisplay(subArgs) .. "</div>")
   
     local year = tonumber(args.year) or calendar.current.year
   
    -- Start building the year calendar
     local output = "<div class='year-calendar'>\n"
    output = output .. "<h2>Calendar for Year " .. year .. " " .. calendar.era .. "</h2>\n"
   
    -- Add each month
     for month = 1, #calendar.months do
         output = output .. "<div class='month-calendar'>\n"
       
        -- Create arguments for the month calendar
        local monthArgs = {
            month = month,
            year = year
        }
       
        -- Generate the calendar for this month
        output = output .. p.calendarDisplay({ args = monthArgs })
        output = output .. "</div>\n"
     end
     end
      
     table.insert(out, "</div>")
    output = output .. "</div>"
     return table.concat(out)
   
     return output
end
end


-- Get the season for a given month
-- Season lookup
function p.getSeason(frame)
function p.getSeason(frame)
     local args = frame.args
     local args = getArgs(frame)
   
     local m    = tonumber(args.month) or calendar.current.month
    -- If called via #invoke, get the args from the parent frame
     if m<1 or m>#calendar.months then m = ((m-1)%#calendar.months)+1 end
    if not args or not args[1] then
     return calendar.seasons[m]
        args = frame:getParent().args
    end
   
     local month = tonumber(args.month) or calendar.current.month
   
    -- Ensure month is in valid range
     if month < 1 or month > #calendar.months then
        month = ((month - 1) % #calendar.months) + 1
    end
   
     return calendar.seasons[month]
end
end


-- Calculate a person's age given their birth date
-- Calculate age in years
function p.calculateAge(frame)
function p.calculateAge(frame)
     local args       = frame.args            or {}
     local args = getArgs(frame)
    local parentArgs = (frame.getParent and frame:getParent().args) or {}
     local bd   = tonumber(args.day)  or 1
 
     local bm  = tonumber(args.month) or 1
    -- pull each field, preferring the named arg in frame.args
     local by  = tonumber(args.year)  or 0
     local birthDay   = tonumber(args.day  or parentArgs.day)  or 1
     local ageStr = p.dateDifference({ day=bd, month=bm, year=by }, calendar.current)
     local birthMonth = tonumber(args.month or parentArgs.month) or 1
     local y    = string.match(ageStr, "^(%d+) years?")
     local birthYear  = tonumber(args.year  or parentArgs.year)  or 0
     return y or "0"
 
     local birthDate = {
        day   = birthDay,
        month = birthMonth,
        year = birthYear
    }
 
    local age = p.dateDifference(birthDate, calendar.current)
     local years = string.match(age, "^(%d+) years?")
     return years or "0"
end
end


 
-- Event time ago with optional prefix/suffix
-- Calculate how long ago an event occurred
function p.eventTimeAgo(frame)
function p.eventTimeAgo(frame)
     local args = frame.args
     local args   = getArgs(frame)
   
     local d      = { day=tonumber(args.day) or 1, month=tonumber(args.month) or 1, year=tonumber(args.year) or 0 }
    -- If called via #invoke, get the args from the parent frame
    if not args or not args[1] then
        args = frame:getParent().args
    end
   
     local eventDay = tonumber(args.day) or 1
    local eventMonth = tonumber(args.month) or 1
    local eventYear = tonumber(args.year) or 0
     local prefix = args.prefix or ""
     local prefix = args.prefix or ""
     local suffix = args.suffix or " ago"
     local suffix = args.suffix or " ago"
      
     return prefix .. p.dateDifference(d, calendar.current) .. suffix
    local eventDate = {
        day = eventDay,
        month = eventMonth,
        year = eventYear
    }
   
    -- Calculate the difference between the event date and the current date
    local timeAgo = p.dateDifference(eventDate, calendar.current)
   
    return prefix .. timeAgo .. suffix
end
end


return p
return p

Revision as of 19:54, 4 May 2025

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