-- For Template:Val, output a number and optional unit.
-- Format options include scientific and uncertainty notations.  
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 = ''
end
iswarning = false -- problems are infrequent so try showing large error so editor will notice
if iswarning then
body = '<sup>' ..
'<span title="' ..
msg:gsub('"', '&quot;') ..
'">warning</span></sup>'
else
body = '<strong>' ..
'Error in &#123;&#123;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 commas are removed so 1,234 is the same as 1234.
local which = index
local function fail(msg)
local description
if which == 'e' then
description = 'exponent parameter (<b>e</b>)'
else
description = 'parameter ' .. which
end
return description .. ' ' .. (msg or 'is not a valid 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('cannot use a range if the first parameter includes "e"')
end
numbers.has_ranges = true
else
if not numbers.has_ranges then
return fail('needs a range in parameter 2')
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('does not accept a range')
end
if numbers.has_ranges and type(index) == 'number' and (index % 2 == 0) then
return fail('should be a range')
end
if index == 'e' then
local e = numbers[1] and numbers[1].exp
if e then
if arg then
return fail('cannot be used if the first parameter includes "e"')
end
arg = e
which = 1
end
end
if arg and arg ~= '' then
arg = arg:gsub(',', '')
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('cannot use e notation')
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 'too many parameters'
end
end
if numbers.has_ranges and (#numbers % 2 == 0) then
return 'need a number after the last parameter because it is a range.'
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('Unit "' .. ucode .. '" has invalid scale "' .. 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