Module:AgesEras: Difference between revisions

From NPOWiki
Jump to navigation Jump to search
No edit summary
(various fixes and improvements)
Line 2: Line 2:
local list = mw.loadData('Module:AgesEras/list')
local list = mw.loadData('Module:AgesEras/list')
local time = mw.language.getContentLanguage()
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: 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 due to alphabetical sorting, but this could be changed in icon
--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
--caution: pairs/ipairs and direct access are about the only things that will work on loadData tables


local function icon(kind, data, game, matchNum, gameNum)
local function icon(kind, data, game, gameNum)
if not data[1] or not data[2] or not data[3] or data[4] then
--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">' ..  
return '<span class="error">' ..  
'invalid ' .. game .. ' ' .. kind .. ' data in [[Module:AgesEras/list]]' ..
'invalid ' .. mw.ustring.upper(game) .. ' ' .. kind .. ' data in [[Module:AgesEras/list]]' ..
'</span>'
'</span>'
end
end
local name = data[2]
local desc = data[3]
game = mw.ustring.upper(game)
game = mw.ustring.upper(game)
local prefix
local gameFull = mw.getCurrentFrame():expandTemplate({ title='abbrgame', args={ game, 'full' } })
local name = item[2]
local desc = item[3]
local prefix, link
if kind == 'game' then
if kind == 'game' then
name = 'NPO'
name = 'NPO'
prefix = '!' .. kind
prefix = '!' .. kind
kind = 'era'
kind = 'era'
link = 'New Pacific Order (' .. gameFull .. ')'
else
else
prefix = kind
prefix = kind
link = ucfirst(kind) .. 's (' .. 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 mw.getCurrentFrame():extensionTag('indicator', indicatorText, { name = indicatorName })
end
end
--[=[ Use this instead for console debugging:
return '<indicator name="' .. gameNum .. '-' .. mw.ustring.lower(game) .. '-' .. prefix .. '-' .. matchNum .. '-' .. mw.ustring.lower(name) .. '">' ..
'[' .. '[' .. 'File:' .. kind .. ' icon ' .. game .. ' ' .. name .. '.png|30px|' .. desc .. ']]' ..
'</indicator>'
]=]
return mw.getCurrentFrame():extensionTag(
'indicator',
'[' .. '[' .. 'File:' .. kind .. ' icon ' .. game .. ' ' .. name .. '.png|30px|' .. desc .. ']]',
{ name = gameNum .. '-' .. mw.ustring.lower(game) .. '-' .. prefix .. '-' .. matchNum .. '-' .. mw.ustring.lower(name) }
)
end
end


--Returns { { start, end }, ... }
--Returns { { start, end } or { error text }, ... }
local function argToTable(arg)
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
if not arg or mw.text.trim(arg) == '' then return {} end
local parsed = {}
local parsed = {}
Line 45: Line 58:
elseif not mw.ustring.find(arg, '-', 1, true) and #mw.text.split(arg, ',', true) == 2 then
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
--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 = '&'
rangeSep = '&'
end
end
Line 84: Line 100:
end
end


--Returns { { age/era index, age/era object } or { 'error', error text }, ... }
local function findMatches(itemList, dates)
local function findMatches(itemList, dates)
if not itemList or not itemList[1] then return {} end
if not itemList or not itemList[1] then return {} end
Line 92: Line 109:
local finish = dates[2]
local finish = dates[2]
if finish == 'present' then finish = '' end
if finish == 'present' then finish = '' end
--formatDate works same as #time, U is Unix time
local isValid
start = time:formatDate('U', start)
finish = time:formatDate('U', finish)
--formatDate works same as #time, U is Unix time. Have to pass instance to pcall.
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
local now = 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
local itemStart, itemFinish, nextItem
for i, item in ipairs(itemList) do
for i, item in ipairs(itemList) do
itemStart = time:formatDate('U', item[1])
itemStart = time:formatDate('U', item[1])
Line 108: Line 137:
--Note: this won't count being present for only the first or last day
--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
if (start <= itemStart and finish > itemStart) or (start > itemStart and start < itemFinish) then
table.insert(matches, item)
table.insert(matches, { i, item })
end
end
end
end
Line 126: Line 155:
else
else
if i == 1 and gameList.game then
if i == 1 and gameList.game then
code = code .. icon('game', gameList.game, game, i, num)
code = code .. icon('game', { 1, gameList.game }, game, num)
end
end
for j, ageMatch in ipairs(findMatches(gameList.ages, range)) do
for j, ageMatch in ipairs(findMatches(gameList.ages, range)) do
code = code .. icon('age', ageMatch, game, j, num)
if ageMatch[1] == 'error' then
code = code .. '<span class="error">' .. ageMatch[2] .. '</span>'
else
code = code .. icon('age', ageMatch, game, num)
end
end
end
for k, eraMatch in ipairs(findMatches(gameList.eras, range)) do
for k, eraMatch in ipairs(findMatches(gameList.eras, range)) do
code = code .. icon('era', eraMatch, game, k, num)
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
end
Line 145: Line 182:
function p.call(args)
function p.call(args)
local code = ''
local code = ''
--Should be in order of branch establishment, this will determine display order.
local gameOrder = list.gameorder
local games = {
argToTable(args.ns),
argToTable(args.cn),
}
--This is a map to /list keys, must be in same order as above.
local gameCodes = { 'ns', 'cn', }
for i, gameTable in ipairs(games) do
for i, game in ipairs(gameOrder) do
code = code .. generateCode(gameTable, gameCodes[i], i)
code = code .. generateCode(argToTable(args[game]), game, i)
end
end
return code
return code
end
function p.test(args)
testMode = true
return p.call(args)
end
end


return p
return p

Revision as of 01:28, 21 August 2020

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)
	--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 = mw.getCurrentFrame():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 = ucfirst(kind) .. 's (' .. 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 mw.getCurrentFrame():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.
	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
	local now = 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 = time:formatDate('U', item[1])
		nextItem = itemList[i + 1]
		if nextItem then
			nextItem = nextItem[1]
		else
			nextItem = ''
		end
		itemFinish = 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
		code = code .. generateCode(argToTable(args[game]), game, i)
	end
	
	return code
end

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

return p