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

require('strict')
local p = {}
local getArgs = require('Module:Arguments').getArgs

-- Units accepted by {{convert}} that come in groups (e.g., "5 ft 6 in")
local multiple = 
{'mich', 'michlk', 'michainlk', 'miyd', 'miydftin', 'mift', 'ydftin', 'ydft',
'ftin', 'footin', 'handin', 'lboz', 'stlb', 'stlboz', 'stlb'}

-- Convert unit list to hash
local mult_table = {}
for _, v in ipairs(multiple) do
	mult_table[v] = true
end

-- Function to pull out values and units from numeric args
-- Returns:
--   values:  list of numeric values, or "false" if no numeric argument is given
--   units: list of units (str)
--   value: if there is a last numeric value unpaired with a unit, it becomes the precision
--   anyValue: whether there is a non-false value in the values list
local function parseValuesUnits(args)
	local values = {}
	local units = {}
	local indx = 1
	local value = nil
	local anyValue = false
	-- loop through numeric arguments in pairs
	while args[indx] or args[indx+1] do
		value = args[indx]
		anyValue = anyValue or value
		-- if there is a unit, save in output lists
		if args[indx+1] then
			table.insert(values, value or false)
			table.insert(units, args[indx+1])
			value = nil
		end
		indx = indx+2
	end
	return values, units, value, anyValue
end

-- Function to identify multiple units and rewrite them as new input or output groups
-- Args:
--   values, units: numeric values and units, as lists with same length
-- Returns:
--   newValues, newUnits: same lists rewritten
local function parseMultiples(values, units)
	local newValues = {}
	local newUnits = {}
	local i = 1
	-- we will search for multiples with up to 4 entries (depending on length)
	local maxMultiple = math.min(4,#units-1)
	local valueFound = false -- flag to suppress second (and later) input values
	--- Hack for handling "stone": check if only value supplied is "lb"
	local onlyPounds = true
	for i = 1, #units do
		if values[i] and units[i] ~= 'lb' then
			onlyPounds = false
			break
		end
	end
	-- sweep through units
	while i <= #units do
		-- determine index of last possible unit that could contain a multiple
		local last_unit = math.min(i+maxMultiple-1,#units)
		local multipleFound = false
		-- try from longest multiple down to double multiple (prefer longest ones)
		for j = last_unit, i+1, -1 do
			local key = table.concat({unpack(units,i,j)}, '')
			if mult_table[key] then
				-- we found a multiple unit
				multipleFound = true
				-- Hack for "stone": add either 'lb' or multiple unit string to output units
				--    depending on whether 'lb' was the only unit string with a value
				if mw.ustring.sub(key,1,2) == 'st' then
					table.insert(newValues, false)
					table.insert(newUnits, onlyPounds and key or 'lb')
				end
				-- if there are any value in the span of the multiple,
				-- then the multiple is an input
				-- assume all missing values after the first are zero
				local firstValueFound = false
				for k = i, j do
					firstValueFound = not valueFound and (firstValueFound or values[k])
					if firstValueFound then
						table.insert(newValues, values[k] or 0)
						table.insert(newUnits, units[k])
					end
				end
				valueFound = valueFound or firstValueFound
				-- if no values in the span of the multiple,
				-- then the multiple is an output. Insert combined string as output unit
				if not firstValueFound then
					table.insert(newValues, false)
					table.insert(newUnits, key)
				end
				i = j+1
				break
			end
		end
		--- If no multiple unit was found, insert value[i] and unit[i] into rewritten lists
		if not multipleFound then
			if valueFound then
				table.insert(newValues, false) -- skip writing value if it is a duplicate
			else
				table.insert(newValues,values[i])
				valueFound = values[i]
			end
			table.insert(newUnits, units[i])
			i = i+1
		end
	end
	return newValues, newUnits			
end

-- Implement {{convinfobox}}
function p._convert(args)
	-- find all values and units in numeric args (and the precision, if it exists)
	local values, units, precision, anyValue = parseValuesUnits(args)
	-- bail if no values at all
	if not anyValue then
		return nil
	end
	-- rewrite values and units if multiple units are found
	values, units = parseMultiples(values, units)
	-- sort input and outputs into different buckets
	local input_values = {}
	local input_units = {}
	local output_units = {}
	for i = 1, #units do
		if values[i] then
			table.insert(input_values, values[i])
			table.insert(input_units, units[i])
		else
			table.insert(output_units, units[i])
		end
	end
	-- bail if nothing to convert
	if #input_values == 0 or #output_units == 0 then
		return nil
	end
	-- assemble argument list to {{convert}}
	local innerArgs = {}
	-- First, pass all input unit(s)
	for i, v in ipairs(input_values) do
		table.insert(innerArgs,v)
		table.insert(innerArgs,input_units[i])
	end
	-- Then the output unit(s) [concatenated as single argument]
	table.insert(innerArgs,table.concat(output_units,"+"))
	if precision then
		table.insert(innerArgs,precision) -- last non-nil value contains precision
	end
	-- now handle all non-numeric arguments, passing to {{convert}}
	innerArgs.abbr = 'on'  -- abbr=on by default
	for k, v in pairs(args) do
		if not tonumber(k) then
			innerArgs[k] = v
		end
	end
	-- Call {{convert}} with innerArgs
	local frame = mw.getCurrentFrame()
	return frame:expandTemplate{title='Convert', args=innerArgs}
end

function p.convert(frame)
	local args = getArgs(frame)
	return p._convert(args) or ""
end

return p