မဝ်ဂျူ:Val

နူ ဝဳကဳပဳဒဳယာ

This module implements {{Val}}.

The following modules are developed:

Use {{val/sandbox}} for testing, for example:

  • {{val/sandbox|1234.5678|(23)|u=cm}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|u=cm}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|4.56|u=cm}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|e=3|u=cm}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|(23)|e=3|u=cm}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|e=3|u=cm}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|4.56|e=3|u=cm}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|4.56|e=3|u=cm|end=$|+errend=U$|-errend=L$}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|(23)|u=deg}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|u=deg}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|4.56|u=deg}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|e=3|u=deg}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|(23)|e=3|u=deg}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|e=3|u=deg}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|4.56|e=3|u=deg}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }

  • {{val/sandbox|1234.5678|1.23|4.56|e=3|u=deg|end=$|+errend=U$|-errend=L$}} → -- 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 (e)', ['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 (u) and unit with link (ul) are both specified, only one is allowed.', ['mt-both-up-upl'] = 'unit per (up) and unit per with link (upl) 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 = '' 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 = '' .. 'warning' else body = '' .. 'Error in {{val}}: ' .. msg .. '' end return anchor .. body .. category end

local range_types = { -- No need for ' ' 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.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 = ' ' .. 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.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 = '' .. t .. '' end if flags then flags = flags .. ' ' .. t else flags = t end end end if flags then text = text .. ' • ' .. flags end add(ucode .. ' = ' .. text .. '
') else add(line .. ' ◆ invalid definition
') 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, '' .. v .. '') end if dpart then table.insert(groups, numdot .. (table.remove(dpart, 1) or )) for _, v in ipairs(dpart) do table.insert(groups, '' .. v .. '') 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 '' .. sup .. '
' .. sub .. '
' 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 ) .. '' .. sup_sub(uncU, uncL, options.align) .. '' else if upper.parens then text = '(' .. uncU .. ')' -- old template did not delimit else text = (angle or ) .. '±' .. 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 0×101234 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' .. delimit(e_10.sign, e_10.clean, fmt) .. '' if number.clean then e_text = '×' .. 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 = '' .. result .. '' 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 }


-- 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 = '[[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 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
		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 '.' 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, ',')
		if dpart then
			result = result .. '.' .. 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, '.' .. (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 = 'Val parameter "' .. k .. '=' .. v .. '" is not supported'
			return valerror(warning, nocat, true)
		end
	end
	if not has_ranges and args[4] then
		return valerror('Val parameter 4 ignored', 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('unit (<b>u</b>) and unit with link (<b>ul</b>) are both specified, only one is allowed.', nocat)
	end
	if args.up and args.upl then
		return valerror('unit per (<b>up</b>) and unit per with link (<b>upl</b>) are both specified, only one is allowed.', 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 }
ကလေၚ်သီကေတ်လဝ် နူ "https://mnw.wikipedia.org/w/index.php?title=မဝ်ဂျူ:Val&oldid=13123"