No edit summary
|
use i18n from Module:Val/sandbox added by User:Moroboshi
|
||
(34 intermediate revisions by 6 users not shown) | |||
Line 1: | Line 1: | ||
-- For Template:Val, output a number and optional unit. |
|||
local p = {} |
|||
-- Format options include scientific and uncertainty notations. |
|||
|
|||
local getArgs |
|||
local delimit_groups = require('Module:Gapnum').groups |
|||
local numdot = '.' -- decimal mark (use ',' for Italian) |
|||
function p.main(frame) |
|||
local numsep = ',' -- group separator (use ' ' for Italian) |
|||
if not getArgs then |
|||
local mtext = { |
|||
getArgs = require('Module:Arguments').getArgs |
|||
-- Message and other text that should be localized. |
|||
['mt-bad-exponent'] = 'exponent parameter (<b>e</b>)', |
|||
['mt-parameter'] = 'parameter ', |
|||
['mt-not-number'] = 'is not a valid number', |
|||
['mt-cannot-range'] = 'cannot use a range if the first parameter includes "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 (<b>u</b>) and unit with link (<b>ul</b>) are both specified, only one is allowed.', |
|||
['mt-both-up-upl'] = 'unit per (<b>up</b>) and unit per with link (<b>upl</b>) are both specified, only one is allowed.', |
|||
} |
|||
local 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 |
end |
||
iswarning = false -- problems are infrequent so try showing large error so editor will notice |
|||
local args = getArgs(frame, {wrappers = 'Template:Val'}) |
|||
if iswarning then |
|||
local number = {n=args[1], nend=args['end']} |
|||
body = '<sup class="noprint Inline-Template" style="white-space:nowrap;">' .. |
|||
local nocat = args.nocategory |
|||
'[[Template:Val|<span title="' .. |
|||
|
|||
msg:gsub('"', '"') .. |
|||
-- Error checking |
|||
'">warning</span>]]</sup>' |
|||
if args[1] and not validnumber(args[1]) then |
|||
else |
|||
return valerror('first argument is not a valid number.',nocat) |
|||
body = '<strong class="error">' .. |
|||
'Error in {{[[Template:val|val]]}}: ' .. |
|||
msg .. |
|||
'</strong>' |
|||
end |
end |
||
return anchor .. body .. category |
|||
if args[2] and not validnumber(args[2]) then |
|||
end |
|||
return valerror('second argument is not a valid number.',nocat) |
|||
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 |
end |
||
local result = {} |
|||
if args[3] and not validnumber(args[3]) then |
|||
local range = range_types[arg] |
|||
return valerror('third argument is not a valid number.',nocat) |
|||
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 |
end |
||
if numbers.has_ranges and type(index) == 'number' and (index % 2 == 0) then |
|||
-- Negative third param |
|||
return fail(mtext['mt-should-range']) |
|||
if args[3] and (not mw.ustring.find(args[3],'[−%-]') or mw.ustring.find(args[3],'^%D0$')) then |
|||
return valerror('third argument is not negative.',nocat) |
|||
end |
end |
||
if |
ifindex == 'e' then |
||
local e = numbers[1] and numbers[1].exp |
|||
return valerror('exponent argument (<b>e</b>) is not a valid number.',nocat) |
|||
if e then |
|||
if arg then |
|||
return fail(mtext['mt-cannot-with-e']) |
|||
end |
|||
arg = e |
|||
which = 1 |
|||
end |
|||
end |
end |
||
if |
ifarg and arg ~= '' then |
||
arg = arg:gsub(numsep, '') |
|||
return valerror('unit (<b>u</b>) and units with link (<b>ul</b>) are both specified, only one is allowed.',nocat) |
|||
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 |
end |
||
numbers[index] = result |
|||
if args.up and args.upl then |
|||
return nil -- no error |
|||
return valerror('unit per (<b>up</b>) and units per with link (<b>upl</b>) are both specified, only one is allowed.',nocat) |
|||
end |
|||
-- Group arguments into related categories and unpack when needed |
|||
local uncertainty = {upper=args[2], lower=args[3], |
|||
errend=args.errend, |
|||
upperend=args['+errend'], lowerend=args['-errend']} |
|||
local u_tbl = {u=args.ul or args.u, ul=args.ul ~= nil, |
|||
p=args.upl or args.up, pl=args.upl ~= nil} |
|||
local misc_tbl = {e=args.e, pre=args.p, suf=args.s, fmt=args.fmt or '', nocat=args.nocategory} |
|||
return p._main(number,uncertainty,u_tbl,misc_tbl) |
|||
end |
end |
||
function |
local function get_args(numbers, args) |
||
-- Extract arguments and store the results in numbers. |
|||
-- format number |
|||
-- Return nothing (no error) if ok; otherwise, return an error message. |
|||
local fmt = misc_tbl.fmt |
|||
for index = 1, 99 do |
|||
local n |
|||
local which = index |
|||
if number.n then |
|||
local arg = args[which] -- has been trimmed |
|||
n = delimit(number.n,fmt) |
|||
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 |
end |
||
if numbers.has_ranges and (#numbers % 2 == 0) then |
|||
|
|||
return mtext['mt-need-number'] |
|||
local e_10 = misc_tbl.e |
|||
-- number suffix |
|||
if n and number.nend then |
|||
n = n..number.nend |
|||
end |
end |
||
end |
|||
local function get_scale(text, ucode) |
|||
-- If units are defined, load the unit submodule to create a string |
|||
-- Return the value of text as a number, or throw an error. |
|||
if u_tbl.u then |
|||
-- This supports extremely basic expressions of the form: |
|||
local makeunit = require('Module:Val/units') |
|||
-- a / b |
|||
units = makeunit(u_tbl.u,{ |
|||
-- a ^ b |
|||
link=u_tbl.ul, |
|||
-- where a and b are numbers or 'pi'. |
|||
per=u_tbl.p, |
|||
local n = tonumber(text) |
|||
per_link=u_tbl.pl}) |
|||
if n then |
|||
return n |
|||
end |
end |
||
n = text:gsub('pi', math.pi) |
|||
for _, op in ipairs({ '/', '^' }) do |
|||
-- Uncertainty |
|||
local a, b = n:match('^(.-)' .. op .. '(.*)$') |
|||
local unc |
|||
if a then |
|||
-- Upper and lower |
|||
a = tonumber(a) |
|||
local uncU, uncL = uncertainty.upper, uncertainty.lower |
|||
b = tonumber(b) |
|||
-- Whether or not the entire number needs to be wrapped in parentheses |
|||
if a and b then |
|||
-- true if: |
|||
if op == '/' then |
|||
---- the expontent parameter (e) is defined |
|||
return a / b |
|||
---- AND |
|||
elseif op == '^' then |
|||
---- no lower uncertainty is defined |
|||
return a ^ b |
|||
---- AND |
|||
end |
|||
---- upper uncertainty is defined and contains no parentheses |
|||
local paren_wrap = misc_tbl.e and (not uncL and (uncU and not uncU:find('%('))) |
|||
-- boolean to be defined and used later |
|||
local paren_uncertainty |
|||
-- Upper is always used, so look for it first |
|||
if uncU then |
|||
-- Look for lower uncertainty |
|||
if uncL then |
|||
-- Load the sup/sub module |
|||
local mSu = require('Module:Su')._main |
|||
-- Format upper and lower |
|||
uncU = delimit(uncU,fmt) |
|||
uncL = delimit(uncL,fmt) |
|||
-- If no exponent is defined, and there are units, add them |
|||
if not e_10 and units then |
|||
uncU = uncU..units |
|||
uncL = uncL..units |
|||
end |
end |
||
break |
|||
-- Add the uncertainty suffixes here |
|||
end |
|||
uncU = uncU..(uncertainty.upperend or '') |
|||
end |
|||
uncL = uncL..(uncertainty.lowerend or '') |
|||
error(string.format(mtext['mt-invalid-scale'], ucode, text)) |
|||
end |
|||
local function get_builtin_unit(ucode, definitions) |
|||
unc = '<span style="margin-left:0.3em;">'..mSu(uncU,uncL)..'</span>' |
|||
-- Return table of information for the specified built-in unit, or nil if not known. |
|||
else |
|||
-- Each defined unit code must be followed by two spaces (not tab characters). |
|||
-- Look for parentheses surrounding upper uncertainty |
|||
local _, pos = definitions:find('\n' .. ucode .. ' ', 1, true) |
|||
local uncU_n = mw.ustring.match(uncU,('%((.+)%)')) or uncU |
|||
if pos then |
|||
-- If no parens, use ± |
|||
local endline = definitions:find('%s*\n', pos) |
|||
if uncU == uncU_n then |
|||
if endline then |
|||
unc = '<span style="margin-left:0.3em;margin-right:0.15em">±</span>'..delimit(uncU_n,fmt)..'</span>' |
|||
local result = {} |
|||
-- Otherwise tidy the number and put it back in parentheses |
|||
local n = 0 |
|||
-- Indicate parentheses were used (for later) |
|||
local text = definitions:sub(pos + 1, endline - 1):gsub('%s%s+', '\t') |
|||
else |
|||
for item in (text .. '\t'):gmatch('(%S.-)\t') do |
|||
unc = '('..delimit(uncU_n,fmt)..')' |
|||
if item == 'ALIAS' then |
|||
paren_uncertainty = true |
|||
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 |
end |
||
if result.si then |
|||
-- Add error suffix |
|||
local s = result.symbol |
|||
if uncertainty.errend then |
|||
if ucode == 'mc' .. s or ucode == 'mu' .. s then |
|||
unc = unc..uncertainty.errend |
|||
result.ucode = 'µ' .. s -- unit code for convert should be this |
|||
end |
|||
end |
end |
||
if n >= 2 or (n >= 1 and result.alias) then |
|||
-- Add units if no exponent argument |
|||
return result |
|||
if not e_10 and units then |
|||
unc = unc..units |
|||
end |
end |
||
-- Ignore invalid definition, treating it as a comment. |
|||
end |
end |
||
end |
end |
||
end |
|||
-- Add units if no exponent argument and no parentheses for uncertainty |
|||
if not e_10 and units and not paren_uncertainty then |
|||
local function convert_lookup(ucode, value, scaled_top, want_link, si, options) |
|||
n = (n or '')..units |
|||
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 |
end |
||
local data = mw.loadData(data_module) |
|||
-- If exponent defined, create 10<sup>e</sup> |
|||
local result = options.want_longscale and |
|||
-- Add units if they're defined |
|||
get_builtin_unit(ucode, data.builtin_units_long_scale) or |
|||
if e_10 then |
|||
get_builtin_unit(ucode, data.builtin_units) |
|||
e_10 = '10<sup>'..delimit(misc_tbl.e)..'</sup>' |
|||
local si, use_convert |
|||
if n then |
|||
if result then |
|||
e_10 = '<span style="margin-left:0.25em;margin-right:0.15em">×</span>'..e_10 |
|||
if result.alias then |
|||
ucode = result.symbol |
|||
use_convert = true |
|||
end |
end |
||
if |
ifresult.scale then |
||
-- Setting si means convert will use the unit as given, and the sort key |
|||
e_10 = e_10..units |
|||
-- 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 |
end |
||
else |
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 |
end |
||
return result |
|||
-- Table to concat in order of what goes where |
|||
local ret = |
|||
table.concat({ |
|||
-- prefix |
|||
misc_tbl.pre or '', |
|||
-- opening parenthesis if needed |
|||
paren_wrap and '(' or '', |
|||
-- number |
|||
n, |
|||
-- uncertainties |
|||
unc or '', |
|||
-- closes parenthesis if needed |
|||
paren_wrap and ')' or '', |
|||
-- 10^e if needed |
|||
e_10, |
|||
-- suffix |
|||
misc_tbl.suf or '' |
|||
}) |
|||
return ret |
|||
end |
end |
||
|
local function makeunit(value, options) |
||
-- Return table of information for the requested unit and options, or |
|||
function delimit(n,fmt) |
|||
-- return nil if no unit. |
|||
local prefix,num |
|||
options = options or {} |
|||
if not fmt then fmt = '' end |
|||
local unit |
|||
fmt = fmt:lower() |
|||
local ucode = options.u |
|||
-- look for + or - preceding the number |
|||
local percode = options.per |
|||
if n:find('[-+]') then |
|||
if ucode then |
|||
prefix,num = mw.ustring.match(n,'([-+])([%d.]+)') |
|||
unit = get_unit(ucode, value, nil, options) |
|||
elseif percode then |
|||
unit = { nospace = true, scaled_top = value } |
|||
else |
else |
||
|
return nil |
||
end |
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) |
|||
-- integer and decimal parts of number |
|||
-- Return wikitext to list the built-in units. |
|||
-- if there is no decimal part, delimit_groups only returns 1 table |
|||
-- A unit code should not contain wikimarkup so don't bother escaping. |
|||
local ipart, dpart = delimit_groups(num) |
|||
local data = mw.loadData(data_module) |
|||
-- comma formatting |
|||
local definitions = data.builtin_units .. data.builtin_units_long_scale |
|||
if fmt == 'commas' then |
|||
local last_was_blank = true |
|||
num = table.concat(ipart,',') |
|||
local n = 0 |
|||
if dpart then |
|||
local result = {} |
|||
dpart = table.concat(dpart) |
|||
local function add(line) |
|||
num = num..'.'..dpart |
|||
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 |
||
end |
|||
-- No special formatting |
|||
local si_prefixes = { |
|||
elseif fmt == 'none' then |
|||
-- These are the prefixes recognized by convert; u is accepted for micro. |
|||
-- default to delimiting with .25em spaces |
|||
y = 'y', |
|||
num = table.concat(ipart) |
|||
z = 'z', |
|||
a = 'a', |
|||
f = 'f', |
|||
p = 'p', |
|||
n = 'n', |
|||
u = 'µ', |
|||
['µ'] = 'µ', |
|||
m = 'm', |
|||
c = 'c', |
|||
d = 'd', |
|||
da = 'da', |
|||
h = 'h', |
|||
k = 'k', |
|||
M = 'M', |
|||
G = 'G', |
|||
T = 'T', |
|||
P = 'P', |
|||
E = 'E', |
|||
Z = 'Z', |
|||
Y = 'Y', |
|||
} |
|||
local function is_valid(ucode, unit) |
|||
if unit and not unit.more_ignored then |
|||
assert(type(unit.symbol) == 'string' and unit.symbol ~= '') |
|||
if unit.alias then |
|||
if unit.link or unit.scale_text or unit.si then |
|||
return false |
|||
end |
|||
end |
|||
if unit.si then |
|||
if unit.scale_text then |
|||
return false |
|||
end |
|||
ucode = unit.ucode or ucode |
|||
local base = unit.symbol |
|||
if ucode == base then |
|||
unit.display = base |
|||
return true |
|||
end |
|||
local plen = #ucode - #base |
|||
if plen > 0 then |
|||
local prefix = si_prefixes[ucode:sub(1, plen)] |
|||
if prefix and ucode:sub(plen + 1) == base then |
|||
unit.display = prefix .. base |
|||
return true |
|||
end |
|||
end |
|||
else |
|||
unit.display = unit.symbol |
|||
return true |
|||
end |
|||
end |
|||
return false |
|||
end |
|||
local lookup = require(convert_module)._unit |
|||
local function show_convert(ucode, unit) |
|||
-- If a built-in unit defines a scale or sets the SI flag, any unit defined in |
|||
-- convert is not used (the scale or SI prefix's scale is used for a sort key). |
|||
-- If there is no scale or SI flag, and the unit is not defined in convert, |
|||
-- the sort key may not be correct; this allows such units to be identified. |
|||
if not (unit.si or unit.scale_text) then |
|||
if mode == 'convert' then |
|||
unit.show = not lookup(unit.alias and unit.symbol or ucode).unknown |
|||
unit.show_text = 'CONVERT' |
|||
elseif mode == 'unknown' then |
|||
unit.show = lookup(unit.alias and unit.symbol or ucode).unknown |
|||
unit.show_text = 'UNKNOWN' |
|||
elseif not unit.alias then |
|||
-- Show convert's scale in square brackets ('[1]' for an unknown unit). |
|||
-- Don't show scale for an alias because it's misleading for temperature |
|||
-- and an alias is probably not useful for anything else. |
|||
local scale = lookup(ucode, {value=1, sort='on'}).scaled_value |
|||
if type(scale) == 'number' then |
|||
scale = string.format('%.5g', scale):gsub('e%+?(%-?)0*(%d+)', 'e%1%2') |
|||
else |
|||
scale = '?' |
|||
end |
|||
unit.show = true |
|||
unit.show_text = '[' .. scale .. ']' |
|||
end |
|||
end |
|||
end |
|||
for line in definitions:gmatch('([^\n]*)\n') do |
|||
local pos, _ = line:find(' ', 1, true) |
|||
if pos then |
|||
local ucode = line:sub(1, pos - 1) |
|||
local unit = get_builtin_unit(ucode, '\n' .. line .. '\n') |
|||
if is_valid(ucode, unit) then |
|||
show_convert(ucode, unit) |
|||
local flags, text |
|||
if unit.alias then |
|||
text = unit.symbol |
|||
else |
|||
text = '[[' .. unit.link .. '|' .. unit.display .. ']]' |
|||
end |
|||
if unit.isangle then |
|||
unit.nospace = nil -- don't show redundant flag |
|||
end |
|||
for _, f in ipairs({ |
|||
{ 'alias', 'ALIAS' }, |
|||
{ 'isangle', 'ANGLE' }, |
|||
{ 'nospace', 'NOSPACE' }, |
|||
{ 'si', 'SI' }, |
|||
{ 'scale_text', unit.scale_text }, |
|||
{ 'show', unit.show_text }, |
|||
}) do |
|||
if unit[f[1]] then |
|||
local t = f[2] |
|||
if t:match('^%u+$') then |
|||
t = '<small>' .. t .. '</small>' |
|||
end |
|||
if flags then |
|||
flags = flags .. ' ' .. t |
|||
else |
|||
flags = t |
|||
end |
|||
end |
|||
end |
|||
if flags then |
|||
text = text .. ' • ' .. flags |
|||
end |
|||
add(ucode .. ' = ' .. text .. '<br />') |
|||
else |
|||
add(line .. ' ◆ <b>invalid definition</b><br />') |
|||
end |
|||
else |
|||
add(line) |
|||
end |
|||
end |
|||
return table.concat(result, '\n') |
|||
end |
|||
local delimit_groups = require('Module:Gapnum').groups |
|||
local function delimit(sign, numstr, fmt) |
|||
-- Return sign and numstr (unsigned digits or numdot only) after formatting. |
|||
-- Four-digit integers are not formatted with gaps. |
|||
fmt = (fmt or ''):lower() |
|||
if fmt == 'none' or (fmt == '' and #numstr == 4 and numstr:match('^%d+$')) then |
|||
return sign .. numstr |
|||
end |
|||
-- Group number by integer and decimal parts. |
|||
-- If there is no decimal part, delimit_groups returns only one table. |
|||
local ipart, dpart = delimit_groups(numstr) |
|||
local result |
|||
if fmt == 'commas' then |
|||
result = sign .. table.concat(ipart, numsep) |
|||
if dpart then |
if dpart then |
||
|
result = result .. numdot .. table.concat(dpart) |
||
num = num..'.'..dpart |
|||
end |
end |
||
else |
else |
||
-- Delimit with a small gap by default. |
|||
num = {} |
|||
local groups = {} |
|||
num[1] = table.remove(ipart,1) |
|||
groups[1] = table.remove(ipart, 1) |
|||
for _, v in ipairs(ipart) do |
for _, v in ipairs(ipart) do |
||
table.insert( |
table.insert(groups, '<span style="margin-left:.25em;">' .. v .. '</span>') |
||
end |
end |
||
if dpart then |
if dpart then |
||
table.insert( |
table.insert(groups, numdot .. (table.remove(dpart, 1) or '')) |
||
for _, v in ipairs(dpart) do |
for _, v in ipairs(dpart) do |
||
table.insert( |
table.insert(groups, '<span style="margin-left:.25em;">' .. v .. '</span>') |
||
end |
end |
||
end |
end |
||
|
result = sign .. table.concat(groups) |
||
end |
end |
||
return result |
|||
end |
|||
local function sup_sub(sup, sub, align) |
|||
-- add prefix back if it had one |
|||
-- Return the same result as Module:Su except val defaults to align=right. |
|||
if prefix then |
|||
if align == 'l' or align == 'left' then |
|||
-- change hyphen to proper minus sign |
|||
|
align = 'left' |
||
elseif align == 'c' or align == 'center' then |
|||
prefix = '−' |
|||
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 |
||
num = prefix..num |
|||
end |
end |
||
return text |
|||
end |
|||
local function uncertainty_text(uncertainty, unit_table, options) |
|||
return tostring(num) |
|||
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 |
end |
||
local function _main(values, unit_spec, options) |
|||
-- Specific message for {{Val}} errors |
|||
if options.sandbox then |
|||
function valerror(msg,nocat) |
|||
data_module = data_module .. '/sandbox' |
|||
local ret = mw.html.create('strong') |
|||
convert_module = convert_module .. '/sandbox' |
|||
: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 |
end |
||
local action = options.action |
|||
return tostring(ret) |
|||
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 |
end |
||
local function check_parameters(args, has_ranges, nocat) |
|||
-- true/false whether or not the string is a valid number |
|||
-- Return warning text for the first problem parameter found, or nothing if ok. |
|||
-- ignores parentheses and parity symbolts |
|||
local whitelist = { |
|||
function validnumber(n) |
|||
a = true, |
|||
-- Look for a number that may be surrounded by parentheses or may have +/- |
|||
action = true, |
|||
n = mw.ustring.match(tostring(n),'^%(?[±%-%+]?([%d\.]+)%)?$') |
|||
debug = true, |
|||
return tonumber(n) ~= nil |
|||
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 |
end |
||
return { main = main, _main = _main } |
|||
return p |
This Lua module is used on approximately 39,000 pages and changes may be widely noticed. Test changes in the module's /sandboxor/testcases subpages, or in your own module sandbox. Consider discussing changes on the talk page before implementing them. |
This module implements {{Val}}.
The following modules are developed:
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{{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{{val/sandbox|1234.5678|1.23|4.56|e=3|u=cm|end=$|+errend=U$|-errend=L$}}
→ 1234.5678$+1.23U${{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°{{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°{{val/sandbox|1234.5678|1.23|4.56|e=3|u=deg|end=$|+errend=U$|-errend=L$}}
→ 1234.5678$°+1.23U$°
-- 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 (<b>e</b>)',
['mt-parameter'] = 'parameter ',
['mt-not-number'] = 'is not a valid number',
['mt-cannot-range'] = 'cannot use a range if the first parameter includes "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 (<b>u</b>) and unit with link (<b>ul</b>) are both specified, only one is allowed.',
['mt-both-up-upl'] = 'unit per (<b>up</b>) and unit per with link (<b>upl</b>) are both specified, only one is allowed.',
}
local 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('"', '"') ..
'">warning</span>]]</sup>'
else
body = '<strong class="error">' ..
'Error in {{[[Template:val|val]]}}: ' ..
msg ..
'</strong>'
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.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 = ' ' .. text
end
return { text = text, isangle = unit.isangle, sortkey = sortkey }
end
local function list_units(mode)
-- Return wikitext to list the built-in units.
-- A unit code should not contain wikimarkup so don't bother escaping.
local data = mw.loadData(data_module)
local definitions = data.builtin_units .. data.builtin_units_long_scale
local last_was_blank = true
local n = 0
local result = {}
local function add(line)
if line == '' then
last_was_blank = true
else
if last_was_blank and n > 0 then
n = n + 1
result[n] = ''
end
last_was_blank = false
n = n + 1
result[n] = line
end
end
local si_prefixes = {
-- These are the prefixes recognized by convert; u is accepted for micro.
y = 'y',
z = 'z',
a = 'a',
f = 'f',
p = 'p',
n = 'n',
u = 'µ',
['µ'] = 'µ',
m = 'm',
c = 'c',
d = 'd',
da = 'da',
h = 'h',
k = 'k',
M = 'M',
G = 'G',
T = 'T',
P = 'P',
E = 'E',
Z = 'Z',
Y = 'Y',
}
local function is_valid(ucode, unit)
if unit and not unit.more_ignored then
assert(type(unit.symbol) == 'string' and unit.symbol ~= '')
if unit.alias then
if unit.link or unit.scale_text or unit.si then
return false
end
end
if unit.si then
if unit.scale_text then
return false
end
ucode = unit.ucode or ucode
local base = unit.symbol
if ucode == base then
unit.display = base
return true
end
local plen = #ucode - #base
if plen > 0 then
local prefix = si_prefixes[ucode:sub(1, plen)]
if prefix and ucode:sub(plen + 1) == base then
unit.display = prefix .. base
return true
end
end
else
unit.display = unit.symbol
return true
end
end
return false
end
local lookup = require(convert_module)._unit
local function show_convert(ucode, unit)
-- If a built-in unit defines a scale or sets the SI flag, any unit defined in
-- convert is not used (the scale or SI prefix's scale is used for a sort key).
-- If there is no scale or SI flag, and the unit is not defined in convert,
-- the sort key may not be correct; this allows such units to be identified.
if not (unit.si or unit.scale_text) then
if mode == 'convert' then
unit.show = not lookup(unit.alias and unit.symbol or ucode).unknown
unit.show_text = 'CONVERT'
elseif mode == 'unknown' then
unit.show = lookup(unit.alias and unit.symbol or ucode).unknown
unit.show_text = 'UNKNOWN'
elseif not unit.alias then
-- Show convert's scale in square brackets ('[1]' for an unknown unit).
-- Don't show scale for an alias because it's misleading for temperature
-- and an alias is probably not useful for anything else.
local scale = lookup(ucode, {value=1, sort='on'}).scaled_value
if type(scale) == 'number' then
scale = string.format('%.5g', scale):gsub('e%+?(%-?)0*(%d+)', 'e%1%2')
else
scale = '?'
end
unit.show = true
unit.show_text = '[' .. scale .. ']'
end
end
end
for line in definitions:gmatch('([^\n]*)\n') do
local pos, _ = line:find(' ', 1, true)
if pos then
local ucode = line:sub(1, pos - 1)
local unit = get_builtin_unit(ucode, '\n' .. line .. '\n')
if is_valid(ucode, unit) then
show_convert(ucode, unit)
local flags, text
if unit.alias then
text = unit.symbol
else
text = '[[' .. unit.link .. '|' .. unit.display .. ']]'
end
if unit.isangle then
unit.nospace = nil -- don't show redundant flag
end
for _, f in ipairs({
{ 'alias', 'ALIAS' },
{ 'isangle', 'ANGLE' },
{ 'nospace', 'NOSPACE' },
{ 'si', 'SI' },
{ 'scale_text', unit.scale_text },
{ 'show', unit.show_text },
}) do
if unit[f[1]] then
local t = f[2]
if t:match('^%u+$') then
t = '<small>' .. t .. '</small>'
end
if flags then
flags = flags .. ' ' .. t
else
flags = t
end
end
end
if flags then
text = text .. ' • ' .. flags
end
add(ucode .. ' = ' .. text .. '<br />')
else
add(line .. ' ◆ <b>invalid definition</b><br />')
end
else
add(line)
end
end
return table.concat(result, '\n')
end
local delimit_groups = require('Module:Gapnum').groups
local function delimit(sign, numstr, fmt)
-- Return sign and numstr (unsigned digits or numdot only) after formatting.
-- Four-digit integers are not formatted with gaps.
fmt = (fmt or ''):lower()
if fmt == 'none' or (fmt == '' and #numstr == 4 and numstr:match('^%d+$')) then
return sign .. numstr
end
-- Group number by integer and decimal parts.
-- If there is no decimal part, delimit_groups returns only one table.
local ipart, dpart = delimit_groups(numstr)
local result
if fmt == 'commas' then
result = sign .. table.concat(ipart, numsep)
if dpart then
result = result .. numdot .. table.concat(dpart)
end
else
-- Delimit with a small gap by default.
local groups = {}
groups[1] = table.remove(ipart, 1)
for _, v in ipairs(ipart) do
table.insert(groups, '<span style="margin-left:.25em;">' .. v .. '</span>')
end
if dpart then
table.insert(groups, numdot .. (table.remove(dpart, 1) or ''))
for _, v in ipairs(dpart) do
table.insert(groups, '<span style="margin-left:.25em;">' .. v .. '</span>')
end
end
result = sign .. table.concat(groups)
end
return result
end
local function sup_sub(sup, sub, align)
-- Return the same result as Module:Su except val defaults to align=right.
if align == 'l' or align == 'left' then
align = 'left'
elseif align == 'c' or align == 'center' then
align = 'center'
else
align = 'right'
end
return '<span style="display:inline-block;margin-bottom:-0.3em;vertical-align:-0.4em;line-height:1.2em;font-size:85%;text-align:' ..
align .. ';">' .. sup .. '<br />' .. sub .. '</span>'
end
local function range_text(items, unit_table, options)
local fmt = options.fmt
local nend = items.nend or ''
if items.isrepeat or unit_table.isangle then
nend = nend .. unit_table.text
end
local text = ''
for i = 1, #items do
if i % 2 == 0 then
text = text .. items[i]
else
text = text .. delimit(items[i].sign, items[i].clean, fmt) .. nend
end
end
return text
end
local function uncertainty_text(uncertainty, unit_table, options)
local angle, text, need_parens
if unit_table.isangle then
angle = unit_table.text
end
local upper = uncertainty.upper or {}
local lower = uncertainty.lower or {}
local uncU = upper.clean
if uncU then
local fmt = options.fmt
local uncL = lower.clean
if uncL then
uncU = delimit('+', uncU, fmt) .. (upper.errend or '')
uncL = delimit('−', uncL, fmt) .. (lower.errend or '')
if angle then
uncU = uncU .. angle
uncL = uncL .. angle
end
text = (angle or '') ..
'<span style="margin-left:0.3em;">' ..
sup_sub(uncU, uncL, options.align) ..
'</span>'
else
if upper.parens then
text = '(' .. uncU .. ')' -- old template did not delimit
else
text = (angle or '') ..
'<span style="margin-left:0.3em;margin-right:0.15em;">±</span>' ..
delimit('', uncU, fmt)
need_parens = true
end
if uncertainty.errend then
text = text .. uncertainty.errend
end
if angle then
text = text .. angle
end
end
else
if angle then
text = angle
end
end
return text, need_parens
end
local function _main(values, unit_spec, options)
if options.sandbox then
data_module = data_module .. '/sandbox'
convert_module = convert_module .. '/sandbox'
end
local action = options.action
if action then
if action == 'list' then
-- Kludge: am using the align parameter (a=xxx) for type of list.
return list_units(options.align)
end
return valerror('invalid action "' .. action .. '".', options.nocat)
end
local number = values.number or (values.numbers and values.numbers[1]) or {}
local e_10 = options.e or {}
local novalue = (number.value == nil and e_10.clean == nil)
local fmt = options.fmt
local want_sort = true
local sortable = options.sortable
if sortable == 'off' or (sortable == nil and novalue) then
want_sort = false
elseif sortable == 'debug' then
-- Same as sortable = 'on' but the sort key is displayed.
else
sortable = 'on'
end
local sort_value = 1
if want_sort then
sort_value = number.value or 1
if e_10.value and sort_value ~= 0 then
-- The 'if' avoids {{val|0|e=1234}} giving an invalid sort_value due to overflow.
sort_value = sort_value * 10^e_10.value
end
end
local unit_table = makeunit(sort_value, {
u = unit_spec.u,
want_link = unit_spec.want_link,
per = unit_spec.per,
want_per_link = unit_spec.want_per_link,
nospace = novalue,
want_longscale = unit_spec.want_longscale,
sortable = sortable,
})
local sortkey
if unit_table then
if want_sort then
sortkey = unit_table.sortkey
end
else
unit_table = { text = '' }
if want_sort then
sortkey = convert_lookup('dummy', sort_value, nil, nil, nil, { sortable = sortable }).sortspan
end
end
local final_unit = unit_table.isangle and '' or unit_table.text
local e_text, n_text, need_parens
local uncertainty = values.uncertainty
if uncertainty then
if number.clean then
n_text = delimit(number.sign, number.clean, fmt) .. (number.nend or '')
local text
text, need_parens = uncertainty_text(uncertainty, unit_table, options)
if text then
n_text = n_text .. text
end
else
n_text = ''
end
else
if values.numbers.isrepeat then
final_unit = ''
end
n_text = range_text(values.numbers, unit_table, options)
need_parens = true
end
if e_10.clean then
if need_parens then
n_text = '(' .. n_text .. ')'
end
e_text = '10<sup>' .. delimit(e_10.sign, e_10.clean, fmt) .. '</sup>'
if number.clean then
e_text = '<span style="margin-left:0.25em;margin-right:0.15em;">×</span>' .. e_text
end
else
e_text = ''
end
local result =
(sortkey or '') ..
(options.prefix or '') ..
n_text ..
e_text ..
final_unit ..
(options.suffix or '')
if result ~= '' then
result = '<span class="nowrap">' .. result .. '</span>'
end
return result .. (options.warning or '')
end
local function check_parameters(args, has_ranges, nocat)
-- Return warning text for the first problem parameter found, or nothing if ok.
local whitelist = {
a = true,
action = true,
debug = true,
e = true,
['end'] = true,
errend = true,
['+errend'] = true,
['-errend'] = true,
fmt = true,
['long scale'] = true,
long_scale = true,
longscale = true,
nocategory = true,
p = true,
s = true,
sortable = true,
u = true,
ul = true,
up = true,
upl = true,
}
for k, v in pairs(args) do
if type(k) == 'string' and not whitelist[k] then
local warning = 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 }