Module:Val
Appearance
This module implements {{Val}}.
The following modules are developed:
- Module:Val • Main module.
- Module:Val/units • Definitions for units built-in to val.
Use {{val/sandbox}} for testing, for example:
{{val/sandbox|1234.5678|(23)|u=cm}}
→ 1234.5678(23) cm{{val/sandbox|1234.5678|1.23|u=cm}}
→ 1234.5678±1.23 cm{{val/sandbox|1234.5678|1.23|4.56|u=cm}}
→ 1234.5678+1.23
−4.56 cm{{val/sandbox|1234.5678|e=3|u=cm}}
→ 1234.5678×103 cm{{val/sandbox|1234.5678|(23)|e=3|u=cm}}
→ 1234.5678(23)×103 cm{{val/sandbox|1234.5678|1.23|e=3|u=cm}}
→ (1234.5678±1.23)×103 cm{{val/sandbox|1234.5678|1.23|4.56|e=3|u=cm}}
→ 1234.5678+1.23
−4.56×103 cm{{val/sandbox|1234.5678|1.23|4.56|e=3|u=cm|end=$|+errend=U$|-errend=L$}}
→ 1234.5678$+1.23U$
−4.56L$×103 cm{{val/sandbox|1234.5678|(23)|u=deg}}
→ 1234.5678(23)°{{val/sandbox|1234.5678|1.23|u=deg}}
→ 1234.5678°±1.23°{{val/sandbox|1234.5678|1.23|4.56|u=deg}}
→ 1234.5678°+1.23°
−4.56°{{val/sandbox|1234.5678|e=3|u=deg}}
→ 1234.5678°×103{{val/sandbox|1234.5678|(23)|e=3|u=deg}}
→ 1234.5678(23)°×103{{val/sandbox|1234.5678|1.23|e=3|u=deg}}
→ (1234.5678°±1.23°)×103{{val/sandbox|1234.5678|1.23|4.56|e=3|u=deg}}
→ 1234.5678°+1.23°
−4.56°×103{{val/sandbox|1234.5678|1.23|4.56|e=3|u=deg|end=$|+errend=U$|-errend=L$}}
→ 1234.5678$°+1.23U$°
−4.56L$°×103
-- For Template:Val, output a number and optional unit.
-- Format options include scientific and uncertainty notations.
local delimit_groups = require('Module:Gapnum').groups
local function valerror(msg, nocat)
-- Return formatted message for {{val}} errors.
if is_test_run then -- LATER remove
return 'Error: "' .. msg .. '"'
end
local ret = mw.html.create('strong')
:addClass('error')
:wikitext('Error in {{Val}}: ' .. msg)
-- Not in talk, user, user_talk, or wikipedia_talk
if not nocat and not mw.title.getCurrentTitle():inNamespaces(1,2,3,5) then
ret:wikitext('[[Category:Pages with incorrect formatting templates use]]')
end
return tostring(ret)
end
local function extract_number(index, numbers, args)
-- Extract number from args[index] and store result in numbers[index]
-- and return true if no argument or if argument is valid.
-- The result is a table which is empty if there was no specified number.
-- Input like 1e3 is regarded as invalid; should use e=3 parameter.
-- Input commas are removed so 1,234 is the same as 1234.
local result = {}
local arg = args[index] -- has been trimmed
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 minus = '−'
local isnegative, propersign, prefix
prefix, arg = arg:match('^(.-)([%d.]+)$')
local value = tonumber(arg)
if not value then
return false
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 false
end
result.clean = arg
result.sign = propersign or ''
result.value = isnegative and -value or value
end
numbers[index] = result
return true
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('\n', pos, true)
if endline then
local result = {}
local n = 0
local text = definitions:sub(pos, 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
result.symbol = item
elseif n == 2 then
result.link = item
elseif n == 3 then
result.scale = get_scale(item, ucode)
else
break
end
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('Module:Convert/sandbox')._unit
return lookup(ucode, {
value = value,
scaled_top = scaled_top,
link = want_link,
si = si,
sort = options.want_sort,
})
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('Module:Val/units')
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_result
if result then
use_result = true
if result.alias then
ucode = result.symbol
use_result = false
end
if result.scale then
value = value * result.scale
end
if result.si then
si = { result.symbol, result.link }
use_result = false
end
end
local convert_unit = convert_lookup(ucode, value, scaled_top, want_link, si, options)
if use_result then
if want_link then
result.text = '[[' .. result.link .. '|' .. result.symbol .. ']]'
else
result.text = result.symbol
end
result.sortkey = convert_unit.sortspan
result.scaled_top = value
else
result = {
text = convert_unit.text,
sortkey = convert_unit.sortspan,
scaled_top = convert_unit.scaled_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 then
text = ' ' .. text
end
return { text = text, isangle = unit.isangle, sortkey = sortkey }
end
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 ~= 'commas' 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))
for _, v in ipairs(dpart) do
table.insert(groups, '<span style="margin-left:.25em">' .. v .. '</span>')
end
end
result = table.concat(groups)
-- LATER Is the following needed?
-- It is for compatibility with {{val}} which uses {{val/delimitnum}}.
result = '<span style="white-space:nowrap">' .. sign .. result .. '</span>'
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 _main(number, uncertainty, unit_spec, misc_tbl)
local e_10 = misc_tbl.e
local fmt = misc_tbl.fmt
-- Unit.
local sortkey
local want_sort = not (misc_tbl.sortable == 'off')
local sort_value = want_sort and ((number.value or 1) * (e_10.value and 10^e_10.value or 1)) or 1
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,
want_longscale = unit_spec.want_longscale,
want_sort = want_sort,
})
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, { want_sort = true }).sortspan
end
end
-- Uncertainty.
local unc_text
local paren_left, paren_right = '', ''
local uncU = uncertainty.upper.clean
if uncU and number.clean then
-- Cannot enter an uncertainty without a preceding number, however, if it were
-- possible, the uncertainty should be ignored to avoid displaying junk.
local uncL = uncertainty.lower.clean
if uncL then
uncU = delimit('+', uncU, fmt) .. (uncertainty.upper.errend or '')
uncL = delimit('−', uncL, fmt) .. (uncertainty.lower.errend or '')
if unit_table.isangle then
uncU = uncU .. unit_table.text
uncL = uncL .. unit_table.text
end
unc_text = '<span style="margin-left:0.3em;">' .. sup_sub(uncU, uncL, misc_tbl.align) .. '</span>'
else
if uncertainty.upper.parens then
unc_text = '(' .. uncU .. ')' -- template does not delimit
else
unc_text = '<span style="margin-left:0.3em;margin-right:0.15em">±</span>' .. delimit('', uncU, fmt)
if e_10.clean then
paren_left = '('
paren_right = ')'
end
end
if uncertainty.errend then
unc_text = unc_text .. uncertainty.errend
end
if unit_table.isangle then
unc_text = unc_text .. unit_table.text
end
end
end
local e_text, n_text
if number.clean then
n_text = delimit(number.sign, number.clean, fmt) .. (number.nend or '')
if not uncertainty.upper.parens and unit_table.isangle then
n_text = n_text .. unit_table.text
end
else
n_text = ''
if not e_10.clean then
e_10.clean = '0'
e_10.sign = ''
end
end
if e_10.clean then
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
return table.concat({
'<span class="nowrap">',
sortkey or '',
misc_tbl.prefix or '',
paren_left,
n_text,
unc_text or '',
paren_right,
e_text,
unit_table.isangle and '' or unit_table.text,
misc_tbl.suffix or '',
'</span>'
})
end
local function main(frame)
local getArgs = require('Module:Arguments').getArgs
local args = getArgs(frame, {wrappers = { 'Template:Val', 'Template:Val/sandboxlua' }})
local nocat = args.nocategory
local numbers = {}
local checks = {
-- index, description
{ 1, 'first parameter' },
{ 2, 'second parameter' },
{ 3, 'third parameter' },
{ 'e', 'exponent parameter (<b>e</b>)' },
}
for _, item in ipairs(checks) do
if not extract_number(item[1], numbers, args) then
return valerror(item[2] .. ' is not a valid number.', nocat)
end
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 number = numbers[1]
local uncertainty = {
upper = numbers[2],
lower = numbers[3],
errend = args.errend,
}
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 misc_tbl = {
e = numbers.e,
prefix = args.p,
suffix = args.s,
fmt = args.fmt or '',
align = args.a,
nocat = args.nocategory,
sortable = args.sortable,
}
number.nend = args['end']
uncertainty.upper.errend = args['+errend']
uncertainty.lower.errend = args['-errend']
return _main(number, uncertainty, unit_spec, misc_tbl)
end
return { main = main, _main = _main }