Module:Val: Difference between revisions
From Zoophilia Wiki
Jump to navigationJump to search
meta>Johnuniq major refactor and fixes to match template's output |
meta>Johnuniq refactor number handling and use a loop for validation; handle sortkey (more to do); use long_scale parameter |
||
| Line 1: | Line 1: | ||
local delimit_groups = require('Module:Gapnum').groups | local delimit_groups = require('Module:Gapnum').groups | ||
| Line 18: | Line 16: | ||
end | end | ||
-- true | local function extract_number(index, numbers, args) | ||
-- | -- Extract number from args[index] and store result in numbers[index] | ||
local | -- 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.]+)$') | |||
if arg:sub(1, 1) == '.' then | |||
arg = '0' .. arg | |||
end | |||
local value = tonumber(arg) | |||
if not value then | |||
return false | |||
end | |||
if prefix == '' or prefix == '±' then | |||
-- Ignore. | |||
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 | end | ||
| Line 109: | Line 141: | ||
end | end | ||
local function delimit(numstr, fmt) | |||
local function delimit( | -- Return numstr (unsigned digits or '.' only) after formatting. | ||
local result | |||
fmt = (fmt or ''):lower() | |||
fmt = fmt | -- 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) | |||
-- integer and decimal parts | |||
-- | |||
local ipart, dpart = delimit_groups( | |||
if fmt == 'commas' then | if fmt == 'commas' then | ||
result = table.concat(ipart, ',') | |||
if dpart then | if dpart then | ||
result = result .. '.' .. table.concat(dpart) | |||
end | end | ||
elseif fmt == 'none' then | elseif fmt == 'none' then | ||
result = numstr | |||
else | else | ||
-- Delimit with a small gap by default. | -- Delimit with a small gap by default. | ||
local groups = {} | |||
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, '.' .. table.remove(dpart, 1)) | ||
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 = table.concat(groups) | |||
-- LATER Is the following needed? | |||
-- It is for compatibility with {{val}} which uses {{val/delimitnum}}. | |||
result = '<span style="white-space:nowrap">' .. result .. '</span>' | |||
end | end | ||
return result | |||
return | |||
end | end | ||
| Line 170: | Line 181: | ||
-- Unit | -- Unit | ||
local want_sort = not (misc_tbl.sortable == 'off') | |||
local unit_table, sortkey | local unit_table, sortkey | ||
local sort_value = want_sort and ((number.value or 1) * (e_10.value and 10^e_10.value or 1)) or 1 | |||
if unit_spec.u then | if unit_spec.u then | ||
unit_table = makeunit(unit_spec.u, { | unit_table = makeunit(unit_spec.u, { | ||
| Line 176: | Line 189: | ||
per = unit_spec.per, | per = unit_spec.per, | ||
per_link = unit_spec.per_link, | per_link = unit_spec.per_link, | ||
longscale = unit_spec.longscale, | |||
value = sort_value, | |||
}) | }) | ||
sortkey = unit_table.sortkey | if want_sort then | ||
sortkey = unit_table.sortkey | |||
sortkey = convert_lookup('dummy', { value = | end | ||
elseif want_sort then | |||
sortkey = convert_lookup('dummy', { value = sort_value }).sortkey | |||
end | |||
if sortkey then | |||
-- TODO convert should return sortkey in span so following is not needed. | |||
sortkey = '<span style="display:none" class="sortkey">' .. sortkey .. '</span>' | |||
end | end | ||
-- Uncertainty | -- Uncertainty | ||
local unc_text | local unc_text | ||
local uncU | local uncU = uncertainty.upper.clean | ||
local uncL = uncertainty.lower.clean | |||
local | |||
if uncU then | if uncU then | ||
if uncL then | if uncL then | ||
local mSu = require('Module:Su')._main -- sup/sub format | local mSu = require('Module:Su')._main -- sup/sub format | ||
uncU = '+' .. delimit(uncU, fmt) .. (uncertainty. | uncU = '+' .. delimit(uncU, fmt) .. (uncertainty.upper.errend or '') | ||
uncL = '−' .. delimit(uncL, fmt) .. (uncertainty. | uncL = '−' .. delimit(uncL, fmt) .. (uncertainty.lower.errend or '') | ||
if unit_table and unit_table.isangle then | if unit_table and unit_table.isangle then | ||
uncU = uncU .. unit_table.text | uncU = uncU .. unit_table.text | ||
| Line 204: | Line 218: | ||
unc_text = '<span style="margin-left:0.3em;">' .. mSu(uncU, uncL) .. '</span>' | unc_text = '<span style="margin-left:0.3em;">' .. mSu(uncU, uncL) .. '</span>' | ||
else | else | ||
if uncertainty.upper.parens then | |||
unc_text = '(' .. uncU .. ')' -- template does not delimit | |||
unc_text = '(' .. | |||
else | else | ||
unc_text = '<span style="margin-left:0.3em;margin-right:0.15em">±</span>' .. delimit(uncU, fmt) | unc_text = '<span style="margin-left:0.3em;margin-right:0.15em">±</span>' .. delimit(uncU, fmt) | ||
end | end | ||
if uncertainty.errend then | if uncertainty.errend then | ||
| Line 220: | Line 232: | ||
end | end | ||
local e_text, n_text | local e_text, n_text | ||
if number. | if number.clean then | ||
n_text = delimit(number. | n_text = number.sign .. delimit(number.clean, fmt) .. (number.nend or '') | ||
if not | if not uncertainty.upper.parens and unit_table and unit_table.isangle then | ||
n_text = n_text .. unit_table.text | n_text = n_text .. unit_table.text | ||
end | end | ||
else | else | ||
n_text = '' | n_text = '' | ||
e_10 = e_10 | if not e_10.clean then | ||
e_10.clean = '0' | |||
e_10.sign = '' | |||
end | |||
end | end | ||
if e_10 then | if e_10.clean then | ||
e_text = '10<sup>' .. delimit(e_10) .. '</sup>' | e_text = '10<sup>' .. e_10.sign .. delimit(e_10.clean, fmt) .. '</sup>' | ||
if number. | if number.clean then | ||
e_text = '<span style="margin-left:0.25em;margin-right:0.15em">×</span>' .. e_text | e_text = '<span style="margin-left:0.25em;margin-right:0.15em">×</span>' .. e_text | ||
end | end | ||
| Line 237: | Line 252: | ||
e_text = '' | e_text = '' | ||
end | end | ||
local paren_wrap = e_10.clean and uncU and not uncertainty.upper.parens and not uncL -- TODO should this be before e_10.clean = '0' above? | |||
return table.concat({ | return table.concat({ | ||
'<span class="nowrap">', | '<span class="nowrap">', | ||
sortkey or '', | |||
misc_tbl.prefix or '', | misc_tbl.prefix or '', | ||
paren_wrap and '(' or '', | paren_wrap and '(' or '', | ||
| Line 252: | Line 269: | ||
local function main(frame) | local function main(frame) | ||
local getArgs = require('Module:Arguments').getArgs | |||
local args = getArgs(frame, {wrappers = { 'Template:Val', 'Template:Val/sandboxlua' }}) | local args = getArgs(frame, {wrappers = { 'Template:Val', 'Template:Val/sandboxlua' }}) | ||
local nocat = args.nocategory | 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) | |||
if | end | ||
end | end | ||
if args.u and args.ul then | 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) | return valerror('unit (<b>u</b>) and unit with link (<b>ul</b>) are both specified, only one is allowed.', nocat) | ||
end | end | ||
if args.up and args.upl then | 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) | return valerror('unit per (<b>up</b>) and unit per with link (<b>upl</b>) are both specified, only one is allowed.', nocat) | ||
end | end | ||
local number = numbers[1] | |||
local uncertainty = { | local uncertainty = { | ||
upper = | upper = numbers[2], | ||
lower = | lower = numbers[3], | ||
errend = args.errend, | errend = args.errend, | ||
} | } | ||
local unit_spec = { | local unit_spec = { | ||
| Line 292: | Line 302: | ||
per = args.upl or args.up, | per = args.upl or args.up, | ||
per_link = args.upl ~= nil, | per_link = args.upl ~= nil, | ||
longscale = (args.longscale or args.long_scale or args['long scale']) == 'on', | |||
} | } | ||
local misc_tbl = { | local misc_tbl = { | ||
e = | e = numbers.e, | ||
prefix = args.p, | prefix = args.p, | ||
suffix = args.s, | suffix = args.s, | ||
fmt = args.fmt or '', | fmt = args.fmt or '', | ||
nocat = args.nocategory, | 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) | return _main(number, uncertainty, unit_spec, misc_tbl) | ||
end | end | ||
return { main = main, _main = _main } | return { main = main, _main = _main } | ||
Revision as of 11:01, 15 July 2015
Documentation for this module may be created at Module:Val/doc
local delimit_groups = require('Module:Gapnum').groups
-- Specific message for {{Val}} errors
local function valerror(msg, nocat)
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.]+)$')
if arg:sub(1, 1) == '.' then
arg = '0' .. arg
end
local value = tonumber(arg)
if not value then
return false
end
if prefix == '' or prefix == '±' then
-- Ignore.
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_builtin_unit(unitcode, 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' .. unitcode .. ' ', 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)
for item in (text .. ' '):gmatch('(%S[^\n]-)%s%s') do
if item == 'NOSPACE' then
result.nospace = true
elseif item == 'ANGLE' then
result.isangle = true
result.nospace = true
else
n = n + 1
if n == 1 then
result.symbol = item
elseif n == 2 then
result.link = item
else
break
end
end
end
if n == 2 then
return result
end
-- Ignore invalid definition, treating it as a comment.
end
end
end
local function convert_lookup(ucode, options)
local lookup = require('Module:Convert/sandbox')._unit
return lookup(ucode, { value = options.value, link = options.want_link })
end
local function get_unit(ucode, value, want_link, want_longscale)
local data = mw.loadData('Module:Val/units')
local result = want_longscale and
get_builtin_unit(ucode, data.builtin_units_long_scale) or
get_builtin_unit(ucode, data.builtin_units)
local convert_unit = convert_lookup(ucode, { value = value, link = want_link })
if result then
-- Have: result.symbol + result.link + result.isangle + result.nospace
if want_link then
result.text = '[[' .. result.link .. '|' .. result.symbol .. ']]'
else
result.text = result.symbol
end
result.sortkey = convert_unit.sortkey
else
result = {
text = convert_unit.text,
sortkey = convert_unit.sortkey,
}
end
return result
end
local function makeunit(ucode, options)
-- Return wikitext, sortkey for the requested unit and options.
-- TODO The sortkey does not account for any per unit.
local function bracketed(ucode, text)
return ucode:find('[*./]') and '(' .. text .. ')' or text
end
options = options or {}
local unit = get_unit(ucode, options.value, options.link, options.longscale)
local text = unit.text
local percode = options.per
if percode then
local perunit = get_unit(percode, 0, options.per_link, options.longscale)
text = bracketed(ucode, text) .. '/' .. bracketed(percode, perunit.text)
end
if not unit.nospace then
text = ' ' .. text
end
return { text = text, isangle = unit.isangle, sortkey = unit.sortkey }
end
local function delimit(numstr, fmt)
-- Return numstr (unsigned digits or '.' only) after formatting.
local result
fmt = (fmt or ''):lower()
-- 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)
if fmt == 'commas' then
result = table.concat(ipart, ',')
if dpart then
result = result .. '.' .. table.concat(dpart)
end
elseif fmt == 'none' then
result = numstr
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">' .. result .. '</span>'
end
return result
end
local function _main(number, uncertainty, unit_spec, misc_tbl)
local e_10 = misc_tbl.e
local fmt = misc_tbl.fmt
-- Unit
local want_sort = not (misc_tbl.sortable == 'off')
local unit_table, sortkey
local sort_value = want_sort and ((number.value or 1) * (e_10.value and 10^e_10.value or 1)) or 1
if unit_spec.u then
unit_table = makeunit(unit_spec.u, {
link = unit_spec.link,
per = unit_spec.per,
per_link = unit_spec.per_link,
longscale = unit_spec.longscale,
value = sort_value,
})
if want_sort then
sortkey = unit_table.sortkey
end
elseif want_sort then
sortkey = convert_lookup('dummy', { value = sort_value }).sortkey
end
if sortkey then
-- TODO convert should return sortkey in span so following is not needed.
sortkey = '<span style="display:none" class="sortkey">' .. sortkey .. '</span>'
end
-- Uncertainty
local unc_text
local uncU = uncertainty.upper.clean
local uncL = uncertainty.lower.clean
if uncU then
if uncL then
local mSu = require('Module:Su')._main -- sup/sub format
uncU = '+' .. delimit(uncU, fmt) .. (uncertainty.upper.errend or '')
uncL = '−' .. delimit(uncL, fmt) .. (uncertainty.lower.errend or '')
if unit_table and unit_table.isangle then
uncU = uncU .. unit_table.text
uncL = uncL .. unit_table.text
end
unc_text = '<span style="margin-left:0.3em;">' .. mSu(uncU, uncL) .. '</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)
end
if uncertainty.errend then
unc_text = unc_text .. uncertainty.errend
end
if unit_table and 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 = number.sign .. delimit(number.clean, fmt) .. (number.nend or '')
if not uncertainty.upper.parens and unit_table 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>' .. e_10.sign .. delimit(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 paren_wrap = e_10.clean and uncU and not uncertainty.upper.parens and not uncL -- TODO should this be before e_10.clean = '0' above?
return table.concat({
'<span class="nowrap">',
sortkey or '',
misc_tbl.prefix or '',
paren_wrap and '(' or '',
n_text,
unc_text or '',
paren_wrap and ')' or '',
e_text,
(unit_table and not unit_table.isangle) and unit_table.text or '',
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,
link = args.ul ~= nil,
per = args.upl or args.up,
per_link = args.upl ~= nil,
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 '',
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 }