Module:Team appearances list
-- This module implements [[Template:Team appearances list]].
 
 local p = {}
 
 local data_competitions
 local function load_data(frame)
 	-- Load data module (or its sandbox) and set variables from its exported data.
 	if not data_competitions then
 		frame = frame or mw.getCurrentFrame()
 		local sandbox = frame:getTitle():find('sandbox', 1, true) and '/sandbox' or ''
 		local datamod = mw.loadData('Module:Team appearances list/data' .. sandbox)
 		data_competitions = datamod.competitions
 	end
 end
 
 local function strip_to_nil(text)
 	-- If text is a string, return its trimmed content, or nil if empty.
 	-- Otherwise return text (which may, for example, be nil).
 	if type(text) == 'string' then
 		text = text:match('(%S.-)%s*$')
 	end
 	return text
 end
 
 local function make_options(args)
 	-- Return table of options from validated args or throw error.
 	local options = {}
 	local function valid_integer(name, min, max, is_optional)
 		local arg = args[name]
 		if arg == nil or arg == '' then
 			if is_optional then
 				return nil
 			end
 			error('Parameter ' .. name .. ' is missing')
 		end
 		arg = tonumber(arg)
 		if type(arg) ~= 'number' then
 			error('Parameter ' .. name .. ' is not a number')
 		end
 		if math.floor(arg) ~= arg then
 			error('Parameter ' .. name .. ' is not an integer')
 		end
 		if not (min &lt;= arg and arg &lt;= max) then
 			error('Parameter ' .. name .. ' is not valid')
 		end
 		return arg
 	end
 	local function valid_text(name)
 		local arg = args[name]
 		if arg == nil or arg == '' then
 			error('Parameter ' .. name .. ' is missing')
 		end
 		if type(arg) ~= 'string' then
 			error('Parameter ' .. name .. ' is not a string')
 		end
 		return arg
 	end
 	options.competition = valid_text('competition')
 	options.team = valid_text('team')
 	options.competitions = data_competitions[options.competition]
 	local begin_optional
 	if options.competitions then
 		begin_optional = true
 	else
 		options.interval = valid_integer('interval', 1, 30)
 	end
 	options.begin_year = valid_integer('begin_year', 1800, 2100, begin_optional)
 	options.end_year = valid_integer('end_year', 1800, 2100, true)
 	if options.begin_year and options.end_year then
 		if options.begin_year &gt; options.end_year then
 			error('Parameter end_year must not be before begin_year')
 		end
 	end
 	options.disqualified_year = valid_integer('disqualified_year', 1800, 2100, true)
 	return options
 end
 
 local function extract_range(text)
 	-- Return first (if text is a single year), or first, last if a range.
 	-- The returned values are numbers.
 	-- Return nothing if text is invalid.
 	local year = text:match('^(%d+)$')
 	if year then
 		if #year == 4 then
 			return tonumber(year)
 		end
 		return
 	end
 	local first, dash, last = text:match('^(%d+)(%D+)(%d+)$')
 	if not (first and #first == 4) then
 		return
 	end
 	dash = strip_to_nil(dash)
 	if not (dash == '-' or dash == '–') then
 		return
 	end
 	if #last ~= 4 then
 		if #last == 2 then
 			last = first:sub(1, 2) .. last
 		else
 			return
 		end
 	end
 	first = tonumber(first)
 	last = tonumber(last)
 	if first &lt; last then
 		return first, last
 	elseif first == last then
 		return first
 	end
 end
 
 local function competition_absences(data)
 	-- Return two tables with absent years and absent year ranges.
 	-- Parameter data is an array of strings from template parameters, or
 	-- numbers or strings from built-in data.
 	-- Parameters that are blank or not numbers or strings are ignored.
 	local absent_years, absent_ranges = {}, {}
 	for _, item in ipairs(data) do
 		if type(item) == 'number' then
 			absent_years[item] = true
 		else
 			item = strip_to_nil(item)
 			if type(item) == 'string' then
 				local first, last = extract_range(item)
 				if not first then
 					error('Year ' .. item .. ' is not valid')
 				end
 				if last then
 					table.insert(absent_ranges, {first, last})
 				else
 					absent_years[first] = true
 				end
 			end
 		end
 	end
 	return absent_years, absent_ranges
 end
 
 local function competition_information(args)
 	-- Return four tables with competition and team information:
 	-- * List of competition years that the team attended or could have attended.
 	-- * Table of disqualified years (the team was absent, but there is an
 	--   article regarding the absent year).
 	-- * Table of absent years (when the team did not attend).
 	-- * List of pairs of years (absent for each year in range, inclusive).
 	local options = make_options(args)
 	local absences
 	local comp_years = {}
 	local begin_year = options.begin_year
 	local end_year = options.end_year
 	local competitions = options.competitions
 	if competitions then
 		absences = competitions[options.team]
 		begin_year = begin_year or (absences and absences.begin_year) or 0
 		end_year = end_year or (absences and absences.end_year) or 9999
 		for _, y in ipairs(competitions) do
 			if y &gt; end_year then
 				break
 			elseif y &gt;= begin_year then
 				table.insert(comp_years, y)
 			end
 		end
 	else
 		end_year = end_year or (os.date('!*t').year + options.interval)
 		for y = begin_year, end_year, options.interval do
 			table.insert(comp_years, y)
 		end
 	end
 	local disqualified_years = {}
 	if options.disqualified_year then
 		-- Input currently only allows entry of a single disqualified year.
 		-- However processing works for any number of such years.
 		disqualified_years[options.disqualified_year] = true
 	end
 	return comp_years, disqualified_years, competition_absences(absences or args)
 end
 
 function p._main(args)
 	load_data()  -- in case this function is called by another module
 	local hlist = require('Module:List').horizontal
 	local competitions, disqualified_years, absent_years, absent_ranges = competition_information(args)
 	local current_year = os.date('!*t').year
 	local function is_absent(y)
 		if absent_years[y] then
 			return true
 		end
 		for _, range in ipairs(absent_ranges) do
 			if range[1] &lt;= y and y &lt;= range[2] then
 				return true
 			end
 		end
 		return false
 	end
 	local appearances = {}
 	local absent_first, absent_last
 	for i = 1, #competitions + 1 do  -- +1 to handle any trailing absences
 		local y = competitions[i]
 		if y and is_absent(y) then
 			if absent_first then
 				absent_last = y
 			else
 				absent_first = y
 			end
 		else
 			if absent_first then
 				table.insert(appearances,
 					'&lt;span style=&quot;color:gray&quot;&gt;' ..
 					(absent_last and (absent_first .. '–' .. absent_last) or absent_first) ..
 					'&lt;/span&gt;')
 				absent_first, absent_last = nil, nil
 			end
 			if y then
 				local display = tostring(y)
 				if y &gt; current_year then
 					display = '&lt;i&gt;' .. display .. '&lt;/i&gt;'
 				end
 				if disqualified_years[y] then
 					display = '&lt;del&gt;' .. display .. '&lt;/del&gt;'
 				end
 				table.insert(appearances, string.format(
 					'[[%d %s விளையாட்டுகளில் %s|%s]]',
 					y, args.competition, args.team, display
 				))
 			end
 		end
 	end
 	return hlist(appearances)
 end
 
 function p.main(frame)
 	load_data(frame)
 	return p._main(frame:getParent().args)
 end