Module:AgesEras

From NPOWiki
Jump to navigation Jump to search

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

local p = {}
local list = mw.loadData('Module:AgesEras/list')
local time = mw.language.getContentLanguage()
local ucfirst = require('Module:ucfirst')
local testMode = false
--arg names must match gameorder keys in /list module
--note: if multiple ranges match the same age/era, the tag will duplicate, but will have the same name so won't be shown
--note: ages come before eras intentionally due to alphabetical sorting, but this could be changed in icon()
--caution: pairs/ipairs and direct access are about the only things that will work on loadData tables

local function icon(kind, data, game, gameNum)
	local frame = mw.getCurrentFrame()
	--Pad to two digits
	gameNum = mw.ustring.format('%02d', gameNum)
	local index = mw.ustring.format('%02d', data[1])
	local item = data[2]
	if not item[1] or not item[2] or not item[3] or item[4] then
		return '<span class="error">' .. 
			'invalid ' .. mw.ustring.upper(game) .. ' ' .. kind .. ' data in [[Module:AgesEras/list]]' ..
			'</span>'
	end
	game = mw.ustring.upper(game)
	local gameFull = frame:expandTemplate({ title='abbrgame', args={ game, 'full' } })
	local name = item[2]
	local desc = item[3]
	local prefix, link
	if kind == 'game' then
		name = 'NPO'
		prefix = '!' .. kind
		kind = 'era'
		link = 'New Pacific Order (' .. gameFull .. ')'
	else
		prefix = kind
		link = 'History of the New Pacific Order in ' .. gameFull .. '#' .. desc
	end
	
	local indicatorName = gameNum .. '-' .. mw.ustring.lower(game) .. '-' .. prefix .. '-' .. index .. '-' .. mw.ustring.lower(name)
	local indicatorText = '[' .. '[' .. 'File:' ..
		ucfirst(kind) .. ' icon ' .. game .. ' ' .. name ..
		'.png|30px|' .. desc .. '|link=' .. link ..
		']]'
	
	if testMode then
		return '<indicator name="' .. indicatorName .. '">' .. indicatorText .. '</indicator>'
	else
		return frame:extensionTag('indicator', indicatorText, { name = indicatorName })
	end
end

--Returns { { start, end } or { error text }, ... }
local function argToTable(arg)
	--This attempts to support most ways of writing ranges of dates, as long as they're consistent.
	if not arg or mw.text.trim(arg) == '' then return {} end
	local parsed = {}
	arg = mw.ustring.lower(arg)
	local rangeSep = ','
	if mw.ustring.find(arg, ';', 1, true) then
		rangeSep = ';'
	elseif not mw.ustring.find(arg, '-', 1, true) and #mw.text.split(arg, ',', true) == 2 then
		--This looks like a single date, don't want to split on comma
		rangeSep = '&'
	elseif mw.ustring.find(arg, ',', 1, true) and (mw.ustring.find(arg, ' - ', 1, true) or mw.ustring.find(arg, ' to ', 1, true)) then
		--Looks like potentially one range with commas in dates
		rangeSep = '&'
	end
	dateSep = ' - '
	if mw.ustring.find(arg, ' to ', 1, true) then
		dateSep = ' to '
	end
	mw.ustring.gsub(arg, ' until ', dateSep)
	
	local ranges = mw.text.split(arg, rangeSep, true)
	local dates
	for i, range in ipairs(ranges) do
		dates = mw.text.split(mw.text.trim(range), dateSep, true)
		if #dates == 1 then
			table.insert(dates, 'present')
		elseif #dates > 2 then
			dates = { 'invalid date range: ' .. range }
		else
			dates[1] = mw.text.trim(dates[1])
			dates[2] = mw.text.trim(dates[2])
			if dates[1] == '' then dates[1] = 'unknown' end
			if dates[2] == '' then dates[2] = 'unknown' end
			if dates[1] == 'present' then dates[1] = 'unknown' end
			
			if dates[1] == 'unknown' then
				dates[1] = dates[2]
			end
			if dates[2] == 'unknown' then
				if dates[1] == 'unknown' then
					dates = { 'invalid date range: ' .. range }
				else
					dates[2] = dates[1]
				end
			end
		end
		table.insert(parsed, dates)
	end
	return parsed
end

--Returns { { age/era index, age/era object } or { 'error', error text }, ... }
local function findMatches(itemList, dates)
	if not itemList or not itemList[1] then return {} end
	local matches = {}
	local start = dates[1]
	--Note: start can be present if it was unknown and finish is present
	if start == 'present' then start = '' end
	local finish = dates[2]
	if finish == 'present' then finish = '' end
	local isValid
	
	--formatDate works same as #time, U is Unix time. Have to pass instance to pcall.
	--Passing empty string gets current time.
	isValid, start = pcall(time.formatDate, time, 'U', start)
	if not isValid then
		return { { 'error', 'invalid date: ' .. dates[1] } }
	end
	isValid, finish = pcall(time.formatDate, time, 'U', finish)
	if not isValid then
		return { { 'error', 'invalid date: ' .. dates[2] } }
	end
	start = tonumber(start)
	finish = tonumber(finish)
	local now = tonumber(time:formatDate('U'))
	if start > finish or start > now or finish > now then
		return { { 'error', 'invalid date range: ' .. dates[1] .. ' to ' .. dates[2] } }
	end
	
	local itemStart, itemFinish, nextItem
	for i, item in ipairs(itemList) do
		itemStart = tonumber(time:formatDate('U', item[1]))
		nextItem = itemList[i + 1]
		if nextItem then
			nextItem = nextItem[1]
		else
			nextItem = ''
		end
		itemFinish = tonumber(time:formatDate('U', nextItem))
		--Note: this won't count being present for only the first or last day
		if (start <= itemStart and finish > itemStart) or (start > itemStart and start < itemFinish) then
			table.insert(matches, { i, item })
		end
	end
	
	return matches
end

local function generateCode(dates, game, num)
	if #dates == 0 then return '' end
	local code = ''
	local gameList = list[game]
	if not gameList then return '' end
	
	for i, range in ipairs(dates) do
		if #range == 1 then
			code = code .. '<span class="error">' .. range[1] .. '</span>'
		else
			if i == 1 and gameList.game then
				code = code .. icon('game', { 1, gameList.game }, game, num)
			end
			for j, ageMatch in ipairs(findMatches(gameList.ages, range)) do
				if ageMatch[1] == 'error' then
					code = code .. '<span class="error">' .. ageMatch[2] .. '</span>'
				else
					code = code .. icon('age', ageMatch, game, num)
				end
			end
			for k, eraMatch in ipairs(findMatches(gameList.eras, range)) do
				if eraMatch[1] == 'error' then
					code = code .. '<span class="error">' .. eraMatch[2] .. '</span>'
				else
					code = code .. icon('era', eraMatch, game, num)
				end
			end
		end
	end
	return code
end

function p.main(frame)
	return p.call(frame:getParent().args)
end

function p.call(args)
	local code = ''
	local gameOrder = list.gameorder
	
	for i, game in ipairs(gameOrder) do
		if args[game] then
			code = code .. generateCode(argToTable(args[game]), game, i)
		end
	end
	
	return code
end

function p.test(args)
	testMode = true
	return p.call(args)
end

return p