Vai al contenuto

Modulo:Val

Da Wikipedia, l'enciclopedia libera.
Info Istruzioni per l'uso
Questo è un modulo scritto in Lua. Le istruzioni che seguono sono contenute nella sottopagina Modulo:Val/man (modifica · cronologia)
Sandbox: Modulo:Val/sandbox (modifica · cronologia) · Sottopagine: lista · Test: Modulo:Val/test (modifica · cronologia · esegui)

Modulo Lua che implementa le funzionalità del Template:M.

Le unità di misura riconosciute sono contenute nella sottopagina Modulo:Val/units.


-- EXPERIMENT from June 2018: Start localization for dewiki, not tested.
-- For Template:Val, output a number and optional unit.
-- Format options include scientific and uncertainty notations.


local numdot = ','  -- decimal mark (use ',' for italian)
local altnumdot = '.' -- alternative decimal mark (use ',' for italiana)
local numsep = ' '  -- group separator (use ' ' for italian)

local data_module = 'Module:Val/units'
local convert_module = 'Module:Convert'

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 &quot;e&quot;',
   ['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 &quot;e&quot;',
   ['mt-not-accept-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 &quot;%s=%s&quot; is not supported',
   ['mt-invalid-scale'] =		   'Unit &quot;%s&quot; has invalid scale &quot;%s&quot;',
   ['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 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-accept-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 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(mw.ustring.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 = mw.ustring.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 }