Module:AgesEras

From NPOWiki
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

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

local p = {}
local list = mw.loadData('Module:AgesEras/list')
local lang = mw.language.getContentLanguage()
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 = 'a' .. 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:' ..
		lang: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(lang.formatDate, lang, 'U', start)
	if not isValid then
		return { { 'error', 'invalid date: ' .. dates[1] } }
	end
	isValid, finish = pcall(lang.formatDate, lang, 'U', finish)
	if not isValid then
		return { { 'error', 'invalid date: ' .. dates[2] } }
	end
	start = tonumber(start)
	finish = tonumber(finish)
	local now = tonumber(lang: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(lang:formatDate('U', item[1]))
		nextItem = itemList[i + 1]
		if nextItem then
			nextItem = nextItem[1]
		else
			nextItem = ''
		end
		itemFinish = tonumber(lang: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