Anonymous
×
Create a new article
Write your page title here:
We currently have 15 articles on TwistedFates Database. Type your article name above or click on one of the titles below and start writing!



    TwistedFates Database
    15Articles

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

    -- For Template:Val, output a number and optional unit.
    -- Format options include scientific and uncertainty notations.
    
    local numdot = '.'  -- decimal mark (use ',' for Italian)
    local numsep = ','  -- group separator (use ' ' for Italian)
    local mtext = {
    	-- Message and other text that should be localized.
    	['mt-bad-exponent'] =       'exponent parameter (<b>e</b>)',
    	['mt-parameter'] =          'parameter ',
    	['mt-not-number'] =         'is not a valid number',
    	['mt-cannot-range'] =       'cannot use a range if the first parameter includes "e"',
    	['mt-need-range'] =         'needs a range in parameter 2',
    	['mt-should-range'] =       'should be a range',
    	['mt-cannot-with-e'] =      'cannot be used if the first parameter includes "e"',
    	['mt-not-range'] =          'does not accept a range',
    	['mt-cannot-e'] =           'cannot use e notation',
    	['mt-too-many-parameter'] = 'too many parameters',
    	['mt-need-number'] =        'need a number after the last parameter because it is a range.',
    	['mt-ignore-parameter4'] =  'Val parameter 4 ignored',
    	['mt-val-not-supported'] =  'Val parameter "%s=%s" is not supported',
    	['mt-invalid-scale'] =      'Unit "%s" has invalid scale "%s"',
    	['mt-both-u-ul'] =          'unit (<b>u</b>) and unit with link (<b>ul</b>) are both specified, only one is allowed.',
    	['mt-both-up-upl'] =        'unit per (<b>up</b>) and unit per with link (<b>upl</b>) are both specified, only one is allowed.',
    }
    
    local data_module = 'Module:Val/units'
    local convert_module = 'Module:Convert'
    
    local function valerror(msg, nocat, iswarning)
    	-- Return formatted message text for an error or warning.
    	-- Can append "#FormattingError" to URL of a page with a problem to find it.
    	local anchor = '<span id="FormattingError"></span>'
    	local body, category
    	if nocat or mw.title.getCurrentTitle():inNamespaces(1, 2, 3, 5) then
    		-- No category in Talk, User, User_talk, or Wikipedia_talk.
    		category = ''
    	else
    		category = '[[Category:Pages with incorrect formatting templates use]]'
    	end
    	iswarning = false  -- problems are infrequent so try showing large error so editor will notice
    	if iswarning then
    		body = '<sup class="noprint Inline-Template" style="white-space:nowrap;">' ..
    			'[[Template:Val|<span title="' ..
    			msg:gsub('"', '&quot;') ..
    			'">warning</span>]]</sup>'
    	else
    		body = '<strong class="error">' ..
    			'Error in &#123;&#123;[[Template:val|val]]&#125;&#125;: ' ..
    			msg ..
    			'</strong>'
    	end
    	return anchor .. body .. category
    end
    
    local range_types = {
    	-- No need for '&nbsp;' because nowrap applies to all output.
    	[","]   = ", ",
    	["by"]  = " by ",
    	["-"]   = "–",
    	["–"]   = "–",
    	["and"] = " and ",
    	["or"]  = " or " ,
    	["to"]  = " to " ,
    	["x"]   = " × ",
    	["×"]   = " × ",
    	["/"]   = "/",
    }
    local range_repeat_unit = {
    	-- WP:UNIT wants unit repeated when a "multiply" range is used.
    	["x"]   = true,
    	["×"]   = true,
    }
    
    local function extract_item(index, numbers, arg)
    	-- Extract an item from arg and store the result in numbers[index].
    	-- If no argument or if argument is valid, return nil (no error);
    	-- otherwise, return an error message.
    	-- The stored result is:
    	-- * a table for a number (empty if there was no specified number); or
    	-- * a string for range text
    	-- Input like 1e3 is regarded as invalid for all except argument 1
    	-- which accepts e notation as an alternative to the 'e' argument.
    	-- Input group separators are removed.
    	local which = index
    	local function fail(msg)
    		local description
    		if which == 'e' then
    			description = mtext['mt-bad-exponent']
    		else
    			description = mtext['mt-parameter'] .. which
    		end
    		return description .. ' ' .. (msg or mtext['mt-not-number']) .. '.'
    	end
    	local result = {}
    	local range = range_types[arg]
    	if range then
    		if type(index) == 'number' and (index % 2 == 0) then
    			if index == 2 then
    				if numbers[1] and numbers[1].exp then
    					return fail(mtext['mt-cannot-range'])
    				end
    				numbers.has_ranges = true
    			else
    				if not numbers.has_ranges then
    					return fail(mtext['mt-need-range'])
    				end
    			end
    			numbers[index] = range
    			if range_repeat_unit[arg] then
    				-- Any "repeat" range forces unit (if any) to be repeated for all items.
    				numbers.isrepeat = true
    			end
    			return nil
    		end
    		return fail(mtext['mt-not-range'])
    	end
    	if numbers.has_ranges and type(index) == 'number' and (index % 2 == 0) then
    		return fail(mtext['mt-should-range'])
    	end
    	if index == 'e' then
    		local e = numbers[1] and numbers[1].exp
    		if e then
    			if arg then
    				return fail(mtext['mt-cannot-with-e'])
    			end
    			arg = e
    			which = 1
    		end
    	end
    	if arg and arg ~= '' then
    		arg = arg:gsub(numsep, '')
    		if numdot ~= '.' then
    			arg = arg:gsub(numdot, '.')
    		end
    		if arg:sub(1, 1) == '(' and arg:sub(-1) == ')' then
    			result.parens = true
    			arg = arg:sub(2, -2)
    		end
    		local a, b = arg:match('^(.+)[Ee](.+)$')
    		if a then
    			if index == 1 then
    				arg = a
    				result.exp = b
    			else
    				return fail(mtext['mt-cannot-e'])
    			end
    		end
    		local isnegative, propersign, prefix
    		local minus = '−'
    		prefix, arg = arg:match('^(.-)([%d.]+)$')
    		local value = tonumber(arg)
    		if not value then
    			return fail()
    		end
    		if arg:sub(1, 1) == '.' then
    			arg = '0' .. arg
    		end
    		if prefix == '' then
    			-- Ignore.
    		elseif prefix == '±' then
    			-- Display for first number, ignore for others.
    			if index == 1 then
    				propersign = '±'
    			end
    		elseif prefix == '+' then
    			propersign = '+'
    		elseif prefix == '-' or prefix == minus then
    			propersign = minus
    			isnegative = true
    		else
    			return fail()
    		end
    		result.clean = arg
    		result.sign = propersign or ''
    		result.value = isnegative and -value or value
    	end
    	numbers[index] = result
    	return nil  -- no error
    end
    
    local function get_args(numbers, args)
    	-- Extract arguments and store the results in numbers.
    	-- Return nothing (no error) if ok; otherwise, return an error message.
    	for index = 1, 99 do
    		local which = index
    		local arg = args[which]  -- has been trimmed
    		if not arg then
    			which = 'e'
    			arg = args[which]
    		end
    		local msg = extract_item(which, numbers, arg)
    		if msg then
    			return msg
    		end
    		if which == 'e' then
    			break
    		end
    		if index > 19 then
    			return mtext['mt-too-many-parameter']
    		end
    	end
    	if numbers.has_ranges and (#numbers % 2 == 0) then
    		return mtext['mt-need-number']
    	end
    end
    
    local function get_scale(text, ucode)
    	-- Return the value of text as a number, or throw an error.
    	-- This supports extremely basic expressions of the form:
    	--   a / b
    	--   a ^ b
    	-- where a and b are numbers or 'pi'.
    	local n = tonumber(text)
    	if n then
    		return n
    	end
    	n = text:gsub('pi', math.pi)
    	for _, op in ipairs({ '/', '^' }) do
    		local a, b = n:match('^(.-)' .. op .. '(.*)$')
    		if a then
    			a = tonumber(a)
    			b = tonumber(b)
    			if a and b then
    				if op == '/' then
    					return a / b
    				elseif op == '^' then
    					return a ^ b
    				end
    			end
    			break
    		end
    	end
    	error(string.format(mtext['mt-invalid-scale'], ucode, text))
    end
    
    local function get_builtin_unit(ucode, definitions)
    	-- Return table of information for the specified built-in unit, or nil if not known.
    	-- Each defined unit code must be followed by two spaces (not tab characters).
    	local _, pos = definitions:find('\n' .. ucode .. '  ', 1, true)
    	if pos then
    		local endline = definitions:find('%s*\n', pos)
    		if endline then
    			local result = {}
    			local n = 0
    			local text = definitions:sub(pos + 1, endline - 1):gsub('%s%s+', '\t')
    			for item in (text .. '\t'):gmatch('(%S.-)\t') do
    				if item == 'ALIAS' then
    					result.alias = true
    				elseif item == 'ANGLE' then
    					result.isangle = true
    					result.nospace = true
    				elseif item == 'NOSPACE' then
    					result.nospace = true
    				elseif item == 'SI' then
    					result.si = true
    				else
    					n = n + 1
    					if n == 1 then
    						local link, symbol = item:match('^%[%[([^|]+)|(.+)%]%]$')
    						if link then
    							result.symbol = symbol
    							result.link = link
    							n = 2
    						else
    							result.symbol = item
    						end
    					elseif n == 2 then
    						result.link = item
    					elseif n == 3 then
    						result.scale_text = item
    						result.scale = get_scale(item, ucode)
    					else
    						result.more_ignored = item
    						break
    					end
    				end
    			end
    			if result.si then
    				local s = result.symbol
    				if ucode == 'mc' .. s or ucode == 'mu' .. s then
    					result.ucode = 'µ' .. s  -- unit code for convert should be this
    				end
    			end
    			if n >= 2 or (n >= 1 and result.alias) then
    				return result
    			end
    			-- Ignore invalid definition, treating it as a comment.
    		end
    	end
    end
    
    local function convert_lookup(ucode, value, scaled_top, want_link, si, options)
    	local lookup = require(convert_module)._unit
    	return lookup(ucode, {
    			value = value,
    			scaled_top = scaled_top,
    			link = want_link,
    			si = si,
    			sort = options.sortable,
    		})
    end
    
    local function get_unit(ucode, value, scaled_top, options)
    	local want_link = options.want_link
    	if scaled_top then
    		want_link = options.want_per_link
    	end
    	local data = mw.loadData(data_module)
    	local result = options.want_longscale and
    		get_builtin_unit(ucode, data.builtin_units_long_scale) or
    		get_builtin_unit(ucode, data.builtin_units)
    	local si, use_convert
    	if result then
    		if result.alias then
    			ucode = result.symbol
    			use_convert = true
    		end
    		if result.scale then
    			-- Setting si means convert will use the unit as given, and the sort key
    			-- will be calculated from the value without any extra scaling that may
    			-- occur if convert found the unit code. For example, if val defines the
    			-- unit 'year' with a scale and if si were not set, convert would also apply
    			-- its own scale because convert knows that a year is 31,557,600 seconds.
    			si = { result.symbol, result.link }
    			value = value * result.scale
    		end
    		if result.si then
    			ucode = result.ucode or ucode
    			si = { result.symbol, result.link }
    			use_convert = true
    		end
    	else
    		result = {}
    		use_convert = true
    	end
    	local convert_unit = convert_lookup(ucode, value, scaled_top, want_link, si, options)
    	result.sortkey = convert_unit.sortspan
    	if use_convert then
    		result.text = convert_unit.text
    		result.scaled_top = convert_unit.scaled_value
    	else
    		if want_link then
    			result.text = '[[' .. result.link .. '|' .. result.symbol .. ']]'
    		else
    			result.text = result.symbol
    		end
    		result.scaled_top = value
    	end
    	return result
    end
    
    local function makeunit(value, options)
    	-- Return table of information for the requested unit and options, or
    	-- return nil if no unit.
    	options = options or {}
    	local unit
    	local ucode = options.u
    	local percode = options.per
    	if ucode then
    		unit = get_unit(ucode, value, nil, options)
    	elseif percode then
    		unit = { nospace = true, scaled_top = value }
    	else
    		return nil
    	end
    	local text = unit.text or ''
    	local sortkey = unit.sortkey
    	if percode then
    		local function bracketed(code, text)
    			return code:find('[*./]') and '(' .. text .. ')' or text
    		end
    		local perunit = get_unit(percode, 1, unit.scaled_top, options)
    		text = (ucode and bracketed(ucode, text) or '') ..
    				'/' .. bracketed(percode, perunit.text)
    		sortkey = perunit.sortkey
    	end
    	if not (unit.nospace or options.nospace) then
    		text = '&nbsp;' .. text
    	end
    	return { text = text, isangle = unit.isangle, sortkey = sortkey }
    end
    
    local function list_units(mode)
    	-- Return wikitext to list the built-in units.
    	-- A unit code should not contain wikimarkup so don't bother escaping.
    	local data = mw.loadData(data_module)
    	local definitions = data.builtin_units .. data.builtin_units_long_scale
    	local last_was_blank = true
    	local n = 0
    	local result = {}
    	local function add(line)
    		if line == '' then
    			last_was_blank = true
    		else
    			if last_was_blank and n > 0 then
    				n = n + 1
    				result[n] = ''
    			end
    			last_was_blank = false
    			n = n + 1
    			result[n] = line
    		end
    	end
    	local si_prefixes = {
    		-- These are the prefixes recognized by convert; u is accepted for micro.
    		y = 'y',
    		z = 'z',
    		a = 'a',
    		f = 'f',
    		p = 'p',
    		n = 'n',
    		u = 'µ',
    		['µ'] = 'µ',
    		m = 'm',
    		c = 'c',
    		d = 'd',
    		da = 'da',
    		h = 'h',
    		k = 'k',
    		M = 'M',
    		G = 'G',
    		T = 'T',
    		P = 'P',
    		E = 'E',
    		Z = 'Z',
    		Y = 'Y',
    	}
    	local function is_valid(ucode, unit)
    		if unit and not unit.more_ignored then
    			assert(type(unit.symbol) == 'string' and unit.symbol ~= '')
    			if unit.alias then
    				if unit.link or unit.scale_text or unit.si then
    					return false
    				end
    			end
    			if unit.si then
    				if unit.scale_text then
    					return false
    				end
    				ucode = unit.ucode or ucode
    				local base = unit.symbol
    				if ucode == base then
    					unit.display = base
    					return true
    				end
    				local plen = #ucode - #base
    				if plen > 0 then
    					local prefix = si_prefixes[ucode:sub(1, plen)]
    					if prefix and ucode:sub(plen + 1) == base then
    						unit.display = prefix .. base
    						return true
    					end
    				end
    			else
    				unit.display = unit.symbol
    				return true
    			end
    		end
    		return false
    	end
    	local lookup = require(convert_module)._unit
    	local function show_convert(ucode, unit)
    		-- If a built-in unit defines a scale or sets the SI flag, any unit defined in
    		-- convert is not used (the scale or SI prefix's scale is used for a sort key).
    		-- If there is no scale or SI flag, and the unit is not defined in convert,
    		-- the sort key may not be correct; this allows such units to be identified.
    		if not (unit.si or unit.scale_text) then
    			if mode == 'convert' then
    				unit.show = not lookup(unit.alias and unit.symbol or ucode).unknown
    				unit.show_text = 'CONVERT'
    			elseif mode == 'unknown' then
    				unit.show = lookup(unit.alias and unit.symbol or ucode).unknown
    				unit.show_text = 'UNKNOWN'
    			elseif not unit.alias then
    				-- Show convert's scale in square brackets ('[1]' for an unknown unit).
    				-- Don't show scale for an alias because it's misleading for temperature
    				-- and an alias is probably not useful for anything else.
    				local scale = lookup(ucode, {value=1, sort='on'}).scaled_value
    				if type(scale) == 'number' then
    					scale = string.format('%.5g', scale):gsub('e%+?(%-?)0*(%d+)', 'e%1%2')
    				else
    					scale = '?'
    				end
    				unit.show = true
    				unit.show_text = '[' .. scale .. ']'
    			end
    		end
    	end
    	for line in definitions:gmatch('([^\n]*)\n') do
    		local pos, _ = line:find('  ', 1, true)
    		if pos then
    			local ucode = line:sub(1, pos - 1)
    			local unit = get_builtin_unit(ucode, '\n' .. line .. '\n')
    			if is_valid(ucode, unit) then
    				show_convert(ucode, unit)
    				local flags, text
    				if unit.alias then
    					text = unit.symbol
    				else
    					text = '[[' .. unit.link .. '|' .. unit.display .. ']]'
    				end
    				if unit.isangle then
    					unit.nospace = nil  -- don't show redundant flag
    				end
    				for _, f in ipairs({
    						{ 'alias', 'ALIAS' },
    						{ 'isangle', 'ANGLE' },
    						{ 'nospace', 'NOSPACE' },
    						{ 'si', 'SI' },
    						{ 'scale_text', unit.scale_text },
    						{ 'show', unit.show_text },
    					}) do
    					if unit[f[1]] then
    						local t = f[2]
    						if t:match('^%u+$') then
    							t = '<small>' .. t .. '</small>'
    						end
    						if flags then
    							flags = flags .. ' ' .. t
    						else
    							flags = t
    						end
    					end
    				end
    				if flags then
    					text = text .. ' • ' .. flags
    				end
    				add(ucode .. ' = ' .. text .. '<br />')
    			else
    				add(line .. ' ◆ <b>invalid definition</b><br />')
    			end
    		else
    			add(line)
    		end
    	end
    	return table.concat(result, '\n')
    end
    
    local delimit_groups = require('Module:Gapnum').groups
    local function delimit(sign, numstr, fmt)
    	-- Return sign and numstr (unsigned digits or numdot only) after formatting.
    	-- Four-digit integers are not formatted with gaps.
    	fmt = (fmt or ''):lower()
    	if fmt == 'none' or (fmt == '' and #numstr == 4 and numstr:match('^%d+$')) then
    		return sign .. numstr
    	end
    	-- Group number by integer and decimal parts.
    	-- If there is no decimal part, delimit_groups returns only one table.
    	local ipart, dpart = delimit_groups(numstr)
    	local result
    	if fmt == 'commas' then
    		result = sign .. table.concat(ipart, numsep)
    		if dpart then
    			result = result .. numdot .. table.concat(dpart)
    		end
    	else
    		-- Delimit with a small gap by default.
    		local groups = {}
    		groups[1] = table.remove(ipart, 1)
    		for _, v in ipairs(ipart) do
    			table.insert(groups, '<span style="margin-left:.25em;">' .. v .. '</span>')
    		end
    		if dpart then
    			table.insert(groups, numdot .. (table.remove(dpart, 1) or ''))
    			for _, v in ipairs(dpart) do
    				table.insert(groups, '<span style="margin-left:.25em;">' .. v .. '</span>')
    			end
    		end
    		result = sign .. table.concat(groups)
    	end
    	return result
    end
    
    local function sup_sub(sup, sub, align)
    	-- Return the same result as Module:Su except val defaults to align=right.
    	if align == 'l' or align == 'left' then
    		align = 'left'
    	elseif align == 'c' or align == 'center' then
    		align = 'center'
    	else
    		align = 'right'
    	end
    	return '<span style="display:inline-block;margin-bottom:-0.3em;vertical-align:-0.4em;line-height:1.2em;font-size:85%;text-align:' ..
    		align .. ';">' .. sup .. '<br />' .. sub .. '</span>'
    end
    
    local function range_text(items, unit_table, options)
    	local fmt = options.fmt
    	local nend = items.nend or ''
    	if items.isrepeat or unit_table.isangle then
    		nend = nend .. unit_table.text
    	end
    	local text = ''
    	for i = 1, #items do
    		if i % 2 == 0 then
    			text = text .. items[i]
    		else
    			text = text .. delimit(items[i].sign, items[i].clean, fmt) .. nend
    		end
    	end
    	return text
    end
    
    local function uncertainty_text(uncertainty, unit_table, options)
    	local angle, text, need_parens
    	if unit_table.isangle then
    		angle = unit_table.text
    	end
    	local upper = uncertainty.upper or {}
    	local lower = uncertainty.lower or {}
    	local uncU = upper.clean
    	if uncU then
    		local fmt = options.fmt
    		local uncL = lower.clean
    		if uncL then
    			uncU = delimit('+', uncU, fmt) .. (upper.errend or '')
    			uncL = delimit('−', uncL, fmt) .. (lower.errend or '')
    			if angle then
    				uncU = uncU .. angle
    				uncL = uncL .. angle
    			end
    			text = (angle or '') ..
    				'<span style="margin-left:0.3em;">' ..
    				sup_sub(uncU, uncL, options.align) ..
    				'</span>'
    		else
    			if upper.parens then
    				text = '(' .. uncU .. ')'  -- old template did not delimit
    			else
    				text = (angle or '') ..
    					'<span style="margin-left:0.3em;margin-right:0.15em;">±</span>' ..
    					delimit('', uncU, fmt)
    				need_parens = true
    			end
    			if uncertainty.errend then
    				text = text .. uncertainty.errend
    			end
    			if angle then
    				text = text .. angle
    			end
    		end
    	else
    		if angle then
    			text = angle
    		end
    	end
    	return text, need_parens
    end
    
    local function _main(values, unit_spec, options)
    	if options.sandbox then
    		data_module = data_module .. '/sandbox'
    		convert_module = convert_module .. '/sandbox'
    	end
    	local action = options.action
    	if action then
    		if action == 'list' then
    			-- Kludge: am using the align parameter (a=xxx) for type of list.
    			return list_units(options.align)
    		end
    		return valerror('invalid action "' .. action .. '".', options.nocat)
    	end
    	local number = values.number or (values.numbers and values.numbers[1]) or {}
    	local e_10 = options.e or {}
    	local novalue = (number.value == nil and e_10.clean == nil)
    	local fmt = options.fmt
    	local want_sort = true
    	local sortable = options.sortable
    	if sortable == 'off' or (sortable == nil and novalue) then
    		want_sort = false
    	elseif sortable == 'debug' then
    		-- Same as sortable = 'on' but the sort key is displayed.
    	else
    		sortable = 'on'
    	end
    	local sort_value = 1
    	if want_sort then
    		sort_value = number.value or 1
    		if e_10.value and sort_value ~= 0 then
    			-- The 'if' avoids {{val|0|e=1234}} giving an invalid sort_value due to overflow.
    			sort_value = sort_value * 10^e_10.value
    		end
    	end
    	local unit_table = makeunit(sort_value, {
    						u = unit_spec.u,
    						want_link = unit_spec.want_link,
    						per = unit_spec.per,
    						want_per_link = unit_spec.want_per_link,
    						nospace = novalue,
    						want_longscale = unit_spec.want_longscale,
    						sortable = sortable,
    					})
    	local sortkey
    	if unit_table then
    		if want_sort then
    			sortkey = unit_table.sortkey
    		end
    	else
    		unit_table = { text = '' }
    		if want_sort then
    			sortkey = convert_lookup('dummy', sort_value, nil, nil, nil, { sortable = sortable }).sortspan
    		end
    	end
    	local final_unit = unit_table.isangle and '' or unit_table.text
    	local e_text, n_text, need_parens
    	local uncertainty = values.uncertainty
    	if uncertainty then
    		if number.clean then
    			n_text = delimit(number.sign, number.clean, fmt) .. (number.nend or '')
    			local text
    			text, need_parens = uncertainty_text(uncertainty, unit_table, options)
    			if text then
    				n_text = n_text .. text
    			end
    		else
    			n_text = ''
    		end
    	else
    		if values.numbers.isrepeat then
    			final_unit = ''
    		end
    		n_text = range_text(values.numbers, unit_table, options)
    		need_parens = true
    	end
    	if e_10.clean then
    		if need_parens then
    			n_text = '(' .. n_text .. ')'
    		end
    		e_text = '10<sup>' .. delimit(e_10.sign, e_10.clean, fmt) .. '</sup>'
    		if number.clean then
    			e_text = '<span style="margin-left:0.25em;margin-right:0.15em;">×</span>' .. e_text
    		end
    	else
    		e_text = ''
    	end
    	local result =
    		(sortkey or '') ..
    		(options.prefix or '') ..
    		n_text ..
    		e_text ..
    		final_unit ..
    		(options.suffix or '')
    	if result ~= '' then
    		result = '<span class="nowrap">' .. result .. '</span>'
    	end
    	return result .. (options.warning or '')
    end
    
    local function check_parameters(args, has_ranges, nocat)
    	-- Return warning text for the first problem parameter found, or nothing if ok.
    	local whitelist = {
    		a = true,
    		action = true,
    		debug = true,
    		e = true,
    		['end'] = true,
    		errend = true,
    		['+errend'] = true,
    		['-errend'] = true,
    		fmt = true,
    		['long scale'] = true,
    		long_scale = true,
    		longscale = true,
    		nocategory = true,
    		p = true,
    		s = true,
    		sortable = true,
    		u = true,
    		ul = true,
    		up = true,
    		upl = true,
    	}
    	for k, v in pairs(args) do
    		if type(k) == 'string' and not whitelist[k] then
    			local warning = string.format(mtext['mt-val-not-supported'], k, v)
    			return valerror(warning, nocat, true)
    		end
    	end
    	if not has_ranges and args[4] then
    		return valerror(mtext['mt-ignore-parameter4'], nocat, true)
    	end
    end
    
    local function main(frame)
    	local getArgs = require('Module:Arguments').getArgs
    	local args = getArgs(frame, {wrappers = { 'Template:Val' }})
    	local nocat = args.nocategory
    	local numbers = {}  -- table of number tables, perhaps with range text
    	local msg = get_args(numbers, args)
    	if msg then
    		return valerror(msg, nocat)
    	end
    	if args.u and args.ul then
    		return valerror(mtext['mt-both-u-ul'], nocat)
    	end
    	if args.up and args.upl then
    		return valerror(mtext['mt-both-up-upl'], nocat)
    	end
    	local values
    	if numbers.has_ranges then
    		-- Multiple values with range separators but no uncertainty.
    		numbers.nend = args['end']
    		values = {
    			numbers = numbers,
    		}
    	else
    		-- A single value with optional uncertainty.
    		local function setfield(i, dst, src)
    			local v = args[src]
    			if v then
    				if numbers[i] then
    					numbers[i][dst] = v
    				else
    					numbers[i] = { [dst] = v }
    				end
    			end
    		end
    		setfield(1, 'nend', 'end')
    		setfield(2, 'errend', '+errend')
    		setfield(3, 'errend', '-errend')
    		values = {
    			number = numbers[1],
    			uncertainty = {
    				upper = numbers[2],
    				lower = numbers[3],
    				errend = args.errend,
    			}
    		}
    	end
    	local unit_spec = {
    			u = args.ul or args.u,
    			want_link = args.ul ~= nil,
    			per = args.upl or args.up,
    			want_per_link = args.upl ~= nil,
    			want_longscale = (args.longscale or args.long_scale or args['long scale']) == 'on',
    		}
    	local options = {
    			action = args.action,
    			align = args.a,
    			e = numbers.e,
    			fmt = args.fmt,
    			nocat = nocat,
    			prefix = args.p,
    			sandbox = string.find(frame:getTitle(), 'sandbox', 1, true) ~= nil,
    			sortable = args.sortable or (args.debug == 'yes' and 'debug' or nil),
    			suffix = args.s,
    			warning = check_parameters(args, numbers.has_ranges, nocat),
    		}
    	return _main(values, unit_spec, options)
    end
    
    return { main = main, _main = _main }