-- 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 =
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 .. categoryendlocal range_types = local range_repeat_unit =
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
'number' and (index % 2
2 then if numbers[1] and numbers[1].exp then return fail(mtext['mt-cannot-range']) end numbers.has_ranges = true elseif not numbers.has_ranges then return fail(mtext['mt-need-range']) 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)
0) then return fail(mtext['mt-should-range']) end if index
'(' and arg:sub(-1)
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 -- Display for first number, ignore for others. if index
'+' then propersign = '+' elseif prefix
minus then propersign = minus isnegative = true elseif prefix ~= "" then -- ignore if prefix is empty return fail end result.clean = arg result.sign = propersign or result.value = isnegative and -value or value end numbers[index] = result return nil -- no errorend
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
0) then return mtext['mt-need-number'] endend
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 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
'ANGLE' then result.isangle = true result.nospace = true elseif item
'SI' then result.si = true else n = n + 1 if n
2 then result.link = item elseif n
'mc' .. s or ucode
local function convert_lookup(ucode, value, scaled_top, want_link, si, options) local lookup = require(convert_module)._unit return lookup(ucode,)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 = value = value * result.scale end if result.si then ucode = result.ucode or ucode si = 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 resultend
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 = 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 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
'string' and unit.symbol ~= ) if unit.alias and (unit.link or unit.scale_text or unit.si) then return false elseif 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 = 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
'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,).scaled_value if type(scale)
local delimit_groups = require('Module:Gapnum').groupslocal 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
and #numstr
'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 resultendlocal function sup_sub(sup, sub, align) -- Return the same result as Module:Su except val defaults to align=right. if align
'left' then align = 'left' elseif align
'center' then align = 'center' else align = 'right' end return '
' .. sup .. '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
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_parensendlocal 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
nil and e_10.clean
'off' or (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 giving an invalid sort_value due to overflow. sort_value = sort_value * 10^e_10.value end end local unit_table = makeunit(sort_value,) local sortkey if unit_table then if want_sort then sortkey = unit_table.sortkey end else unit_table = if want_sort then sortkey = convert_lookup('dummy', sort_value, nil, nil, nil,).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 )endlocal function check_parameters(args, has_ranges, nocat) -- Return warning text for the first problem parameter found, or nothing if ok. local whitelist = for k, v in pairs(args) do if type(k)
local function main(frame) local getArgs = require('Module:Arguments').getArgs local args = getArgs(frame,) 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 = 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] = end end end setfield(1, 'nend', 'end') setfield(2, 'errend', '+errend') setfield(3, 'errend', '-errend') values = end local unit_spec = local options = return _main(values, unit_spec, options)end
return