Module:IriniaCalendar: Difference between revisions

From DSRPG
No edit summary
Tag: Manual revert
No edit summary
 
(2 intermediate revisions by the same user not shown)
Line 4: Line 4:
local p = {}
local p = {}


-- Configurable variables for the calendar system
-- Configurable calendar variables
local calendar = {
local calendar = {
    -- Current date in the world (can be updated as game progresses)
     current = { day = 4, month = 5, year = 1236 },
     current = {
        day = 4,
        month = 5, -- Bloom Moon
        year = 1236, -- AE (After Establishment)
    },
   
    -- 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_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)
     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 = {
     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_year = 359
    -- 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)
-- Format a date via #invoke args: day, month, year, format
function p.updateCurrentDate(frame)
function p.formatDate(frame)
     local args = frame.args
     local args = frame.args or {}
   
     local day   = tonumber(args.day)   or calendar.current.day
    -- If called via #invoke, get the args from the parent frame
     local month = tonumber(args.month) or calendar.current.month
    if not args or not args[1] then
     local year = tonumber(args.year) or calendar.current.year
        args = frame:getParent().args
     local fmt  = args.format         or "full"
     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
     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]
    return calendar.months[month]
end


-- Format a date according to the specified format
     if fmt == "full" then
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
         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
        -- fallback for unknown formats
         return day .. " " .. monthName .. ", " .. year .. " " .. calendar.era
         return day .. " " .. monthName .. ", " .. year .. " " .. calendar.era
     end
     end
end
end


-- Parse a date string into a date object
-- Total days since year 0
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)
function p.totalDays(date)
     if not date or not date.day or not date.month or not date.year then
     if not date or not date.day or not date.month or not date.year then return 0 end
        return 0
     local total = date.year * calendar.days_in_year
    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
     for i = 1, date.month - 1 do
         total = total + calendar.days_in_month[i]
         total = total + calendar.days_in_month[i]
     end
     end
   
    -- Add days in current month
     total = total + date.day
     total = total + date.day
   
     return total
     return total
end
end


-- Calculate how many years, months, and days between two dates
-- Compare two dates: returns -1, 0, or 1
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 -1 elseif a.year > b.year then return 1 end
    if a.month < b.month then return -1 elseif a.month > b.month then return 1 end
    if a.day < b.day then return -1 elseif a.day > b.day then return 1 end
    return 0
end
 
-- Calculate difference between two dates as a string
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
         years = years - 1
     if date2.month > date1.month then
         months = 12 - d1.month + d2.month
         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
     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 prev = d2.month - 1
        -- Add one month (now that we've borrowed)
         if prev < 1 then prev = 12 end
        months = months + 1
         days = calendar.days_in_month[prev] - 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
end


-- Compare two dates: returns -1 if date1 < date2, 0 if equal, 1 if date1 > date2
-- How long since a given date
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)
function p.timeSince(frame)
     local args = frame.args
     local args = frame.args or {}
      
     local d    = {
    -- If called via #invoke, get the args from the parent frame
         day   = tonumber(args.day)   or 1,
    if not args or not args[1] then
        month = tonumber(args.month) or 1,
         args = frame:getParent().args
        year = tonumber(args.year) or 0
    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
     }
     }
   
     return p.dateDifference(d, calendar.current)
    -- 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
-- How long until a given date
function p.timeUntil(frame)
function p.timeUntil(frame)
     local args = frame.args
     local args = frame.args or {}
   
     local d    = {
    -- If called via #invoke, get the args from the parent frame
         day   = tonumber(args.day)   or 1,
    if not args or not args[1] then
        month = tonumber(args.month) or 1,
        args = frame:getParent().args
        year = tonumber(args.year) or 0
    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
     }
     }
      
     return p.dateDifference(calendar.current, d)
    -- 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
end


-- Get the season for a given month
-- Calculate age in years given birth date
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)
function p.calculateAge(frame)
     local args = frame.args
     local args = frame.args or {}
   
     local d   = {
    -- If called via #invoke, get the args from the parent frame
         day  = tonumber(args.day)   or 1,
    if not args or not args[1] then
        month = tonumber(args.month) or 1,
        args = frame:getParent().args
        year  = tonumber(args.year) or 0
    end
   
    local birthDay = tonumber(args.day) or 1
    local birthMonth = tonumber(args.month) or 1
    local birthYear = tonumber(args.year) or 0
   
    local birthDate = {
        day = birthDay,
        month = birthMonth,
        year = birthYear
    }
   
    -- Calculate the difference between the birth date and the current date
     local age = p.dateDifference(birthDate, calendar.current)
   
    -- Extract just the years from the age string
    local years = string.match(age, "^(%d+) years?")
    if years then
        return years
    else
        return "0"
    end
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
     }
     }
   
     local diff = p.dateDifference(d, calendar.current)
    -- Calculate the difference between the event date and the current date
     local y    = string.match(diff, "^(%d+) years?")
     local timeAgo = p.dateDifference(eventDate, calendar.current)
     return y or "0"
      
     return prefix .. timeAgo .. suffix
end
end


return p
return p

Latest revision as of 20:00, 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 = {}

-- Configurable calendar variables
local calendar = {
    current = { day = 4, month = 5, year = 1236 },
    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_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
    },
    weekdays = {
        [1] = "Silverday", [2] = "Brassday", [3] = "Woodday",
        [4] = "Stoneday",  [5] = "Glassday", [6] = "Steamday",
        [7] = "Gearsday"
    },
    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",
    days_in_year = 359
}

-- Format a date via #invoke args: day, month, year, format
function p.formatDate(frame)
    local args  = frame.args or {}
    local day   = tonumber(args.day)   or calendar.current.day
    local month = tonumber(args.month) or calendar.current.month
    local year  = tonumber(args.year)  or calendar.current.year
    local fmt   = args.format          or "full"

    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
        -- fallback for unknown formats
        return day .. " " .. monthName .. ", " .. year .. " " .. calendar.era
    end
end

-- Total days since 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 = date.year * calendar.days_in_year
    for i = 1, date.month - 1 do
        total = total + calendar.days_in_month[i]
    end
    total = total + date.day
    return total
end

-- Compare two dates: returns -1, 0, or 1
function p.compareDate(a, b)
    if a.year < b.year then return -1 elseif a.year > b.year then return 1 end
    if a.month < b.month then return -1 elseif a.month > b.month then return 1 end
    if a.day < b.day then return -1 elseif a.day > b.day then return 1 end
    return 0
end

-- Calculate difference between two dates as a string
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 prev = d2.month - 1
        if prev < 1 then prev = 12 end
        days = calendar.days_in_month[prev] - 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

-- How long since a given date
function p.timeSince(frame)
    local args = frame.args or {}
    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

-- How long until a given date
function p.timeUntil(frame)
    local args = frame.args or {}
    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

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

return p