Module:IriniaCalendar

From DSRPG
Revision as of 18:58, 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 = {}

-- Configurable variables for the calendar system
local calendar = {
    -- Current date in the world (can be updated as game progresses)
    current = {
        day = 4,
        month = 5, -- Bloom Moon
        year = 1236, -- AE (After Establishment)
    },
    
    -- 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 (adjust as needed for your calendar)
    days_in_month = {
        [1] = 30, -- Frost Moon
        [2] = 28, -- Shadow Moon
        [3] = 30, -- Storm Moon
        [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)
    weekdays = {
        [1] = "Silverday",
        [2] = "Brassday",
        [3] = "Woodday",
        [4] = "Stoneday",
        [5] = "Glassday",
        [6] = "Steamday",
        [7] = "Gearsday"
    },
    
    -- Seasons (useful for some calendar displays)
    seasons = {
        [1] = "Winter", -- Frost Moon, Shadow Moon
        [2] = "Winter",
        [3] = "Spring", -- Storm Moon, Mist Moon, Bloom Moon
        [4] = "Spring",
        [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",
    
    -- Days in a year
    days_in_year = 359 -- Sum of all month days above
}

-- Update the current date (can be called from other modules or templates)
function p.updateCurrentDate(frame)
    local args = frame.args
    
    -- 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.year = tonumber(args.year) or calendar.current.year
    
    -- Return confirmation
    return "Current date updated to " .. p.formatDate(calendar.current)
end

-- Get the current date
function p.getCurrentDate(frame)
    local args = frame.args
    local format = "full"
    
    -- 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

-- Convert numerical month to name
function p.getMonthName(frame)
    local args = frame.args
    
    -- 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 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
function p.formatDate(date, format)
    format = format or "full"
    
    local day = date.day or 1
    local month = date.month or 1
    local year = date.year or 0
    
    -- Ensure month is in valid range
    if month < 1 or month > #calendar.months then
        month = ((month - 1) % #calendar.months) + 1
    end
    
    local monthName = calendar.months[month]
    
    -- Different format options
    if format == "full" then
        return day .. " " .. monthName .. ", " .. year .. " " .. calendar.era
    elseif format == "short" then
        return day .. " " .. monthName .. ", " .. year
    elseif format == "month-year" then
        return monthName .. ", " .. year .. " " .. calendar.era
    elseif format == "day-month" then
        return day .. " " .. monthName
    elseif format == "numeric" then
        return day .. "/" .. month .. "/" .. year .. " " .. calendar.era
    else
        return day .. " " .. monthName .. ", " .. year .. " " .. calendar.era
    end
end

-- Parse a date string into a date object
function p.parseDate(dateStr)
    local day, monthName, year = string.match(dateStr, "(%d+)%s+(%a+%s*%a*),%s*(%d+)")
    
    if not day or not monthName or not year then
        return nil
    end
    
    -- Find the month number from the name
    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

-- Calculate total days from year 0
function p.totalDays(date)
    if not date or not date.day or not date.month or not date.year then
        return 0
    end
    
    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
end

-- Calculate how many years, months, and days between two dates
function p.dateDifference(date1, date2)
    -- If date2 is before date1, swap them and set a negative flag
    local negative = false
    if p.compareDate(date1, date2) > 0 then
        date1, date2 = date2, date1
        negative = true
    end
    
    -- Initialize with zeros
    local years = 0
    local months = 0
    local days = 0
    
    -- Start with the difference in years
    years = date2.year - date1.year
    
    -- 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
        months = 12 - date1.month + date2.month
    else -- Same month
        months = 0
    end
    
    -- Calculate days
    if date2.day >= date1.day then
        days = date2.day - date1.day
    else
        -- Need to borrow from months
        if months > 0 then
            months = months - 1
        else
            -- Need to borrow from years
            if years > 0 then
                years = years - 1
                months = 11 -- We're setting to 11 because we'll add one below
            end
        end
        
        -- Add one month (now that we've borrowed)
        months = months + 1
        
        -- 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
    
    -- Format the result
    local result = {}
    if years ~= 0 then
        table.insert(result, years .. " year" .. (years ~= 1 and "s" or ""))
    end
    if months ~= 0 then
        table.insert(result, months .. " month" .. (months ~= 1 and "s" or ""))
    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

-- Calculate time since a given date, relative to the current date
function p.timeSince(frame)
    local args = frame.args
    
    -- 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 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

-- Calculate time until a given date, relative to the current date
function p.timeUntil(frame)
    local args = frame.args
    
    -- 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 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

-- Format a timeline entry with a specific date and event text
function p.timelineEntry(frame)
    local args = frame.args
    
    -- 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 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

-- Generate a calendar display for a specific month and year
function p.calendarDisplay(frame)
    local args = frame.args
    
    -- 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 month = tonumber(args.month) or calendar.current.month
    local year = tonumber(args.year) or calendar.current.year
    
    -- 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

-- Generate an annual calendar display
function p.yearCalendar(frame)
    local args = frame.args
    
    -- 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 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
    
    output = output .. "</div>"
    
    return output
end

-- Get the season for a given month
function p.getSeason(frame)
    local args = frame.args
    
    -- 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 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

-- Calculate a person's age given their birth date
function p.calculateAge(frame)
    local args       = frame.args            or {}
    local parentArgs = (frame.getParent and frame:getParent().args) or {}

    -- pull each field, preferring the named arg in frame.args
    local birthDay   = tonumber(args.day   or parentArgs.day)   or 1
    local birthMonth = tonumber(args.month or parentArgs.month) or 1
    local birthYear  = tonumber(args.year  or parentArgs.year)  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


-- Calculate how long ago an event occurred
function p.eventTimeAgo(frame)
    local args = frame.args
    
    -- 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 suffix = args.suffix or " ago"
    
    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

return p