Module:Calendar

local module = {}

function timestamp(t) return os.date("%Y%m%d", t) -- full fandom timestamp YYYYMMDDHHmmss end

function now -- Guild day - WEB / Mobile local d = os.date('!*t') -- using os.time - 2 * 60 * 60 is undefined behaviour d.hour = d.hour - 2 -- UTC -2 -- according to the lua reference os.time can return any value, no guarantee that it is in seconds return os.date('!*t', os.time(d)) -- refetch table if hours got below 0ignore end

local date = setmetatable({}, {   __call = function(self, params)        local createDate        local meta = {            __call = function(self, params)                if params then                    return createDate{                        year = params.year or 0,                        month = params.month or 0,                        day = params.day or 0,                    }                end            end,            __add = function(a, b)                return createDate{                    year =  a.year  + b.year,                    month = a.month + b.month,                    day =   a.day   + b.day,                }            end,            __sub = function(a, b)                return createDate{                    year =  a.year  - b.year,                    month = a.month - b.month,                    day =   a.day   - b.day,                }            end,            __mul = function(a, b)                return createDate{ year = a.year  * b,                    month = a.month * b,                    day =   a.day   * b,                } end, __lt = function(a, b)               return a.time < b.time end, __le = function(a, b)               return a.time <= b.time end, }       meta.__index = meta createDate = function(t) local success, result = pcall(os.time, t)

if(success and result >= 0) then t = os.date('*t', result) t.time = result end

return setmetatable(t, meta) end

return setmetatable(self, meta)(params) end })

local event = setmetatable({}, {   __call = function(self, params)        local meta = {}

meta.__call = function(self, params) local obj = {}

for k, v in pairs(params) do               obj[k] = v            end

obj.start = date(obj.start) obj.duration = date(obj.duration) obj.interval = date(obj.interval)

if obj.start then obj['end'] = obj.start + obj.duration end

return setmetatable(obj, meta) end

meta.setStart = function(self, start) self.start = date(start) self['end'] = self.start + self.duration

return self end

meta.__index = meta

return setmetatable(self, meta)(params) end })

local calendar = setmetatable({}, {   __call = function(self, params)        local meta = {}        local oneDay = date{day = 1}

meta.__call = function(self, params) local cdata = {} local self = event({               name = 'calendar',                start = params.start,                duration = params.duration,            }) self.data = cdata -- fill timespan with empty tables local cstart = self.start local cend = self['end']

while cstart < cend do               local t = {} local stamp = cstart.time -- reference by index table.insert(cdata, {stamp, t}) -- reference by key cdata[stamp] = t               cstart = cstart + oneDay end

return setmetatable(self, meta) end

local insertEvent = function(dest, event, estart, eend) local day = 0 -- counting event days

while estart < eend do               local d = dest[estart.time]

if d then table.insert(d, event)

event[estart.time] = day -- saving which day of the event this timestamp represent end

estart = estart + oneDay day = day + 1 end

event.lastDay = day - 1 end

meta.add = function(self, event) local cstart = self.start local cend = self['end'] local cdata = self.data

local estart = event.start local einterval = event.interval local eend = event['end']

if einterval then local eduration = event.duration -- loop until first occurrence, kinda inefficient -- but easier than reducing years / months to days while eend <= cstart do                   eend = eend + einterval end

estart = eend - eduration -- loop event until out of timespan while estart < cend do                   insertEvent(cdata, event, estart, eend)

eend = eend + einterval estart = eend - eduration end else if cstart < eend and estart < cend then insertEvent(cdata, event, estart, eend) end end

return self end

meta.__index = meta

return setmetatable(self, meta)(params) end })

function fillCalendar(params) local calendar = calendar(params)

for _, e in ipairs(params.data) do       for _, v in ipairs(e) do            e.start = parseDate(v.timestamp) e.duration = parseDate(v.duration) e.interval = parseDate(v.interval)

calendar:add(event(e)) end end

return calendar end

function parseDate(str) local number = tonumber(str)

if number then local year, month, day = str:match('^(%d%d%d%d)(%d%d)(%d%d)$') -- YYYYMMDD

if year then year = tonumber(year) month = tonumber(month) day = tonumber(day)

return { year = year and year > 0 and year or nil, month = month and month > 0 and month or nil, day = day and day > 0 and day or nil, }       end

return {day = number} end end

function loadParameter(title, args) local phistory = 'history%d' local pduration = 'duration%d' local pinterval = 'interval%d' local duration = args.duration local interval = args.interval local value = args.history1 local idx = 1 -- get parameters local event = { name = title, button = args.button, short = args.title, class = args.class, }   while value do        table.insert(event, {            timestamp = value,            duration = args[pduration:format(idx)] or duration,            interval = args[pinterval:format(idx)] or interval,        }) idx = idx + 1 value = args[phistory:format(idx)] end

table.sort(event, function(a, b) return a.timestamp < b.timestamp end)

return event end

function module.history(frame) local fargs = frame.args local pargs = frame:getParent.args

local name = fargs.title local event = loadParameter(name, pargs)

if not pargs.ignore then -- only outupt something if there are history entries local output = {} local key = name .. '_' .. (event.class or '') -- format output for parsing for k1, v in pairs(event) do           if type(v) == 'table' then for k2, v in pairs(v) do                   table.insert(output, string.format('\t%s_%s_%s\t%s', key, k1, k2, v)) end else table.insert(output, string.format('\t%s_%s\t%s', key, k1, v)) end end

return table.concat(output) end end

function loadData(frame) frame = frame or mw.getCurrentFrame

local fargs = frame.args local data = frame:callParserFunction{name = '#dpl', args = { '', -- necessary for callParserFunction mode = 'userformat', uses = 'Template:Infobox Event', category = fargs.platform or 'Browser', ordermethod = 'sortkey', include = '{Infobox Event¦Event/History}', }}   local events = {} local meta_data = { __index = function(self, key) local data = setmetatable({}, meta_data)

rawset(self, key, data)

return data end, }   local meta_event =  { __index = function(self, key) local data = setmetatable({}, meta_data)

rawset(events, #events + 1, data) rawset(self, key, data)

return data end, }   setmetatable(events, {        __index = function(self, key)            local data = setmetatable({}, meta_event)

rawset(self, key, data)

return data end, })   for entry, value in string.gmatch(data, '\t([^\t]+)\t([^\t]+)') do        local event = events

for key in string.gmatch(entry, '([^_]*)_') do           event = event[tonumber(key) or key] end

local key = entry:match('_([^_]+)$')

event[tonumber(key) or key] = value end -- remove itself on next call meta_data.__index = function(self, key) setmetatable(self, nil) end

return setmetatable(events, nil) end

function calendarEntries(frame, args) frame = frame or mw.getCurrentFrame

local fargs = args or frame.args or {} local now = now local calendar = fillCalendar{ start = { year = fargs.start_year  or now.year, month = fargs.start_month or now.month, day =  fargs.start_day   or now.day, },       duration = { year = fargs.duration_year  or nil, month = fargs.duration_month or nil, day =  fargs.duration_day   or nil, },       data = loadData(frame), }   local rows = {} local data = calendar.data

local istart, iend, istep

if fargs.reverse == "true" then istart = #data iend = 1 istep = -1 else istart = 1 iend = #data istep = 1 end

for i = istart, iend, istep do       local data = data[i] local stamp = data[1] local entries = data[2]

for k, v in ipairs(entries) do           entries[k] = frame:expandTemplate{title = 'Calendar/Entry', args = { event = v.name, short = v.short, button = v.button and string.gsub(v.button, '%$(.-)%$', os.date('*t', stamp)), days = v.lastDay - v[stamp] + 1, tab = v.class, }}       end

table.insert(rows, {           timestamp = timestamp(stamp),            entries = table.concat(entries),        }) end

return rows end

function module.calendar(frame, args) local rows = calendarEntries(frame, args)

for k, v in ipairs(rows) do       rows[k] = frame:expandTemplate{title = 'Calendar/Row', args = v}    end

return frame:expandTemplate{title = 'DataTable', args = { style = "text-align: left;", content = table.concat(rows), }} end

function module.calendarGrid(frame, args) local rows = calendarEntries(frame, args) local divs = {}

for k, v in ipairs(rows) do       table.insert(divs, frame:expandTemplate{title = 'Calendar/Date', args = {v.timestamp}}) table.insert(divs, string.format(' %s ', v.entries)) end

return string.format(' %s ', table.concat(divs)) end

function formatDuration(t) if t then local output = {}

for k, v in pairs(t) do           if v == 1 then table.insert(output, '1 ' .. k)           else table.insert(output, string.format('%s %ss', v, k)) end end

table.sort(output)

return table.concat(output, ', ') end

return nil end

function module.infobox(frame, fargs, pargs) local fargs = fargs or frame.args local pargs = pargs or frame:getParent.args local event = loadParameter(fargs.title, pargs) local lang = mw.getContentLanguage -- copy args to event for k, v in pairs(pargs) do       event[k] = v    end -- format history if not set if not event.history then local history = {} local today = date(now)

for k, v in ipairs(event) do           if v.interval then local step = date(parseDate(v.interval)) local d = date(parseDate(v.timestamp)) local duration = v.duration

while d <= today do                   table.insert(history, {timestamp = timestamp(os.time(d)), duration = duration})

d = d + step end else table.insert(history, v)           end -- disable all history entries because tables can't be passed to target template event[k] = nil end if #history > 0 then -- sort history by year local year = setmetatable({}, {               __index = function(self, key)                    local data = {}

rawset(self, #self + 1, key) rawset(self, key, data)

return data end, })           -- format history            for k, v in ipairs(history) do                local timestamp = v.timestamp

table.insert(year[timestamp:sub(0, 4)], string.format(' %s (%s) ', os.date('%a, %d %B %Y', os.time(parseDate(timestamp))), formatDuration(parseDate(v.duration)) ))           end -- create tabber local tabber = {}

for i = #year, 1, -1 do                local v = year[i]

table.insert(tabber, string.format('|-|%s= %s ', v, table.concat(year[v]))) end

event.duration = nil event.history = frame:extensionTag{name = 'tabber', content = table.concat(tabber)} end end -- format rewards if not set if not event.rewards then local data = frame:callParserFunction{name = '#dpl', args = { '', -- necessary for callParserFunction mode = 'userformat', skipthispage = 'false', title = mw.title.getCurrentTitle.text, include = '{Event/Quests/Entry¦Event/Quests/Total}', }}       local meta_count = { __index = function(self, key) rawset(self, #self + 1, key)

return 0 end, }       local meta_key = { __index = function(self, key) local data = setmetatable({}, meta_count)

rawset(self, #self + 1, key) rawset(self, key, data)

return data end, }       local count = setmetatable({}, {            __index = function(self, key)                local data = setmetatable({}, meta_key)

rawset(self, #self + 1, key) rawset(self, key, data)

return data end, })       for key, quest_class, quest_type, value in string.gmatch(data, '\t([^=]+)=([^=]*)=([^=]*)=([^\t]+)') do            local count = count[quest_class][key]

count[quest_type] = count[quest_type] + tonumber(value) end

if #count > 0 then setmetatable(count, nil)

for idx, header in ipairs(count) do               local rewards = setmetatable(count[header], nil)

table.sort(rewards)

for idx, key in ipairs(rewards) do                   local data = setmetatable(rewards[key], nil) table.sort(data) for idx, quest_type in ipairs(data) do                       local count = lang:formatNum(data[quest_type]) if quest_type == '' then data[idx] = count else data[idx] = string.format('%s (%s)', count, quest_type) end end rewards[idx] = string.format('%s %s', table.concat(data, ' + '), event['link_' .. key:lower:gsub(' ', '_')] or key, key) end

count[header] = table.concat(rewards, ' ') end

if event.class then event.rewards = count[event.class] else if #count == 1 then event.rewards = count[count[1]] else for idx, header in ipairs(count) do                       count[idx] = string.format('|-|%s=%s', header, count[header]) end event.rewards = frame:extensionTag{name = 'tabber', content = table.concat(count)} end end end end -- format duration if present if event.duration then event.duration = formatDuration(parseDate(event.duration)) end

return frame:expandTemplate{title = 'Infobox Event/Internal', args = event} end

function module.table(frame, args) frame = frame or mw.getCurrentFrame

local args = args or frame:getParent.args local params = {}

for k, v in ipairs(args) do       params[k] = v    end

local length = #params local columns = tonumber(args.cols) local div = length / columns local dfloor = math.floor(div) local dceil = math.ceil(div)

if dfloor ~= dceil then error(string.format('Uneven partition, found %d entries, expected %d or %d', length, dfloor * columns, dceil * columns)) end

local count = {} local output = {} local trim = mw.text.trim local expand = {title = 'DataTable/Row'}

for i = columns + 1, length, columns do       local args = {type1 = 'header', unpack(params, i, i + columns - 1)}

for k, v in ipairs(args) do           v = trim(v)

if v ~= '-' then count[k] = (count[k] or 0) + (tonumber(v) or tonumber(v:match('%d+')) or 1) end end

expand.args = args table.insert(output, frame:expandTemplate(expand)) end

expand.args = {['type'] = 'header', 'Total', unpack(count, 2)} table.insert(output, frame:expandTemplate(expand))

return frame:expandTemplate{ title = 'DataTable', args = {style = 'table-layout:fixed;' .. (args.style or ''), content = table.concat(output), unpack(params, 1, columns)}, } end

function module.total(frame, args) frame = frame or mw.getCurrentFrame

local args = args or frame:getParent.args

if args.ignore then return end

local params = {}

for k, v in ipairs(args) do       params[k] = v    end

local length = #params local trim = mw.text.trim local columns = tonumber(args.cols) -- security checks are done in function table, hoping that noone saves bad tables ^^ local count = {}

for i = columns + 1, length, columns do       local args = {unpack(params, i, i + columns - 1)}

for k, v in ipairs(args) do           v = trim(v)

if v ~= '-' then count[k] = (count[k] or 0) + (tonumber(v) or tonumber(v:match('%d+')) or 1) end end end

local header = {unpack(params, 1, columns)}

for k, v in pairs(header) do       header[k] = trim(v) end

local quest_class = args.class or '' local quest_type = args.type or ''

for k, v in ipairs(count) do       count[k] = string.format('\t%s=%s=%s=%s', header[k], quest_class, quest_type, v)    end

table.remove(count, 1) -- first column doesn't matter

return table.concat(count) end

function module.list(frame, args) frame = frame or mw.getCurrentFrame

local args = args or frame.args local events = {}

for _, event in ipairs(loadData(frame)) do       local name = event.name local timestamp = -1

for _, entry in ipairs(event) do           if entry.interval then timestamp = 0 break end

local event_timestamp = tonumber(entry.timestamp)

timestamp = (timestamp > event_timestamp) and timestamp or event_timestamp end

events[event] = timestamp end

local dates_keys = {} local dates = setmetatable({}, {       __index = function(self, key)            local data = {}

rawset(dates_keys, #dates_keys + 1, key) rawset(self, key, data)

return data end, })

for k, v in pairs(events) do       table.insert(dates[math.floor(v / 10000)], k)    end

table.sort(dates_keys)

local output = {string.format(' Event List (%s) ', args.platform or 'Browser')}

function comp(a, b)       return a.name < b.name end

local header = setmetatable({       [-1] = 'Miscellaneous',        [0] = 'Recurring',    }, {        __index = function(self, key)            local str = tostring(key)

rawset(self, key, str)

return str end, })

for _, v in pairs(dates_keys) do		if header[v] == 'Miscellaneous' then table.insert(output, string.format(' %s ', header[v])) -- Originally all headers was like this, without links else if header[v] == 'Recurring' then table.insert(output, string.format(' %s ', header[v])) else table.insert(output, string.format(' %s ', header[v], header[v])) end end local data = dates[v]

table.sort(data, comp)

for k, v in ipairs(data) do           local class = v.class local name = v.name

if class then data[k] = string.format('%s (%s)', name, class, name, class) else data[k] = string.format('%s', name, name) end end

table.insert(output, table.concat(data, ' ')) end

return table.concat(output) end

return module