Module:Convert

From Wikipedia
Jump to: navigation, search

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

-- Convert a value from one unit of measurement to another.
-- Example: {{convert|123|lb|kg}} --> 123 pounds (56 kg)
 
local MINUS = '−'  -- Unicode U+2212 MINUS SIGN (UTF-8: e2 88 92)
local format = string.format
local floor = math.floor
 
-- Configuration options to keep magic values in one location.
local numdot, numsep, maxsigfig, lang
local limit_abuse = 30  -- avoid using very large precision/sigfig (need >20 to avoid values like 1e-22 rounding to 0)
-- Following specify the conversion data which is defined in another module
-- because it is too large to be conveniently included here.
-- To allow easy comparison between "require" and "loadData", a config option
-- can be set to control which is used.
local SIprefixes, units, default_exceptions, link_exceptions
 
local function set_config(frame)
	-- Set configuration options from template #invoke or defaults.
	local args = frame.args
	numdot = args.numdot or '.'       -- decimal mark before fractional digits
	numsep = args.numsep or ','       -- thousands separator for numbers (',', '.', '')
	maxsigfig = args.maxsigfig or 14  -- maximum number of significant figures
	lang = args.lang or 'en'          -- language code for messages
	if maxsigfig > limit_abuse then
		maxsigfig = limit_abuse
	end
	-- Scribunto sets the global variable 'mw'.
	-- A testing program can set the global variable 'is_test_run'.
	local convertdata
	local data_module = is_test_run and "convertdata" or "Module:Convertdata"
	if args.use_require then
		convertdata = require(data_module)
	else
		convertdata = mw.loadData(data_module)
	end
	SIprefixes = convertdata.SIprefixes
	units = convertdata.units
	default_exceptions = convertdata.default_exceptions
	link_exceptions = convertdata.link_exceptions
end
 
local function strip(text)
	-- If text is a string, return its content with no leading/trailing
	-- whitespace. Otherwise return nil (a nil argument gives a nil result).
	if type(text) == 'string' then
		return text:match("^%s*(.-)%s*$")
	end
end
 
------------------------------------------------------------------------
-- BEGIN: Messages that may be displayed by Module:Convert.
-- LATER: Perhaps move to Module:Convertmessages to simplify this module.
local all_categories = {
	['en'] = {
		general = '[[Category:Convert error]]',
		unknown = '[[Category:Convert unknown unit]]',
		mismatch = '[[Category:Convert dimension mismatch]]',
	},
}
 
local all_messages = {
	-- All output messages.
	['en'] = {
		-- The prefix is inserted before each message.
		-- LATER: Link should be to 'Template', not 'Module'.
		--        Put the correct name when have a template established.
		cvt_prefix = '[[Module talk:Convert|Conversion error]]:',
		-- Messages; each is a numbered table: { 'error text', 'category key' }.
		cvt_bad_default = { 'Unit "%s" has an invalid default', 'unknown' },
		cvt_bad_num = { 'Value "%s" must be a number', 'general' },
		cvt_bad_num2 = { 'Second value "%s" must be a number', 'general' },
		cvt_bad_prec = { 'Parameter precision "%s" must be an integer', 'general' },
		cvt_bad_sigfig = { 'Parameter sigfig "%s" must be an integer', 'general' },
		cvt_bad_unit = { 'Unit "%s" is invalid here', 'unknown' },
		cvt_mismatch = { 'Cannot convert "%s" to "%s"', 'mismatch' },
		cvt_no_default = { 'Unit "%s" has no default output unit', 'unknown' },
		cvt_no_num = { 'Need value', 'general' },
		cvt_no_num2 = { 'Need second value', 'general' },
		cvt_no_unit = { 'Need name of unit', 'unknown' },
		cvt_should_be = { 'Use "%s" (not "%s") as the unit code', 'general' },
		cvt_sigfig_pos = { 'sigfig "%s" must be positive', 'general' },
		cvt_unknown = { 'Unit "%s" is not known', 'unknown' },
	},
}
 
local messages = {}  -- simulating how it would be if in another module
 
function messages.message(msg, lang)
	-- Return wikitext for an error message, including category if specified
	-- for the message type.
	-- msg = numbered table:
	--    msg[1] = 'cvt_xxx' (string used as a key to get message info)
	--    msg[2] = 'parm1' (string to replace first %s if any in message)
	--    msg[3] = 'parm2' (string to replace second %s if any in message)
	--    msg[4] = 'parm3' (string to replace third %s if any in message)
	-- lang = 'en' (default), or other language code.
	lang = lang or 'en'
	local mlang = all_messages[lang]
	if mlang then
		local t = mlang[msg[1]]
		if t then
			-- t[1] = message text, t[2] = category
			local text = string.format(t[1] or 'Missing message',
				msg[2] or '?',
				msg[3] or '?',
				msg[4] or '?')
			local cat = all_categories[lang][t[2]] or ''
			local prefix = mlang['cvt_prefix'] or ''
			return '<span style="color:black; background-color:orange;">' ..
					prefix .. ' ' .. text .. cat .. '</span>'
		end
	end
	return 'Convert internal error: unknown message'
end
-- END: Messages that may be displayed by Module:Convert.
------------------------------------------------------------------------
 
local function shallow_copy(t)
	-- Return a shallow copy of t.
	-- Do not need the features and overhead of mw.clone() provided by Scribunto.
	local result = {}
	for k, v in pairs(t) do
		result[k] = v
	end
	return result
end
 
local unit_mt = {
	-- Metatable to get missing values for a unit that does not accept SI prefixes,
	-- or a for a unit that accepts prefixes but where no prefix was used.
	-- In the latter case, and before use, fields symbol, name1, name1_us
	-- must be set from _symbol, _name1, _name1_us respectively.
	__index = function (self, key)
		local value
		if key == 'name1' or key == 'sym_us' then
			value = self.symbol
		elseif key == 'name2' then
			value = self.name1 .. 's'
		elseif key == 'name1_us' then
			value = self.name1
			if not rawget(self, 'name2_us') then
				-- If name1_us is 'foot', do not make name2_us by appending 's'.
				self.name2_us = self.name2
			end
		elseif key == 'name2_us' then
			local raw1_us = rawget(self, 'name1_us')
			if raw1_us then
				value = raw1_us .. 's'
			else
				value = self.name2
			end
		elseif key == 'link' then
			value = self.name1
		else
			return nil  -- some fields like invert and multiple may be nil
		end
		rawset(self, key, value)
		return value
	end
}
 
local unit_prefixed_mt = {
	-- Metatable to get missing values for a unit that accepts SI prefixes,
	-- and where a prefix has been used.
	-- Before use, fields si_name, si_prefix must be defined.
	__index = function (self, key)
		local value
		if key == 'symbol' then
			value = self.si_prefix .. self._symbol
		elseif key == 'sym_us' then
			value = self.symbol  -- always the same as sym_us for prefixed units
		elseif key == 'name1' then
			local pos = rawget(self, 'prefix_position') or 1
			value = self._name1
			value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
		elseif key == 'name2' then
			value = self.name1 .. 's'
		elseif key == 'name1_us' then
			value = rawget(self, '_name1_us')
			if value then
				local pos = rawget(self, 'prefix_position') or 1
				value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
			else
				value = self.name1
			end
		elseif key == 'name2_us' then
			if rawget(self, '_name1_us') then
				value = self.name1_us .. 's'
			else
				value = self.name2
			end
		elseif key == 'link' then
			value = self.name1
		else
			return nil  -- some fields like invert and multiple may be nil
		end
		rawset(self, key, value)
		return value
	end
}
 
local function lookup(unitcode, sp, what)
	-- Return true, t where t is a copy of the unit's converter table,
	-- or return false, t where t is an error message table.
	-- Parameter 'sp' is 'us' for US spelling of prefixes, or nil.
	-- Parameter 'what' determines whether combination units are accepted:
	--   'no_combination'  : single unit only
	--   'any_combination' : single unit or combination or multiple
	--   'only_multiple'   : single unit or multiple only
	-- Parameter unitcode is a symbol (like 'g'), with an optional SI prefix (like 'kg').
	-- If, for example, 'kg' is in this table, that entry is used;
	-- otherwise the prefix ('k') is applied to the base unit ('g').
	-- If unitcode is a known combination code (and if allowed by what),
	-- a table of multiple unit tables is included in the result.
	-- For compatibility with the old template, underscores in unitcode are replaced
	-- with spaces so {{convert|350|board_feet}} --> 350 board feet (0.83 m³).
	if unitcode == nil or unitcode == '' then
		return false, { 'cvt_no_unit' }
	end
	unitcode = unitcode:gsub('_', ' ')
	local t = units[unitcode]
	if t ~= nil then
		if t.shouldbe then
			return false, { 'cvt_should_be', t.shouldbe, unitcode }
		end
		local target = t.target  -- nil, or unitcode is an alias for this target
		if target then
			local force_sp_us = t.sp_us
			local success, result = lookup(target, sp or (force_sp_us and 'us'), what)
			if not success then return false, result end
			result.sp_us = force_sp_us
			return true, result
		end
		local combo = t.combination  -- nil or a table of unitcodes
		if combo then
			local multiple = t.multiple
			if what == 'no_combination' or (what == 'only_multiple' and multiple == nil) then
				return false, { 'cvt_bad_unit', unitcode }
			end
			-- Recursively create a combination table containing the
			-- converter table of each unitcode.
			local success
			local comboresult = { utype = t.utype, multiple = multiple, combination = {} }
			local cvt = comboresult.combination
			for i, v in ipairs(combo) do
				success, cvt[i] = lookup(v, sp, multiple and 'no_combination' or 'only_multiple')
				if not success then return false, cvt[i] end
			end
			return true, comboresult
		end
		local result = shallow_copy(t)
		if result.prefixes then
			result.symbol = result._symbol
			result.name1 = result._name1
			result.name1_us = result._name1_us
		end
		return true, setmetatable(result, unit_mt)
	end
	for plen = 2, 1, -1 do
		-- Look for an SI prefix; should never occur with an alias.
		-- Check for longer prefix first ('dam' is decametre).
		-- Micro (µ) is two bytes in utf-8, so is found with plen = 2.
		local prefix = string.sub(unitcode, 1, plen)
		local si = SIprefixes[prefix]
		if si then
			local t = units[unitcode:sub(plen+1)]
			if t and t.prefixes then
				local result = shallow_copy(t)
				if (sp == 'us' or t.sp_us) and si.name_us then
					result.si_name = si.name_us
				else
					result.si_name = si.name
				end
				result.si_prefix = si.prefix or prefix
				result.scale = t.scale * 10 ^ (si.exponent * t.prefixes)
				return true, setmetatable(result, unit_prefixed_mt)
			end
		end
	end
	return false, { 'cvt_unknown', unitcode }
end
 
local function ntsh_complement(text)
	-- Return text (string of digits) after subtracting each digit from 9.
	local result = ''
	local first, last = 1, #text
	while first <= last do
		local lenblock = last + 1 - first
		if lenblock > 12 then
			lenblock = 12
		end
		local block = tonumber(text:sub(first, first + lenblock - 1))
		local nines = tonumber(string.rep('9', lenblock))
		local fmt = '%0' .. tostring(lenblock) .. '.0f'
		result = result .. format(fmt, nines - block)
		first = first + lenblock
	end
	return result
end
 
local function ntsh(n, debug)
	-- Return html text to be used for a hidden sort key so that
	-- the given number will be sorted in numeric order.
	-- If debug == 'yes', output is in a box (not hidden).
	-- This implements Template:Ntsh (number table sorting, hidden).
	local result, i, f, style
	if n >= 0 then
		if n > 1e16 then
			result = '~'
		else
			i, f = math.modf(n)
			f = floor(1e6 * f)
			result = format('&1%016.0f%06d', i, f)
		end
	else
		n = -n
		if n > 1e16 then
			result = '!'
		else
			i, f = math.modf(n)
			f = floor(1e6 * f)
			result = format('%016.0f%06d', i, f)
			result = '&0' .. ntsh_complement(result)
		end
	end
	if debug == 'yes' then
		style = 'border:1px solid'
	else
		style = 'display:none'
	end
	return '<span style="' .. style .. '">' .. result .. '</span>'
end
 
local function hyphenated(name)
	-- Return a hyphenated form of given name (for adjectival usage).
	-- This uses a simple and efficient procedure that works for most cases.
	-- Some units (if used) would require more, and can later think about
	-- adding a method to handle exceptions.
	-- The procedure is to replace each space with a hyphen, but
	-- not a space after ')' [for "(pre-1954&nbsp;US) nautical mile"], and
	-- not spaces immediately before '(' or in '(...)' [for cases like
	-- "British thermal unit (ISO)" and "Calorie (International Steam Table)"].
	local pos
	if name:sub(1, 1) == '(' then
		pos = name:find(')', 1, true)
		if pos ~= nil then
			return name:sub(1, pos+1) .. name:sub(pos+2):gsub(' ', '-')
		end
	elseif name:sub(-1, -1) == ')' then
		pos = name:find('(', 1, true)
		if pos ~= nil then
			return name:sub(1, pos-2):gsub(' ', '-') .. name:sub(pos-1)
		end
	end
	return name:gsub(' ', '-')
end
 
local function change_sign(text)
	-- Change sign of text for correct appearance because it is negated.
	if text:sub(1, 1) == '-' then
		return text:sub(2)
	end
	return '-' .. text
end
 
local function use_minus(text)
	-- Return text with Unicode minus instead of '-', if present.
	if text:sub(1, 1) == '-' then
		return MINUS .. text:sub(2)
	end
	return text
end
 
local function with_separator(text)
	-- Return text with thousand separators inserted.
	-- The given text is like '123' or '12345.6789' or '1.23e45'
	-- (e notation can only occur when processing an input value).
	-- The text has no sign (caller inserts that later, if necessary).
	-- Separator is inserted only in the integer part of the significand
	-- (not after numdot, and not after 'e' or 'E').
	-- Four-digit integer parts have a separator (like '1,234').
	if numsep == '' then
		return text
	end
	local last = text:match('()[' .. numdot .. 'eE]')  -- () returns position
	if last == nil then
		last = #text
	else
		last = last - 1  -- index of last character before dot/e/E
	end
	if last >= 4 then
		local groups = {}
		local first = last % 3
		if first > 0 then
			table.insert(groups, text:sub(1, first))
		end
		first = first + 1
		while first < last do
			table.insert(groups, text:sub(first, first+2))
			first = first + 3
		end
		return table.concat(groups, numsep) .. text:sub(last+1)
	end
	return text
end
 
-- Input values can use values like 1.23e12, but are never displayed
-- using exponent notation like 1.23×10¹².
-- Very small or very large output values use exponent notation.
-- Use format(fmtpower, significand, exponent) where each arg is a string.
local fmtpower = '%s<span style="margin-left:0.2em">×<span style="margin-left:0.1em">10</span></span><sup>%s</sup>'
 
local function with_exponent(show, exponent)
	-- Return wikitext to display the implied value in exponent notation.
	if #show > 1 then
		show = show:sub(1, 1) .. numdot .. show:sub(2)
	end
	return format(fmtpower, show, use_minus(tostring(exponent)))
end
 
local function make_sigfig(value, sigfig)
	-- Return show, exponent that are equivalent to the result of
	-- converting the number 'value' (where value >= 0) to a string,
	-- rounded to 'sigfig' significant figures.
	-- The returned items are:
	--   show: a string of digits; no sign and no dot;
	--         there is an implied dot before show.
	--   exponent: a number (an integer) to shift the implied dot.
	-- Resulting value = tonumber('.' .. show) * 10^exponent.
	-- Examples:
	--   make_sigfig(23.456, 3) returns '235', 2 (.235 * 10^2).
	--   make_sigfig(0.0023456, 3) returns '235', -2 (.235 * 10^-2).
	--   make_sigfig(0, 3) returns '000', 1 (.000 * 10^1).
	if sigfig <= 0 then
		sigfig = 1
	elseif sigfig > maxsigfig then
		sigfig = maxsigfig
	end
	if value == 0 then
		return string.rep('0', sigfig), 1
	end
	local exp, frac = math.modf(math.log10(value))
	if frac >= 0 then
		frac = frac - 1
		exp = exp + 1
	end
	local digits = format('%.0f', floor((10^(frac + sigfig)) + 0.5))
	if #digits > sigfig then
		-- Overflow (for sigfig=3: like 0.9999 rounding to "1000"; need "100").
		digits = digits:sub(1, sigfig)
		exp = exp + 1
	end
	assert(#digits == sigfig, 'Bug: rounded number has wrong length')
	return digits, exp
end
 
local function format_number(show, exponent, isnegative)
	-- Return t where t is a table with the results; fields:
	--   show = wikitext formatted to display implied value
	--   is_scientific = true if show uses scientific notation
	--   clean = unformatted show (possibly adjusted and with inserted numdot)
	--   sign = '' or MINUS
	--   exponent = exponent (possibly adjusted)
	-- The clean and exponent fields can be used to calculate the
	-- rounded absolute value, if needed.
	--
	-- The value implied by the arguments is found from:
	--   exponent is nil; and
	--   show is a string of digits (no sign), with an optional dot;
	--   show = '123.4' is value 123.4, '1234' is value 1234.0;
	-- or:
	--   exponent is an integer indicating where dot should be;
	--   show is a string of digits (no sign and no dot);
	--   there is an implied dot before show;
	--   show does not start with '0';
	--   show = '1234', exponent = 3 is value 0.1234*10^3 = 123.4.
	--
	-- The formatted result:
	-- * Includes a Unicode minus if isnegative.
	-- * Has numsep inserted where necessary.
	-- * Uses scientific notation for very small or large values.
	-- * Has no more than maxsigfig significant digits
	--   (same as old template and {{#expr}}).
	local sign = isnegative and MINUS or ''
	local maxlen = maxsigfig
	if exponent == nil then
		local integer, dot, fraction = show:match('^(%d*)([' .. numdot .. ']?)(.*)')
		if #integer >= 10 then
			show = integer .. fraction
			exponent = #integer
		elseif integer == '0' or integer == '' then
			local zeros, figs = fraction:match('^(0*)([^0]?.*)')
			if #figs == 0 then
				if #zeros > maxlen then
					show = '0' .. numdot .. zeros:sub(1, maxlen)
				end
			elseif #zeros >= 4 then
				show = figs
				exponent = -#zeros
			elseif #figs > maxlen then
				show = '0' .. numdot .. zeros .. figs:sub(1, maxlen)
			end
		else
			maxlen = maxlen + #dot
			if #show > maxlen then
				show = show:sub(1, maxlen)
			end
		end
	end
	if exponent ~= nil then
		if #show > maxlen then
			show = show:sub(1, maxlen)
		end
		if exponent > 10 or exponent <= -4 or (exponent == 10 and show ~= '1000000000') then
			-- Rounded value satisfies: value >= 1e9 or value < 1e-4 (1e9 = 0.1e10).
			return {
				clean = '.' .. show,
				exponent = exponent,
				sign = sign,
				show = sign .. with_exponent(show, exponent-1),
				is_scientific = true }
		end
		if exponent >= #show then
			show = show .. string.rep('0', exponent - #show)  -- result has no dot
		elseif exponent <= 0 then
			show = '0' .. numdot .. string.rep('0', -exponent) .. show
		else
			show = show:sub(1, exponent) .. numdot .. show:sub(exponent+1)
		end
	end
	if isnegative and show:match('^0.?0*$') then
		sign = ''  -- don't show minus if result is negative but rounds to zero
	end
	return {
		clean = show,
		sign = sign,
		show = sign .. with_separator(show) }
end
 
-- Fraction output format (like old template).
-- frac1: sign, numerator, denominator
-- frac2: wholenumber, sign, numerator, denominator
local frac1 = '<span style="white-space:nowrap">%s<sup>%s</sup>&frasl;<sub>%s</sub></span>'
local frac2 = '<span class="frac nowrap">%s<s style="display:none">%s</s><sup>%s</sup>&frasl;<sub>%s</sub></span>'
 
local function extract_fraction(text, negative)
	-- If text represents a fraction, return value, show where
	-- value is a number and show is a string.
	-- Otherwise, return nil.
	--
	-- In the following, '(3/8)' represents the wikitext required to
	-- display a fraction with numerator 3 and denominator 8.
	-- In the wikitext, Unicode minus is used for a negative value.
	--   text          value, show            value, show
	--                 if not negative       if negative
	--   3 / 8         0.375, '(3/8)'        -0.375, '−(3/8)'
	--   2 + 3 / 8     2.375, '2(3/8)'       -1.625, '−2(−3/8)'
	--   2 - 3 / 8     1.625, '2(−3/8)'      -2.375, '−2(3/8)'
	--   1 + 20/8      3.5  , '1/(20/8)'     1.5   , '−1/(−20/8)'
	--   1 - 20/8      -1.5., '1(−20/8)'     -3.5  , '−1(20/8)'
	-- Wherever an integer appears above, numbers like 1.25 or 12.5e-3
	-- (which may be negative) are also accepted (like old template).
	-- Template interprets '1.23e+2+12/24' as '123(12/24)' = 123.5!
	local lhs, negfrac, rhs, numstr, numerator, denstr, denominator, wholestr, whole, value
	lhs, denstr = text:match('^%s*([^/]-)%s*/%s*(.-)%s*$')
	denominator = tonumber(denstr)
	if denominator == nil then return nil end
	wholestr, negfrac, rhs = lhs:match('^%s*(.-[^eE])%s*([+-])%s*(.-)%s*$')
	if wholestr == nil or wholestr == '' then
		wholestr = nil
		whole = 0
		numstr = lhs
	else
		whole = tonumber(wholestr)
		if whole == nil then return nil end
		numstr = rhs
	end
	negfrac = (negfrac == '-')
	numerator = tonumber(numstr)
	if numerator == nil then return nil end
	if negative == negfrac or wholestr == nil then
		value = whole + numerator / denominator
	else
		value = whole - numerator / denominator
		numstr = change_sign(numstr)
	end
	if tostring(value):find('#', 1, true) then
		return nil  -- overflow or similar
	end
	numstr = use_minus(numstr)
	denstr = use_minus(denstr)
	local wikitext
	if wholestr then
		local sign = negative and MINUS or '+'
		if negative then
			wholestr = change_sign(wholestr)
		end
		wikitext = format(frac2, use_minus(wholestr), sign, numstr, denstr)
	else
		local sign = negative and MINUS or ''
		wikitext = format(frac1, sign, numstr, denstr)
	end
	return value, wikitext
end
 
local missing = { 'cvt_no_num', 'cvt_no_num2' }
local invalid = { 'cvt_bad_num', 'cvt_bad_num2' }
 
local function extract_number(args, index, which)
	-- Return true, info if can extract a number from the text in args[index],
	-- where info is a table with the result,
	-- or return false, t where t is an error message table.
	-- Parameter 'which' (1 or 2) selects which input value is being
	-- processed (to select the appropriate error message, if needed).
	-- Before processing, the input text is cleaned:
	-- * Any thousand separators (valid or not) are removed.
	-- * Any sign (and optional following whitespace) is replaced with
	--   '-' (if negative) or '' (otherwise).
	--   That replaces Unicode minus with '-'.
	-- If successful, the returned info table contains named fields:
	--   value    = a valid number
	--   singular = true if value is 1 (to use singular form of units)
	--            = false if value is -1 (like old template)
	--   clean    = cleaned text with any separators and sign removed
	--   show     = text formatted for output
	-- For show:
	-- * Thousand separators are inserted.
	-- * If negative, a Unicode minus is used; otherwise the sign
	--   is '+' (if the input text used '+'), or is ''.
	-- TODO Think about fact that the input value might be like 1.23e+123.
	-- Will the exponent break anything?
	local text = strip(args[index])
	if text == nil or text == '' then return false, { missing[which] } end
	local clean, sign
	if numsep == '' then
		clean = text
	else
		clean = text:gsub('[' .. numsep .. ']', '')  -- use '[.]' if numsep is '.'
	end
	-- Remove any sign character (assuming a number starts with '.' or a digit).
	sign, clean = clean:match('^%s*([^ .%d]*)%s*(.*)')
	if sign == nil or clean == nil then
		return false, { missing[which] }  -- should never occur
	end
	local propersign, negative
	if sign == MINUS or sign == '-' then
		propersign = MINUS
		negative = true
	elseif sign == '+' then
		propersign = '+'
		negative = false
	elseif sign == '' then
		propersign = ''
		negative = false
	else
		return false, { invalid[which], text }
	end
	local show, singular
	local value = tonumber(clean)
	if value == nil then
		value, show = extract_fraction(clean, negative)
		if value == nil then
			return false, { invalid[which], text }
		end
		singular = false  -- any fraction (even with value 1) is regarded as plural
	end
	if show == nil then
		singular = (value == 1 and not negative)
		show = propersign .. with_separator(clean)
	end
	if negative and (value ~= 0) then
		value = -value
	end
	return true, {
		value = value,
		singular = singular,
		clean = clean,
		show = show
	}
end
 
local function require_integer(text, invalid)
	-- Return true, n where n = integer equivalent to given text,
	-- or return false, t where t is an error message table.
	-- Input should be the text for a simple integer (no separators, no Unicode minus).
	-- Using regex avoids irritations with input like '-0.000001'.
	if text == nil then return false, { 'cvt_no_num' } end
	if string.match(text, '^-?%d+$') == nil then
		return false, { invalid, text }
	end
	return true, tonumber(text)
end
 
local function get_parms(pframe)
	-- If successful, return true, args, unit where
	--   args is a table of all arguments passed to the template
	--        converted to named arguments, and
	--   unit is the input unit table;
	-- or return false, t where t is an error message table.
	-- Some of the named args that are added here could be provided by the
	-- user of the template.
	-- MediaWiki removes leading and trailing whitespace from the values of
	-- named arguments. However, the values of numbered arguments include any
	-- whitespace entered in the template, and whitespace is used by some
	-- parameters (example: the numbered parameters associated with "disp=x").
	local range_types = {  -- text to separate input, output ranges; for 'x', depends on abbr
		['and']   = {' and ', '&nbsp;and '},
		['by']    = {' by ', '&nbsp;by '},
		['to']    = {' to ', '&nbsp;to '},
		['-']     = {'–', '–'},
		['to(-)'] = {'&nbsp;to ', '–'},
		['to-']   = {'&nbsp;to ', '–'},
		['x']     = {{ ['out'] = ' by '    , ['in'] = ' ×&nbsp;', ['off'] = ' by ', ['on'] = ' ×&nbsp;' },
					 { ['out'] = ' ×&nbsp;', ['in'] = ' by '    , ['off'] = ' by ', ['on'] = ' ×&nbsp;' }},
		['+/-']   = {'&nbsp;±&nbsp;', '&nbsp;±&nbsp;'},
	}
	local success, info1, info2
	local args = {}  -- arguments passed to template
	for k, v in pairs(pframe.args) do
		args[k] = v
	end
	success, info1 = extract_number(args, 1, 1)
	if not success then return false, info1 end
	local in_unit, precision
	local next = strip(args[2])
	local i = 3
	local range = range_types[next]
	if range == nil then
		in_unit = next
	else
		args.range = range
		args.is_range_x = (next == 'x')
		success, info2 = extract_number(args, 3, 2)
		if not success then return false, info2 end
		in_unit = strip(args[4])
		i = 5
	end
	local success, in_unit_table = lookup(in_unit, args.sp, 'no_combination')
	if not success then return false, in_unit_table end
	if args.test == 'msg' then
		-- Am testing the messages produced when no output unit is specified, and
		-- the input unit has a missing or invalid default.
		-- Set two units for testing that.
		-- LATER: Remove this code.
		if in_unit == 'chain' then
			in_unit_table.default = nil  -- no default
		elseif in_unit == 'rd' then
			in_unit_table.default  = "ft|X|m"  -- an invalid expression
		end
	end
	in_unit_table.valinfo = { info1, info2 }  -- info2 is nil if no range
	in_unit_table.inout = 'in'  -- this is an input unit
	next = strip(args[i])
	i = i + 1
	if tonumber(next) == nil then
		args.out_unit = next
		next = strip(args[i])
		if tonumber(next) ~= nil then
			i = i + 1
			precision = next
		end
	else
		precision = next
	end
	if args.adj == nil and args.sing ~= nil then
		args.adj = args.sing  -- sing (singular) is apparently an old equivalent of adj
	end
	if args.adj == 'mid' then
		args.adj = 'on'
		next = args[i]
		i = i + 1
		if next == nil then
			args.mid = ''
		else  -- mid-text words
			if next:sub(1, 1) == '-' then
				args.mid = next
			else
				args.mid = ' ' .. next
			end
		end
	elseif args.adj == 'on' then
		args.mid = ''
	end
	if precision == nil then
		if tonumber(args[i]) ~= nil then
			precision = strip(args[i])
			i = i + 1
		end
	end
	local disp = args.disp
	if disp == 'x' then
		args.joins = { args[i] or '', args[i+1] or '' }
	elseif disp == 's' or disp == '/' then
		args.disp = 'slash'
	end
	args.precision = args.precision or precision  -- allow named parameter
	local abbr = args.abbr
	if abbr == nil then
		-- Default is to abbreviate output (use symbol), or input if flipped.
		args.abbr = (disp == 'flip') and 'in' or 'out'
	else
		args.abbr_org = abbr  -- original abbr (as entered by user)
		if disp == 'flip' then  -- 'in' = LHS, 'out' = RHS
			if abbr == 'in' then
				abbr = 'out'
			elseif abbr == 'out' then
				abbr = 'in'
			end
		end
		args.abbr = abbr
	end
	return true, args, in_unit_table
end
 
local function default_precision(inclean, invalue, outvalue, in_current, out_current)
	-- Return a default value for precision (an integer like 2, 0, -2).
	-- Code follows procedures used in old template.
	-- Am putting exceptions to standard calculations here, as they are
	-- discovered. Can later decide if something cleaner should be done.
	-- LATER: Things like the hand unit of length will need special processing.
	local log10 = math.log10
	local prec, minprec, adjust
	local utype = out_current.utype
	local fudge = 1e-14  -- {{Order of magnitude}} adds this, so we do too
	-- Find fractional digits, handling cases like inclean = '12.345e6'.
	local integer, dot, fraction = inclean:match('^(%d*)([' .. numdot .. ']?)(%d*)')
	if utype == 'temperature' then
		-- LATER: Give an error message if (invalue < in_current.offset): below absolute zero?
		adjust = 0
		local kelvin = (invalue - in_current.offset) * in_current.scale
		if kelvin <= 0 then  -- can get zero, or small but negative value due to precision problems
			minprec = 2
		else
			minprec = 2 - floor(log10(kelvin) + fudge)  -- 3 sigfigs in kelvin
		end
	else
		if invalue == 0 or outvalue <= 0 then
			-- We are never called with a negative outvalue, but it might be zero.
			-- This is special-cased to avoid calculation exceptions.
			return 0
		end
		if out_current.symbol == 'ft' and dot == '' then
			-- More precision when output ft with integer input value.
			adjust = -log10(in_current.scale)
		else
			adjust = log10(math.abs(invalue / outvalue))
		end
		adjust = adjust + log10(2)
		-- Ensure that the output has at least two significant figures.
		minprec = 1 - floor(log10(outvalue) + fudge)
	end
	if dot == '' then
		prec = -integer:match('0*$'):len()  -- '12300' gives -2, but so does '12300e-5'
	else
		if fraction == '' and utype ~= 'temperature' then
			prec = 1  -- "123." has same precision as "123.0", like old template
		else
			prec = #fraction
		end
	end
	return math.max(floor(prec + adjust), minprec)
end
 
local function convert(value, in_current, out_current)
	local inscale = in_current.scale
	local outscale = out_current.scale
	if in_current.invert ~= nil then
		if in_current.invert * out_current.invert < 0 then
			return 1 / (value * inscale * outscale)
		end
		return value * (inscale / outscale)
	elseif in_current.offset ~= nil then
		return (value - in_current.offset) * (inscale / outscale) + out_current.offset
	else
		return value * (inscale / outscale)
	end
end
 
local function cvtround(parms, info, in_current, out_current)
	-- Return true, t where t is a table with the conversion results; fields:
	--   show = rounded, formatted string from converting value in info,
	--      using the rounding specified in parms.
	--   singular = true if result is positive, and (after rounding)
	--      is "1", or like "1.00";
	--   (and more fields shown below, and a calculated 'absvalue' field).
	-- or return true, nil if no value specified;
	-- or return false, t where t is an error message table.
	-- This code combines convert/round because some rounding requires
	-- knowledge of what we are converting.
	local invalue, inclean, show, exponent, singular
	if info ~= nil then
		invalue, inclean = info.value, info.clean
	end
	if invalue == nil or invalue == '' then
		return true, nil
	end
	local outvalue = convert(invalue, in_current, out_current)
	local isnegative
	if outvalue < 0 then
		isnegative = true
		outvalue = -outvalue
	end
	local success
	local precision = parms.precision
	local sigfig = parms.sigfig
	local disp = parms.disp
	if precision then
		-- Ignore sigfig, disp.
		success, precision = require_integer(precision, 'cvt_bad_prec')
		if not success then return false, precision end
	elseif sigfig then
		-- Ignore disp.
		success, sigfig = require_integer(sigfig, 'cvt_bad_sigfig')
		if not success then return false, sigfig end
		if sigfig <= 0 then
			return false, { 'cvt_sigfig_pos', parms.sigfig }
		end
		show, exponent = make_sigfig(outvalue, sigfig)
	elseif disp == '5' then
		show = format('%.0f', floor((outvalue / 5) + 0.5) * 5)
	else
		precision = default_precision(inclean, invalue, outvalue, in_current, out_current)
	end
	if precision then
		if precision >= 0 then
			if precision > limit_abuse then
				precision = limit_abuse
			elseif precision <= 8 then
				-- Add a fudge to handle common cases of bad rounding due to inability
				-- to precisely represent some values. This makes the following work:
				-- {{convert|-100.1|C|K}} and {{convert|5555000|um|m|2}}.
				-- Old template uses #expr round, which invokes PHP round().
				-- LATER: Investigate how PHP round() works.
				outvalue = outvalue + 2e-14
			end
			local fmt = '%.' .. format('%d', precision) .. 'f'
			show = format(fmt, outvalue)
		else
			precision = -precision  -- #digits to zero (in addition to digits after dot)
			if precision > limit_abuse then
				precision = limit_abuse
			end
			local shift = 10 ^ precision
			if shift > outvalue then
				show = '0'  -- like old template, user can zero all digits
			else
				show = format('%.0f', floor(outvalue/shift + 0.5))
				exponent = #show + precision
			end
		end
	end
	-- TODO Does following work when exponent ~= nil?
	--      What if show = '1000' and exponent = 1 (value = .1000*10^1 = 1)?
	--      What if show = '1000' and exponent = 2 (value = .1000*10^2 = 10)?
	if (show == '1' or show:match('^1%.0*$') ~= nil) and not isnegative then
		-- Use match because on some systems 0.99999999999999999 is 1.0.
		singular = true
	end
	local t = format_number(show, exponent, isnegative)
	t.singular = singular
	t.raw_absvalue = outvalue  -- absolute value before rounding
	return true, setmetatable(t, {
		__index = function (self, key)
			if key == 'absvalue' then
				-- Calculate absolute value after rounding, if needed.
				local clean, exponent = rawget(self, 'clean'), rawget(self, 'exponent')
				local value = tonumber(clean)  -- absolute value (any negative sign has been ignored)
				if exponent ~= nil then
					value = value * 10^exponent
				end
				rawset(self, key, value)
				return value
			end
		end })
end
 
-- TODO Think about when to use ' ' and when to use '&nbsp;'.
-- For outputs, could use raw_absvalue (not bothering with calculated absvalue).
-- Old template always uses nbsp before a unit symbol, but seems inconsistent
-- before a unit name. Template:Convert/LoffAonSoff says
--   "Numbers less than 1,000 is not wrapped nor are unit symbols"
-- so 1000 is a threshold (use nbsp for smaller values), but no conclusive results.
-- Possibly a concern is wrapping when using {{convert}} in a table
-- (don't want to force a column to be unnecessarily wide by using nbsp).
local disp_joins = {
	['or']         = { ' or '    , ''  },
	['sqbr-sp']    = { ' ['      , ']' },
	['sqbr-nbsp']  = { '&nbsp;[' , ']' },
	['comma']      = { ', '      , ''  },
	['slash-sp']   = { ' / '     , ''  },
	['slash-nbsp'] = { '&nbsp;/ ', ''  },
	['slash-nosp'] = { '/'       , ''  },
	['b']          = { ' ('      , ')' },
}
 
local function evaluate_condition(value, condition)
	-- Return true or false from applying a conditional expression to value,
	-- or throw an error if invalid.
	-- A very limited set of expressions is supported:
	--    v < 9
	--    v * 9 < 9
	-- where
	--    'v' is replaced with value
	--    9 is any number (as defined by Lua tonumber)
	--    '<' can also be '<=' or '>' or '>='
	-- In addition, the following form is supported:
	--    LHS and RHS
	-- where
	--    LHS, RHS = any of above expressions.
	local function compare(value, text)
		local arithop, factor, compop, limit = text:match('^%s*v%s*([*]?)(.-)([<>]=?)(.*)$')
		if arithop == nil then
			error('Invalid default expression', 0)
		elseif arithop == '*' then
			factor = tonumber(factor)
			if factor == nil then
				error('Invalid default expression', 0)
			end
			value = value * factor
		end
		limit = tonumber(limit)
		if limit == nil then
			error('Invalid default expression', 0)
		end
		if compop == '<' then
			return value < limit
		elseif compop == '<=' then
			return value <= limit
		elseif compop == '>' then
			return value > limit
		elseif compop == '>=' then
			return value >= limit
		end
		error('Invalid default expression', 0)  -- should not occur
	end
	local lhs, rhs = condition:match('^(.-%W)and(%W.*)')
	if lhs == nil then
		return compare(value, condition)
	end
	return compare(value, lhs) and compare(value, rhs)
end
 
local function get_default(value, unit_table)
	-- Return true, s where s = name of unit's default output unit,
	-- or return false, t where t is an error message table.
	-- Some units have a default that depends on the input value
	-- (the first value if a range of values is used).
	-- If '|' is in the default, the first pipe-delimited field is an
	-- expression that uses 'v' to represent the input value.
	-- Example: 'v < 120 | small | big | suffix' (suffix is optional)
	-- evaluates 'v < 120' as a boolean with result
	-- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise.
	local default = default_exceptions[unit_table.symbol] or unit_table.default
	if default == nil then
		return false, { 'cvt_no_default', unit_table.symbol }
	end
	if default:find('|', 1, true) == nil then
		return true, default
	end
	local t = {}
	default = default .. '|'  -- to get last item
	for item in default:gmatch('%s*(.-)%s*|') do
		table.insert(t, item)  -- split on '|', removing leading/trailing whitespace
	end
	if #t == 3 or #t == 4 then
		local success, result = pcall(evaluate_condition, value, t[1])
		if success then
			default = result and t[2] or t[3]
			if #t == 4 then
				default = default .. t[4]
			end
			return true, default
		end
	end
	return false, { 'cvt_bad_default', unit_table.symbol }
end
 
local function make_id(parms, which, unit_table)
	-- Return id, f where
	--   id = unit symbol or name, possibly modified
	--   f = false if id is a symbol, otherwise true (id is a name)
	-- for value 1 or 2 (which), and 'in' or 'out' (unit_table.inout).
	-- Result is '' if no symbol/name is to be used.
	local abbr = parms.abbr
	if abbr == 'values' then
		return ''
	end
	local inout = unit_table.inout
	local valinfo = unit_table.valinfo
	local abbr_org = parms.abbr_org
	local adj = parms.adj
	local disp = parms.disp
	local singular = valinfo[which].singular or (inout == 'in' and adj == 'on')
	if unit_table.usename then
		-- Old template does something like this.
		if inout == 'in' then
			if adj ~= 'on' and (abbr_org == 'out' or disp == 'flip') then
				local value = valinfo[which].value
				singular = (0 < value and value < 1.0001)
			end
		else
			if (abbr_org == 'on') or
				(disp == nil and (abbr_org == nil or abbr_org == 'out')) or
				(disp == 'flip' and abbr_org == 'in') then
				singular = (valinfo[which].absvalue < 1.0001 and
							not valinfo[which].is_scientific)
			end
		end
	end
	local key_name = (singular and not parms.is_range_x) and 'name1' or 'name2'
	local key_symbol = 'symbol'
	if parms.sp == 'us' or unit_table.sp_us then
		key_name = key_name .. '_us'
		key_symbol = 'sym_us'
	end
	local want_name
	if unit_table.usename then
		want_name = true
	else
		if abbr_org == nil then
			if disp == 'or' or disp == 'slash' then
				want_name = true
			end
			if unit_table.utype == 'temperature' or unit_table.utype == 'temperature change' then
				want_name = false
			end
		end
		if want_name == nil then
			if abbr == 'on' or abbr == inout or (abbr == 'mos' and inout == 'out') then
				want_name = false
			else
				want_name = true
			end
		end
	end
	local id = unit_table[want_name and key_name or key_symbol]
	local lk = parms.lk
	if lk == 'on' or lk == inout then
		local link = link_exceptions[unit_table.symbol] or unit_table.link
		if link ~= nil then
			id = '[[' .. link .. '|' .. id .. ']]'
		end
	end
	return id, want_name
end
 
local function process_input(parms, in_current)
	-- Processing required once per conversion.
	-- Return block of text to represent input (value/unit).
	local id1, want_name = make_id(parms, 1, in_current)
	local extra = ''
	local result
	local adj = parms.adj
	local disp = parms.disp
	if disp == 'output only' or
	   disp == 'output number only' or disp == 'number' or
	   disp == 'u2' or disp == 'unit2' then
		result = ''
		parms.joins = { '', '' }
	elseif disp == 'unit' then
		if adj == 'on' then
			result = hyphenated(id1)
		else
			result = id1
		end
		parms.joins = { '', '' }
	else
		local abbr = parms.abbr
		local mos = (abbr == 'mos')
		local range = parms.range
		local id = (range == nil) and id1 or make_id(parms, 2, in_current)
		if id ~= '' then
			if adj == 'on' then
				mos = false  -- if hyphenated, suppress repeat of unit in a range
				extra = '-' .. hyphenated(id) .. parms.mid
			else
				extra = '&nbsp;' .. id
			end
		end
		local valinfo = in_current.valinfo
		if range == nil then
			result = valinfo[1].show
		else
			local rtext = range[1]
			if type(rtext) == 'table' then
				rtext = rtext[abbr] or rtext['off']
			end
			if mos then
				result = valinfo[1].show .. '&nbsp;' .. id1 .. rtext .. valinfo[2].show
			elseif parms.is_range_x and not want_name then
				result = valinfo[1].show .. '&nbsp;' .. id .. rtext .. valinfo[2].show
			else
				result = valinfo[1].show .. rtext .. valinfo[2].show
			end
		end
		if disp == nil then  -- special case the most common setting
			parms.joins = disp_joins['b']
		elseif disp ~= 'x' then
			-- Old template does this.
			if disp == 'slash' then
				if parms.abbr_org == nil then
					disp = 'slash-nbsp'
				elseif abbr == 'in' or abbr == 'out' then
					disp = 'slash-sp'
				else
					disp = 'slash-nosp'
				end
			elseif disp == 'sqbr' then
				if abbr == 'on' then
					disp = 'sqbr-nbsp'
				else
					disp = 'sqbr-sp'
				end
			end
			parms.joins = disp_joins[disp] or disp_joins['b']
		end
	end
	return result .. extra
end
 
local function process_one_output(parms, out_current)
	-- Processing required for each output unit.
	-- Return block of text to represent output (value/unit).
	local id1, want_name = make_id(parms, 1, out_current)
	local extra = ''
	local result
	local disp = parms.disp
	if disp == 'u2' or disp == 'unit2' then  -- 'unit2' is not in old template
		if parms.adj == 'on' then
			result = hyphenated(id1)
		else
			result = id1
		end
	else
		local range = parms.range
		if not (disp == 'output number only' or disp == 'number') then
			local id = (range == nil) and id1 or make_id(parms, 2, out_current)
			if id ~= '' then
				extra = '&nbsp;' .. id
			end
		end
		local valinfo = out_current.valinfo
		if range == nil then
			result = valinfo[1].show
		else
			local rtext = range[2]
			if type(rtext) == 'table' then
				rtext = rtext[parms.abbr] or rtext['on']
			end
			if parms.is_range_x and not want_name then
				result = valinfo[1].show .. extra .. rtext .. valinfo[2].show
			else
				result = valinfo[1].show .. rtext .. valinfo[2].show
			end
		end
	end
	return result .. extra
end
 
local function make_output_single(parms, info, in_unit_table, out_unit_table)
	-- Return true, item where item = wikitext of the conversion result
	-- for a single output (which is not a combination or a multiple);
	-- or return false, t where t is an error message table.
	local success, info1, info2
	success, info1 = cvtround(parms, info[1], in_unit_table, out_unit_table)
	if not success then return false, info1 end
	success, info2 = cvtround(parms, info[2], in_unit_table, out_unit_table)
	if not success then return false, info2 end
	out_unit_table.valinfo = { info1, info2 }
	return true, process_one_output(parms, out_unit_table)
end
 
local function make_output_multiple(parms, info, in_unit_table, out_unit_table)
	-- Return true, item where item = wikitext of the conversion result
	-- for an output which is a multiple (like 'ftin');
	-- or return false, t where t is an error message table.
	local multiple = out_unit_table.multiple  -- table of scaling factors (will not be nil)
	local combos = out_unit_table.combination  -- table of unit tables (will not be nil)
	local abbr = parms.abbr
	local abbr_org = parms.abbr_org
	local disp = parms.disp
	local want_name = (abbr_org == nil and (disp == 'or' or disp == 'slash')) or
					  not (abbr == 'on' or abbr == 'out' or abbr == 'mos')
	local want_link = (parms.lk == 'on' or parms.lk == 'out')
	local function make_result(info)
		local outvalue, sign, fmt
		local results = {}
		for i = 1, #combos do
			local thisvalue
			local out_current = combos[i]
			out_current.inout = 'out'
			local scale = multiple[i]
			if i == 1 then  -- least significant unit ('in' from 'ftin')
				local success, outinfo = cvtround(parms, info, in_unit_table, out_current)
				if not success then return false, outinfo end
				sign = outinfo.sign
				local fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or ''
				fmt = '%.' .. #fraction .. 'f'  -- to reproduce precision
				if fraction == '' then
					outvalue = floor(outinfo.raw_absvalue + 0.5)  -- keep all integer digits of least significant unit
				else
					outvalue = outinfo.absvalue
				end
			end
			if scale then
				outvalue, thisvalue = floor(outvalue / scale), outvalue % scale
			else
				thisvalue = outvalue
			end
			local id
			if want_name then
				id = out_current[(thisvalue == 1) and 'name1' or 'name2']
			else
				id = out_current['symbol']
			end
			if want_link then
				local link = out_current.link
				if link ~= nil then
					id = '[[' .. link .. '|' .. id .. ']]'
				end
			end
			local strval = (thisvalue == 0) and '0' or with_separator(format(fmt, thisvalue))
			table.insert(results, strval .. '&nbsp;' .. id)
			if outvalue == 0 then
				break
			end
			fmt = '%.0f'  -- only least significant unit can have a fraction
		end
		local reversed, count = {}, #results
		for i = 1, count do
			reversed[i] = results[count + 1 - i]
		end
		return sign .. table.concat(reversed, ' ')
	end
	local result = make_result(info[1])
	local range = parms.range
	if range ~= nil then
		local rtext = range[2]
		if type(rtext) == 'table' then
			rtext = rtext[abbr] or rtext['on']
		end
		result = result .. rtext .. make_result(info[2])
	end
	return true, result
end
 
local function process(parms, in_unit_table)
	-- Return true, s where s = final wikitext result,
	-- or return false, t where t is an error message table.
	local success, out_unit_table
	local info = in_unit_table.valinfo
	local invalue1 = info[1].value
	local out_unit = parms.out_unit
	if out_unit == nil or out_unit == '' then
		success, out_unit = get_default(invalue1, in_unit_table)
		if not success then return false, out_unit end
	end
	success, out_unit_table = lookup(out_unit, parms.sp, 'any_combination')
	if not success then return false, out_unit_table end
	if in_unit_table.utype ~= out_unit_table.utype then
		return false, { 'cvt_mismatch', in_unit_table.utype, out_unit_table.utype }
	end
	local outputs = {}
	local combos  -- nil (for 'ft' or 'ftin'), or table of unit tables (for 'm ft')
	if out_unit_table.multiple == nil then  -- nil ('ft' or 'm ft'), or table of factors ('ftin')
		combos = out_unit_table.combination
	end
	local imax = combos and #combos or 1  -- 1 (single unit) or number of unit tables
	for i = 1, imax do
		local success, item
		local out_current = combos and combos[i] or out_unit_table
		out_current.inout = 'out'
		if out_current.multiple == nil then
			success, item = make_output_single(parms, info, in_unit_table, out_current)
		else
			success, item = make_output_multiple(parms, info, in_unit_table, out_current)
		end
		if not success then return false, item end
		table.insert(outputs, item)
	end
	local disp = parms.disp
	local in_block = process_input(parms, in_unit_table)
	local out_block = (disp == 'unit') and '' or table.concat(outputs, '; ')
	if disp == 'flip' then
		in_block, out_block = out_block, in_block
	end
	local wikitext = in_block .. parms.joins[1] .. out_block .. parms.joins[2]
	if parms.sortable == 'on' then
		wikitext = ntsh(invalue1, parms.debug) .. wikitext
	end
	return true, wikitext
end
 
local function convert(frame)
	set_config(frame)
	local result
	local success, parms, in_unit_table = get_parms(frame:getParent())
	if success then
		success, result = process(parms, in_unit_table)
	else
		result = parms
	end
	if success then
		return result
	end
	return messages.message(result, lang)
end
 
local function dump(s)
	if s == nil then
		return 'NIL'
	elseif s == '' then
		return '(empty)'
	elseif type(s) ~= 'string' then
		return '(not string)'
	else
		local t = { "'" }
		for i = 1, #s do
			local c = s:sub(i, i)
			local n = c:byte()
			if ' ' <= c and n <= 126 then
				t[#t+1] = c
			else
				t[#t+1] = string.format('<%02x>', n)
			end
		end
		t[#t+1] = "'"
		return table.concat(t)
	end
end
 
local function convert_dump(frame)
	-- Trying to work out what is seen with input like:
	-- {{convert|123<ref>Hello</ref>|m|ft}}
	local args = frame:getParent().args
	local result = { }
	for i, v in ipairs(args) do
		local pos = v:find(string.char(127), 1, true)
		local append = ''
		if pos then
			if v:find('-ref-', 1, true) then
				append = ' (ref)'
			else
				append = ' (other)'
			end
		end
		result[i] = dump(v) .. append
	end
	return table.concat(result, '\n\n')
end
 
return { convert = convert_dump }