Module:Convert/sandbox
![]() | This is the module sandbox page for Module:Convert (diff). See also the companion subpage for test cases. |
![]() | See Template:Convert/Transwiki guide for help copying this module and modifying it for use on another wiki. |
![]() | This Lua module is used on approximately 1,330,000 pages, or roughly 2% of all pages. To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them. |
![]() | This module is rated as ready for general use. It has reached a mature form and is thought to be relatively bug-free and ready for use wherever appropriate. It is ready to mention on help pages and other Wikipedia resources as an option for new users to learn. To reduce server load and bad output, it should be improved by sandbox testing rather than repeated trial-and-error editing. |
![]() | This module is subject to page protection. It is a highly visible module in use by a very large number of pages, or is substituted very frequently. Because vandalism or mistakes would affect many pages, and even trivial editing might cause substantial load on the servers, it is protected from editing. |
![]() | This module can only be edited by administrators because it is transcluded onto one or more cascade-protected pages. |
![]() | This module depends on the following other modules: |
![]() | This module uses TemplateStyles: |
This module converts a value from one unit of measurement to another. For example:
{{convert|123|lb|kg}}
→ 123 pounds (56 kg)
The module is called using a template—parameters passed to the template are used by this module to control how a conversion is performed. For example, units can be abbreviated (like kg
), or displayed as names (like kilogram
), and the output value can be rounded to a specified precision. For usage information, see Help:Convert.
Templates and modules
Templates that invoke this module are:
The following modules are required:
- Module:Convert – (this module) code to convert units
- Module:Convert/data – unit definitions
- Module:Convert/text – text messages, and parameter names and values
The following modules are optional and are used only if required and if the module exists:
- Module:Convert/extra – extra (temporary) unit definitions; used if a unit is not found in Module:Convert/data
- Module:ConvertNumeric – code to spell an input value in words (only English is supported; however, see vi:Module:ConvertNumeric)
For Wikidata support the following modules are required:
The following help pages are available:
- Help:Convert – overview
- Help:Convert messages – describes error and warning messages; messages link to this page so it is required when the module is copied to another wiki
- Help:Convert units – overview of units
A page containing a convert error is added to the following hidden category, providing the page is in a specified namespace (articles, by default):
Units are defined in the wikitext of the master list of units.
- Module:Convert/documentation/conversion data – master list of unit definitions
- Module:Convert/makeunits – translates wikitext from the master list to Lua
- Module talk:Convert/makeunits – makeunits results; copy the text to Module:Convert/data
Module:Convert/data is transcluded into every page using the convert module, so experimenting with a new unit in that module would involve a significant overhead. The Module:Convert/extra module is an alternative which is only transcluded on pages with a unit that is not defined in the main data module.
Module talk:Convert/show lists all unit links so they can be checked.
Sandbox
When making a change, copy the current modules to the sandbox pages, then edit the sandbox copies:
- Module:Convert • Module:Convert/sandbox • different (diff)
- Module:Convert/data • Module:Convert/data/sandbox • same content
- Module:Convert/text • Module:Convert/text/sandbox • same content
- Module:Convert/extra • Module:Convert/extra/sandbox • same content
- Module:Convert/wikidata • Module:Convert/wikidata/sandbox • same content
- Module:Convert/wikidata/data • Module:Convert/wikidata/data/sandbox • same content
Use the following template to test the results (example {{convert/sandbox|123|lb|kg}}
):
Template:Convert/sandbox invokes Module:Convert/sandbox with parameter |sandbox=sandbox
which causes convert to use the sandbox modules rather than the normal modules.
The following should be used to test the results of editing the convert modules.
- Template:Convert/testcases#Sandbox testcases – links to testcases
- Module:Convert/tester – module to run tests by comparing template output with fixed text
It is not necessary to save a testcases page before viewing test results. For example, Template:Convert/testcases/sandbox4 could be edited to change the tests. While still editing that page, paste "Template talk:Convert/testcases/sandbox4
" (without quotes) into the page title box under "Preview page with this template", then click "Show preview".
Configuration
The template that invokes this module can define options to configure the module. For example:
{{#invoke:convert|convert|numdot=,|numsep=.}}
- Sets the decimal mark to be a comma, and the thousands separator to be a dot.
Other options, with default values, are:
|maxsigfig=14
– maximum number of significant figures|nscat=0
– namespaces (comma separated) in which an error or warning adds a category to the page|warnings=0
– 0 (zero) disables warnings; 1 shows important warnings; 2 shows all warnings
An option in the template can specify that the sandbox versions of the modules be used. If specified, the text on the right-hand side of the equals sign must be the name of the subpage for each sandbox module.
|sandbox=sandbox
– omit for normal operation
All text used for input parameters and for output messages and categories can be customized. For example, at enwiki the option |lk=on
can be used to link each displayed unit to its article. The "lk
" and "on
" can be replaced with any desired text. In addition, input and output numbers can be formatted and can use digits in the local language. See the translation guide for more information.
To do
Document the modules to access Wikidata!
Module version history
- Version 1 December 2013
- Version 2 January 2014
- Version 3 April 2014
- Version 4 July 2014
- Version 5 September 2014
- Version 6 November 2014
- Version 7 December 2014
- Version 8 February 2015
- Version 9 February 2015
- Version 10 May 2015
- Version 11 June 2015
- Version 12 August 2015
- Version 13 March 2016
- Version 14 June 2016 (introduced handling of Wikidata)
- Version 15 September 2016
- Version 16 January 2017
- Version 17 May 2017
- Version 18 July 2017
- Version 19 August 2017
- Version 20 December 2017 (changed symbols for dot and micro)
- Version 21 January 2018 (remove many deprecated options)
- Version 22 February 2018 (many unit link changes)
- Version 23 June 2018 (warnings for ignored numbered parameters; adj=pre/disp=preunit changes; currency units removed)
- Version 24 May 2019 (hidden sort key uses data-sort-value; avoid using the extra data module)
- Version 25 May 2021 (use templatestyles Template:Fraction/styles.css or Template:Sfrac/styles.css for fractions)
- Version 26 June 2021 (many unit link changes)
- Version 27 February 2022 (enhance Mach parameters; use spaced en dash when needed; unit tweaks)
- Version 28 April 2023 (new SI prefixes; add disp=semicolon; unit adjustments including fixing scales of mpge and BTU/lb)
- Version 29 May 2023 (liter/litre fix to show symbol 'L' rather than 'l' per MOS)
- Version 30 October 2024 (units can use convertPlural in languages with more than one plural form and can have a pername; E3km displays "thousand" even with abbr=on)
-- Convert a value from one unit of measurement to another.
-- Example: {{convert|123|lb|kg}} --> 123 pounds (56 kg)
local MINUS = '−' -- Unicode U+2212 MINUS SIGN (UTF-8: e2 88 92)
local format = string.format
local floor = math.floor
-- Names when using engineering notation (a prefix of "eN" where N is a number).
-- key = { "name", "linked name, if link wanted" }
-- LATER: Perhaps this should be in convertdata to keep output text in one place.
local eng_scales = {
["3"] = { "thousand" },
["6"] = { "million" },
["9"] = { "billion", "[[1000000000 (number)|billion]]" },
["12"] = { "trillion", "[[1000000000000 (number)|trillion]]" },
["15"] = { "quadrillion", "[[1000000000000000 (number)|quadrillion]]" },
}
-- Configuration options to keep magic values in one location.
local numdot, numsep, maxsigfig, lang
local limit_abuse = 30 -- avoid using very large precision/sigfig (need >20 to avoid values like 1e-22 rounding to 0)
-- Following specify the conversion data which is defined in another module
-- because it is too large to be conveniently included here.
-- To allow easy comparison between "require" and "loadData", a config option
-- can be set to control which is used.
local SIprefixes, units, default_exceptions, link_exceptions
local function set_config(frame)
-- Set configuration options from template #invoke or defaults.
local args = frame.args
numdot = args.numdot or '.' -- decimal mark before fractional digits
numsep = args.numsep or ',' -- thousands separator for numbers (',', '.', '')
maxsigfig = args.maxsigfig or 14 -- maximum number of significant figures
lang = args.lang or 'en' -- language code for messages
if maxsigfig > limit_abuse then
maxsigfig = limit_abuse
end
-- Scribunto sets the global variable 'mw'.
-- A testing program can set the global variable 'is_test_run'.
local convertdata
local data_module = is_test_run and "convertdata" or "Module:Convert/data"
if args.use_require then
convertdata = require(data_module)
else
convertdata = mw.loadData(data_module)
end
SIprefixes = convertdata.SIprefixes
units = convertdata.units
default_exceptions = convertdata.default_exceptions
link_exceptions = convertdata.link_exceptions
end
local function strip(text)
-- If text is a string, return its content with no leading/trailing
-- whitespace. Otherwise return nil (a nil argument gives a nil result).
if type(text) == 'string' then
return text:match("^%s*(.-)%s*$")
end
end
------------------------------------------------------------------------
-- BEGIN: Messages that may be displayed by Module:Convert.
-- LATER: Perhaps move to Module:Convertmessages to simplify this module.
local all_categories = {
['en'] = {
general = '[[Category:Convert error]]',
unknown = '[[Category:Convert unknown unit]]',
mismatch = '[[Category:Convert dimension mismatch]]',
},
}
-- Following puts wanted style around each unit code marked like '...%{ft%}...'.
local unitcode_regex = '%%([{}])'
local unitcode_replace = { ['{'] = '<code style="background:transparent;">', ['}'] = '</code>' }
local all_messages = {
-- All output messages.
['en'] = {
-- The prefix is inserted before each message.
-- LATER: Link should be to 'Template', not 'Module'.
-- Put the correct name when have a template established.
cvt_prefix = '[[Module talk:Convert|Conversion error]]:',
-- Messages; each is a numbered table: { 'error text', 'category key', gsub_regex, gsub_table }.
cvt_bad_default = { 'Unit "%s" has an invalid default', 'unknown' },
cvt_bad_num = { 'Value "%s" must be a number', 'general' },
cvt_bad_num2 = { 'Second value "%s" must be a number', 'general' },
cvt_bad_prec = { 'Parameter precision "%s" must be an integer', 'general' },
cvt_bad_sigfig = { 'Parameter sigfig "%s" must be an integer', 'general' },
cvt_bad_unit = { 'Unit "%s" is invalid here', 'unknown' },
cvt_mismatch = { 'Cannot convert "%s" to "%s"', 'mismatch' },
cvt_no_default = { 'Unit "%s" has no default output unit', 'unknown' },
cvt_no_num = { 'Need value', 'general' },
cvt_no_num2 = { 'Need second value', 'general' },
cvt_no_unit = { 'Need name of unit', 'unknown' },
cvt_should_be = { '%s', 'general', unitcode_regex, unitcode_replace },
cvt_sigfig_pos = { 'sigfig "%s" must be positive', 'general' },
cvt_unknown = { 'Unit "%s" is not known', 'unknown' },
},
}
local messages = {} -- simulating how it would be if in another module
function messages.message(msg, lang)
-- Return wikitext for an error message, including category if specified
-- for the message type.
-- msg = numbered table:
-- msg[1] = 'cvt_xxx' (string used as a key to get message info)
-- msg[2] = 'parm1' (string to replace first %s if any in message)
-- msg[3] = 'parm2' (string to replace second %s if any in message)
-- msg[4] = 'parm3' (string to replace third %s if any in message)
-- lang = 'en' (default), or other language code.
lang = lang or 'en'
local mlang = all_messages[lang]
if mlang then
local t = mlang[msg[1]]
if t then
-- t[1] = message text, t[2] = category, t[3] = gsub_regex, t[4] = gsub_replace
local text = string.format(t[1] or 'Missing message',
msg[2] or '?',
msg[3] or '?',
msg[4] or '?')
local cat = all_categories[lang][t[2]] or ''
local prefix = mlang['cvt_prefix'] or ''
local regex, replace = t[3], t[4]
if regex and replace then
text = text:gsub(regex, replace)
end
return '<span style="color:black; background-color:orange;">' ..
prefix .. ' ' .. text .. cat .. '</span>'
end
end
return 'Convert internal error: unknown message'
end
-- END: Messages that may be displayed by Module:Convert.
------------------------------------------------------------------------
local function shallow_copy(t)
-- Return a shallow copy of t.
-- Do not need the features and overhead of mw.clone() provided by Scribunto.
local result = {}
for k, v in pairs(t) do
result[k] = v
end
return result
end
local unit_mt = {
-- Metatable to get missing values for a unit that does not accept SI prefixes,
-- or a for a unit that accepts prefixes but where no prefix was used.
-- In the latter case, and before use, fields symbol, name1, name1_us
-- must be set from _symbol, _name1, _name1_us respectively.
__index = function (self, key)
local value
if key == 'name1' or key == 'sym_us' then
value = self.symbol
elseif key == 'name2' then
value = self.name1 .. 's'
elseif key == 'name1_us' then
value = self.name1
if not rawget(self, 'name2_us') then
-- If name1_us is 'foot', do not make name2_us by appending 's'.
self.name2_us = self.name2
end
elseif key == 'name2_us' then
local raw1_us = rawget(self, 'name1_us')
if raw1_us then
value = raw1_us .. 's'
else
value = self.name2
end
elseif key == 'link' then
value = self.name1
elseif key == 'engscale' then
value = false
else
return nil -- some fields like invert and multiple may be nil
end
rawset(self, key, value)
return value
end
}
local unit_prefixed_mt = {
-- Metatable to get missing values for a unit that accepts SI prefixes,
-- and where a prefix has been used.
-- Before use, fields si_name, si_prefix must be defined.
__index = function (self, key)
local value
if key == 'symbol' then
value = self.si_prefix .. self._symbol
elseif key == 'sym_us' then
value = self.symbol -- always the same as sym_us for prefixed units
elseif key == 'name1' then
local pos = rawget(self, 'prefix_position') or 1
value = self._name1
value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
elseif key == 'name2' then
value = self.name1 .. 's'
elseif key == 'name1_us' then
value = rawget(self, '_name1_us')
if value then
local pos = rawget(self, 'prefix_position') or 1
value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
else
value = self.name1
end
elseif key == 'name2_us' then
if rawget(self, '_name1_us') then
value = self.name1_us .. 's'
else
value = self.name2
end
elseif key == 'link' then
value = self.name1
elseif key == 'engscale' then
value = false
else
return nil -- some fields like invert and multiple may be nil
end
rawset(self, key, value)
return value
end
}
local function lookup(unitcode, sp, what)
-- Return true, t where t is a copy of the unit's converter table,
-- or return false, t where t is an error message table.
-- Parameter 'sp' is nil, or is 'us' for US spelling of SI prefixes and
-- the symbol and names of the unit. If 'us', the result includes field
-- sp_us = true (that field may also have been in the unit definition).
-- Parameter 'what' determines whether combination units are accepted:
-- 'no_combination' : single unit only
-- 'any_combination' : single unit or combination or multiple
-- 'only_multiple' : single unit or multiple only
-- Parameter unitcode is a symbol (like 'g'), with an optional SI prefix (like 'kg').
-- If, for example, 'kg' is in this table, that entry is used;
-- otherwise the prefix ('k') is applied to the base unit ('g').
-- If unitcode is a known combination code (and if allowed by what),
-- a table of multiple unit tables is included in the result.
-- For compatibility with the old template, underscores in unitcode are replaced
-- with spaces so {{convert|350|board_feet}} --> 350 board feet (0.83 m³).
if unitcode == nil or unitcode == '' then
return false, { 'cvt_no_unit' }
end
unitcode = unitcode:gsub('_', ' ')
local t = units[unitcode]
if t then
if t.shouldbe then
return false, { 'cvt_should_be', t.shouldbe }
end
local force_sp_us = (sp == 'us')
if t.sp_us then
force_sp_us = true
sp = 'us'
end
local target = t.target -- nil, or unitcode is an alias for this target
if target then
local success, result = lookup(target, sp, what)
if not success then return false, result end
for _, item in ipairs({ 'customary', 'default', 'link', 'symbol' }) do
if t[item] then
result[item] = t[item]
end
end
return true, result
end
local combo = t.combination -- nil or a table of unitcodes
if combo then
local multiple = t.multiple
if what == 'no_combination' or (what == 'only_multiple' and multiple == nil) then
return false, { 'cvt_bad_unit', unitcode }
end
-- Recursively create a combination table containing the
-- converter table of each unitcode.
local comboresult = { utype = t.utype, multiple = multiple, combination = {} }
local cvt = comboresult.combination
for i, v in ipairs(combo) do
local success, t = lookup(v, sp, multiple and 'no_combination' or 'only_multiple')
if not success then return false, t end
cvt[i] = t
end
return true, comboresult
end
local result = shallow_copy(t)
result.sp_us = force_sp_us
if result.prefixes then
result.symbol = result._symbol
result.name1 = result._name1
result.name1_us = result._name1_us
end
return true, setmetatable(result, unit_mt)
end
for plen = 2, 1, -1 do
-- Look for an SI prefix; should never occur with an alias.
-- Check for longer prefix first ('dam' is decametre).
-- Micro (µ) is two bytes in utf-8, so is found with plen = 2.
local prefix = string.sub(unitcode, 1, plen)
local si = SIprefixes[prefix]
if si then
local t = units[unitcode:sub(plen+1)]
if t and t.prefixes then
local result = shallow_copy(t)
if sp == 'us' then
result.sp_us = true
end
if result.sp_us and si.name_us then
result.si_name = si.name_us
else
result.si_name = si.name
end
result.si_prefix = si.prefix or prefix
result.scale = t.scale * 10 ^ (si.exponent * t.prefixes)
return true, setmetatable(result, unit_prefixed_mt)
end
end
end
-- Accept any unit with an engineering notation prefix like "e6cuft"
-- (million cubic feet), but not chained prefixes like "e6e6cuft", and
-- not if the unit is a combination or multiple, and not if
-- the unit has an offset (temperature; would need extra work to handle).
local exponent, baseunit = unitcode:match('^e(%d+)(.*)')
if exponent then
local engscale = eng_scales[exponent]
if engscale then
local success, result = lookup(baseunit, sp, 'no_combination')
if not success then return false, result end
if not (result.offset or result.engscale) then
result.defkey = unitcode -- key to lookup default exception
engscale.exponent = exponent
result.engscale = engscale
result.scale = result.scale * 10 ^ tonumber(exponent)
return true, result
end
end
end
return false, { 'cvt_unknown', unitcode }
end
local function ntsh_complement(text)
-- Return text (string of digits) after subtracting each digit from 9.
local result = ''
local first, last = 1, #text
while first <= last do
local lenblock = last + 1 - first
if lenblock > 12 then
lenblock = 12
end
local block = tonumber(text:sub(first, first + lenblock - 1))
local nines = tonumber(string.rep('9', lenblock))
local fmt = '%0' .. tostring(lenblock) .. '.0f'
result = result .. format(fmt, nines - block)
first = first + lenblock
end
return result
end
local function ntsh(n, debug)
-- Return html text to be used for a hidden sort key so that
-- the given number will be sorted in numeric order.
-- If debug == 'yes', output is in a box (not hidden).
-- This implements Template:Ntsh (number table sorting, hidden).
local result, i, f, style
if n >= 0 then
if n > 1e16 then
result = '~'
else
i, f = math.modf(n)
f = floor(1e6 * f)
result = format('&1%016.0f%06d', i, f)
end
else
n = -n
if n > 1e16 then
result = '!'
else
i, f = math.modf(n)
f = floor(1e6 * f)
result = format('%016.0f%06d', i, f)
result = '&0' .. ntsh_complement(result)
end
end
if debug == 'yes' then
style = 'border:1px solid'
else
style = 'display:none'
end
return '<span style="' .. style .. '">' .. result .. '</span>'
end
local function hyphenated(name)
-- Return a hyphenated form of given name (for adjectival usage).
-- This uses a simple and efficient procedure that works for most cases.
-- Some units (if used) would require more, and can later think about
-- adding a method to handle exceptions.
-- The procedure is to replace each space with a hyphen, but
-- not a space after ')' [for "(pre-1954 US) nautical mile"], and
-- not spaces immediately before '(' or in '(...)' [for cases like
-- "British thermal unit (ISO)" and "Calorie (International Steam Table)"].
local pos
if name:sub(1, 1) == '(' then
pos = name:find(')', 1, true)
if pos then
return name:sub(1, pos+1) .. name:sub(pos+2):gsub(' ', '-')
end
elseif name:sub(-1, -1) == ')' then
pos = name:find('(', 1, true)
if pos then
return name:sub(1, pos-2):gsub(' ', '-') .. name:sub(pos-1)
end
end
return name:gsub(' ', '-')
end
local function change_sign(text)
-- Change sign of text for correct appearance because it is negated.
if text:sub(1, 1) == '-' then
return text:sub(2)
end
return '-' .. text
end
local function use_minus(text)
-- Return text with Unicode minus instead of '-', if present.
if text:sub(1, 1) == '-' then
return MINUS .. text:sub(2)
end
return text
end
local function with_separator(text)
-- Return text with thousand separators inserted.
-- The given text is like '123' or '12345.6789' or '1.23e45'
-- (e notation can only occur when processing an input value).
-- The text has no sign (caller inserts that later, if necessary).
-- Separator is inserted only in the integer part of the significand
-- (not after numdot, and not after 'e' or 'E').
-- Four-digit integer parts have a separator (like '1,234').
if numsep == '' then
return text
end
local last = text:match('()[' .. numdot .. 'eE]') -- () returns position
if last == nil then
last = #text
else
last = last - 1 -- index of last character before dot/e/E
end
if last >= 4 then
local groups = {}
local first = last % 3
if first > 0 then
table.insert(groups, text:sub(1, first))
end
first = first + 1
while first < last do
table.insert(groups, text:sub(first, first+2))
first = first + 3
end
return table.concat(groups, numsep) .. text:sub(last+1)
end
return text
end
-- Input values can use values like 1.23e12, but are never displayed
-- using exponent notation like 1.23×10¹².
-- Very small or very large output values use exponent notation.
-- Use format(fmtpower, significand, exponent) where each arg is a string.
local fmtpower = '%s<span style="margin-left:0.2em">×<span style="margin-left:0.1em">10</span></span><sup>%s</sup>'
local function with_exponent(show, exponent)
-- Return wikitext to display the implied value in exponent notation.
if #show > 1 then
show = show:sub(1, 1) .. numdot .. show:sub(2)
end
return format(fmtpower, show, use_minus(tostring(exponent)))
end
local function make_sigfig(value, sigfig)
-- Return show, exponent that are equivalent to the result of
-- converting the number 'value' (where value >= 0) to a string,
-- rounded to 'sigfig' significant figures.
-- The returned items are:
-- show: a string of digits; no sign and no dot;
-- there is an implied dot before show.
-- exponent: a number (an integer) to shift the implied dot.
-- Resulting value = tonumber('.' .. show) * 10^exponent.
-- Examples:
-- make_sigfig(23.456, 3) returns '235', 2 (.235 * 10^2).
-- make_sigfig(0.0023456, 3) returns '235', -2 (.235 * 10^-2).
-- make_sigfig(0, 3) returns '000', 1 (.000 * 10^1).
if sigfig <= 0 then
sigfig = 1
elseif sigfig > maxsigfig then
sigfig = maxsigfig
end
if value == 0 then
return string.rep('0', sigfig), 1
end
local exp, frac = math.modf(math.log10(value))
if frac >= 0 then
frac = frac - 1
exp = exp + 1
end
local digits = format('%.0f', floor((10^(frac + sigfig)) + 0.5))
if #digits > sigfig then
-- Overflow (for sigfig=3: like 0.9999 rounding to "1000"; need "100").
digits = digits:sub(1, sigfig)
exp = exp + 1
end
assert(#digits == sigfig, 'Bug: rounded number has wrong length')
return digits, exp
end
local function format_number(show, exponent, isnegative)
-- Return t where t is a table with the results; fields:
-- show = wikitext formatted to display implied value
-- is_scientific = true if show uses scientific notation
-- clean = unformatted show (possibly adjusted and with inserted numdot)
-- sign = '' or MINUS
-- exponent = exponent (possibly adjusted)
-- The clean and exponent fields can be used to calculate the
-- rounded absolute value, if needed.
--
-- The value implied by the arguments is found from:
-- exponent is nil; and
-- show is a string of digits (no sign), with an optional dot;
-- show = '123.4' is value 123.4, '1234' is value 1234.0;
-- or:
-- exponent is an integer indicating where dot should be;
-- show is a string of digits (no sign and no dot);
-- there is an implied dot before show;
-- show does not start with '0';
-- show = '1234', exponent = 3 is value 0.1234*10^3 = 123.4.
--
-- The formatted result:
-- * Includes a Unicode minus if isnegative.
-- * Has numsep inserted where necessary.
-- * Uses scientific notation for very small or large values.
-- * Has no more than maxsigfig significant digits
-- (same as old template and {{#expr}}).
local sign = isnegative and MINUS or ''
local maxlen = maxsigfig
if exponent == nil then
local integer, dot, fraction = show:match('^(%d*)([' .. numdot .. ']?)(.*)')
if #integer >= 10 then
show = integer .. fraction
exponent = #integer
elseif integer == '0' or integer == '' then
local zeros, figs = fraction:match('^(0*)([^0]?.*)')
if #figs == 0 then
if #zeros > maxlen then
show = '0' .. numdot .. zeros:sub(1, maxlen)
end
elseif #zeros >= 4 then
show = figs
exponent = -#zeros
elseif #figs > maxlen then
show = '0' .. numdot .. zeros .. figs:sub(1, maxlen)
end
else
maxlen = maxlen + #dot
if #show > maxlen then
show = show:sub(1, maxlen)
end
end
end
if exponent then
if #show > maxlen then
show = show:sub(1, maxlen)
end
if exponent > 10 or exponent <= -4 or (exponent == 10 and show ~= '1000000000') then
-- Rounded value satisfies: value >= 1e9 or value < 1e-4 (1e9 = 0.1e10).
return {
clean = '.' .. show,
exponent = exponent,
sign = sign,
show = sign .. with_exponent(show, exponent-1),
is_scientific = true }
end
if exponent >= #show then
show = show .. string.rep('0', exponent - #show) -- result has no dot
elseif exponent <= 0 then
show = '0' .. numdot .. string.rep('0', -exponent) .. show
else
show = show:sub(1, exponent) .. numdot .. show:sub(exponent+1)
end
end
if isnegative and show:match('^0.?0*$') then
sign = '' -- don't show minus if result is negative but rounds to zero
end
return {
clean = show,
sign = sign,
show = sign .. with_separator(show) }
end
-- Fraction output format (like old template).
-- frac1: sign, numerator, denominator
-- frac2: wholenumber, sign, numerator, denominator
local frac1 = '<span style="white-space:nowrap">%s<sup>%s</sup>⁄<sub>%s</sub></span>'
local frac2 = '<span class="frac nowrap">%s<s style="display:none">%s</s><sup>%s</sup>⁄<sub>%s</sub></span>'
local function extract_fraction(text, negative)
-- If text represents a fraction, return value, show where
-- value is a number and show is a string.
-- Otherwise, return nil.
--
-- In the following, '(3/8)' represents the wikitext required to
-- display a fraction with numerator 3 and denominator 8.
-- In the wikitext, Unicode minus is used for a negative value.
-- text value, show value, show
-- if not negative if negative
-- 3 / 8 0.375, '(3/8)' -0.375, '−(3/8)'
-- 2 + 3 / 8 2.375, '2(3/8)' -1.625, '−2(−3/8)'
-- 2 - 3 / 8 1.625, '2(−3/8)' -2.375, '−2(3/8)'
-- 1 + 20/8 3.5 , '1/(20/8)' 1.5 , '−1/(−20/8)'
-- 1 - 20/8 -1.5., '1(−20/8)' -3.5 , '−1(20/8)'
-- Wherever an integer appears above, numbers like 1.25 or 12.5e-3
-- (which may be negative) are also accepted (like old template).
-- Template interprets '1.23e+2+12/24' as '123(12/24)' = 123.5!
local lhs, negfrac, rhs, numstr, numerator, denstr, denominator, wholestr, whole, value
lhs, denstr = text:match('^%s*([^/]-)%s*/%s*(.-)%s*$')
denominator = tonumber(denstr)
if denominator == nil then return nil end
wholestr, negfrac, rhs = lhs:match('^%s*(.-[^eE])%s*([+-])%s*(.-)%s*$')
if wholestr == nil or wholestr == '' then
wholestr = nil
whole = 0
numstr = lhs
else
whole = tonumber(wholestr)
if whole == nil then return nil end
numstr = rhs
end
negfrac = (negfrac == '-')
numerator = tonumber(numstr)
if numerator == nil then return nil end
if negative == negfrac or wholestr == nil then
value = whole + numerator / denominator
else
value = whole - numerator / denominator
numstr = change_sign(numstr)
end
if tostring(value):find('#', 1, true) then
return nil -- overflow or similar
end
numstr = use_minus(numstr)
denstr = use_minus(denstr)
local wikitext
if wholestr then
local sign = negative and MINUS or '+'
if negative then
wholestr = change_sign(wholestr)
end
wikitext = format(frac2, use_minus(wholestr), sign, numstr, denstr)
else
local sign = negative and MINUS or ''
wikitext = format(frac1, sign, numstr, denstr)
end
return value, wikitext
end
local missing = { 'cvt_no_num', 'cvt_no_num2' }
local invalid = { 'cvt_bad_num', 'cvt_bad_num2' }
local function extract_number(args, index, which)
-- Return true, info if can extract a number from the text in args[index],
-- where info is a table with the result,
-- or return false, t where t is an error message table.
-- Parameter 'which' (1 or 2) selects which input value is being
-- processed (to select the appropriate error message, if needed).
-- Before processing, the input text is cleaned:
-- * Any thousand separators (valid or not) are removed.
-- * Any sign (and optional following whitespace) is replaced with
-- '-' (if negative) or '' (otherwise).
-- That replaces Unicode minus with '-'.
-- If successful, the returned info table contains named fields:
-- value = a valid number
-- singular = true if value is 1 (to use singular form of units)
-- = false if value is -1 (like old template)
-- clean = cleaned text with any separators and sign removed
-- show = text formatted for output
-- For show:
-- * Thousand separators are inserted.
-- * If negative, a Unicode minus is used; otherwise the sign
-- is '+' (if the input text used '+'), or is ''.
-- TODO Think about fact that the input value might be like 1.23e+123.
-- Will the exponent break anything?
local text = strip(args[index])
if text == nil or text == '' then return false, { missing[which] } end
local clean, sign
if numsep == '' then
clean = text
else
clean = text:gsub('[' .. numsep .. ']', '') -- use '[.]' if numsep is '.'
end
-- Remove any sign character (assuming a number starts with '.' or a digit).
sign, clean = clean:match('^%s*([^ .%d]*)%s*(.*)')
if sign == nil or clean == nil then
return false, { missing[which] } -- should never occur
end
local propersign, negative
if sign == MINUS or sign == '-' then
propersign = MINUS
negative = true
elseif sign == '+' then
propersign = '+'
negative = false
elseif sign == '' then
propersign = ''
negative = false
else
return false, { invalid[which], text }
end
local show, singular
local value = tonumber(clean)
if value == nil then
value, show = extract_fraction(clean, negative)
if value == nil then
return false, { invalid[which], text }
end
singular = false -- any fraction (even with value 1) is regarded as plural
end
if show == nil then
singular = (value == 1 and not negative)
show = propersign .. with_separator(clean)
end
if negative and (value ~= 0) then
value = -value
end
return true, {
value = value,
singular = singular,
clean = clean,
show = show
}
end
local function require_integer(text, invalid)
-- Return true, n where n = integer equivalent to given text,
-- or return false, t where t is an error message table.
-- Input should be the text for a simple integer (no separators, no Unicode minus).
-- Using regex avoids irritations with input like '-0.000001'.
if text == nil then return false, { 'cvt_no_num' } end
if string.match(text, '^-?%d+$') == nil then
return false, { invalid, text }
end
return true, tonumber(text)
end
local function get_parms(pframe)
-- If successful, return true, args, unit where
-- args is a table of all arguments passed to the template
-- converted to named arguments, and
-- unit is the input unit table;
-- or return false, t where t is an error message table.
-- Some of the named args that are added here could be provided by the
-- user of the template.
-- MediaWiki removes leading and trailing whitespace from the values of
-- named arguments. However, the values of numbered arguments include any
-- whitespace entered in the template, and whitespace is used by some
-- parameters (example: the numbered parameters associated with "disp=x").
local range_types = { -- text to separate input, output ranges; for 'x', depends on abbr
['and'] = {' and ', ' and '},
['by'] = {' by ', ' by '},
['to'] = {' to ', ' to '},
['-'] = {'–', '–'},
['to(-)'] = {' to ', '–'},
['to-'] = {' to ', '–'},
['x'] = {{ ['out'] = ' by ' , ['in'] = ' × ', ['off'] = ' by ', ['on'] = ' × ' },
{ ['out'] = ' × ', ['in'] = ' by ' , ['off'] = ' by ', ['on'] = ' × ' }},
['+/-'] = {' ± ', ' ± '},
}
local success, info1, info2
local args = {} -- arguments passed to template
for k, v in pairs(pframe.args) do
args[k] = v
end
success, info1 = extract_number(args, 1, 1)
if not success then return false, info1 end
local in_unit, precision
local next = strip(args[2])
local i = 3
local range = range_types[next]
if range == nil then
in_unit = next
else
args.range = range
args.is_range_x = (next == 'x')
success, info2 = extract_number(args, 3, 2)
if not success then return false, info2 end
in_unit = strip(args[4])
i = 5
end
local success, in_unit_table = lookup(in_unit, args.sp, 'no_combination')
if not success then return false, in_unit_table end
if args.test == 'msg' then
-- Am testing the messages produced when no output unit is specified, and
-- the input unit has a missing or invalid default.
-- Set two units for testing that.
-- LATER: Remove this code.
if in_unit == 'chain' then
in_unit_table.default = nil -- no default
elseif in_unit == 'rd' then
in_unit_table.default = "ft|X|m" -- an invalid expression
end
end
in_unit_table.valinfo = { info1, info2 } -- info2 is nil if no range
in_unit_table.inout = 'in' -- this is an input unit
next = strip(args[i])
i = i + 1
if tonumber(next) == nil then
args.out_unit = next
next = strip(args[i])
if tonumber(next) then
i = i + 1
precision = next
end
else
precision = next
end
if args.adj == nil and args.sing ~= nil then
args.adj = args.sing -- sing (singular) is apparently an old equivalent of adj
end
if args.adj == 'mid' then
args.adj = 'on'
next = args[i]
i = i + 1
if next == nil then
args.mid = ''
else -- mid-text words
if next:sub(1, 1) == '-' then
args.mid = next
else
args.mid = ' ' .. next
end
end
elseif args.adj == 'on' then
args.mid = ''
end
if precision == nil then
if tonumber(args[i]) then
precision = strip(args[i])
i = i + 1
end
end
local disp = args.disp
if disp == 'x' then
args.joins = { args[i] or '', args[i+1] or '' }
elseif disp == 's' or disp == '/' then
args.disp = 'slash'
end
args.precision = args.precision or precision -- allow named parameter
local abbr = args.abbr
if abbr == nil then
-- Default is to abbreviate output (use symbol), or input if flipped.
args.abbr = (disp == 'flip') and 'in' or 'out'
else
args.abbr_org = abbr -- original abbr (as entered by user)
if disp == 'flip' then -- 'in' = LHS, 'out' = RHS
if abbr == 'in' then
abbr = 'out'
elseif abbr == 'out' then
abbr = 'in'
end
end
args.abbr = abbr
end
return true, args, in_unit_table
end
local function default_precision(inclean, invalue, outvalue, in_current, out_current)
-- Return a default value for precision (an integer like 2, 0, -2).
-- Code follows procedures used in old template.
-- Am putting exceptions to standard calculations here, as they are
-- discovered. Can later decide if something cleaner should be done.
-- LATER: Things like the hand unit of length will need special processing.
local log10 = math.log10
local prec, minprec, adjust
local utype = out_current.utype
local fudge = 1e-14 -- {{Order of magnitude}} adds this, so we do too
-- Find fractional digits, handling cases like inclean = '12.345e6'.
local integer, dot, fraction = inclean:match('^(%d*)([' .. numdot .. ']?)(%d*)')
local exception = (utype == 'temperature' and not
(in_current.exception == 'temperature' or out_current.exception == 'temperature'))
if exception then
-- LATER: Give an error message if (invalue < in_current.offset): below absolute zero?
adjust = 0
local kelvin = (invalue - in_current.offset) * in_current.scale
if kelvin <= 0 then -- can get zero, or small but negative value due to precision problems
minprec = 2
else
minprec = 2 - floor(log10(kelvin) + fudge) -- 3 sigfigs in kelvin
end
else
if invalue == 0 or outvalue <= 0 then
-- We are never called with a negative outvalue, but it might be zero.
-- This is special-cased to avoid calculation exceptions.
return 0
end
if out_current.symbol == 'ft' and dot == '' then
-- More precision when output ft with integer input value.
adjust = -log10(in_current.scale)
else
adjust = log10(math.abs(invalue / outvalue))
end
adjust = adjust + log10(2)
-- Ensure that the output has at least two significant figures.
minprec = 1 - floor(log10(outvalue) + fudge)
end
if dot == '' then
prec = -integer:match('0*$'):len() -- '12300' gives -2, but so does '12300e-5'
else
if fraction == '' and not exception then
prec = 1 -- "123." has same precision as "123.0", like old template
else
prec = #fraction
end
end
return math.max(floor(prec + adjust), minprec)
end
local function convert(value, in_current, out_current)
local inscale = in_current.scale
local outscale = out_current.scale
if in_current.invert then
if in_current.invert * out_current.invert < 0 then
return 1 / (value * inscale * outscale)
end
return value * (inscale / outscale)
elseif in_current.offset then
return (value - in_current.offset) * (inscale / outscale) + out_current.offset
else
return value * (inscale / outscale)
end
end
local function cvtround(parms, info, in_current, out_current)
-- Return true, t where t is a table with the conversion results; fields:
-- show = rounded, formatted string from converting value in info,
-- using the rounding specified in parms.
-- singular = true if result is positive, and (after rounding)
-- is "1", or like "1.00";
-- (and more fields shown below, and a calculated 'absvalue' field).
-- or return true, nil if no value specified;
-- or return false, t where t is an error message table.
-- This code combines convert/round because some rounding requires
-- knowledge of what we are converting.
local invalue, inclean, show, exponent, singular
if info then
invalue, inclean = info.value, info.clean
end
if invalue == nil or invalue == '' then
return true, nil
end
local outvalue = convert(invalue, in_current, out_current)
local isnegative
if outvalue < 0 then
isnegative = true
outvalue = -outvalue
end
local success
local precision = parms.precision
local sigfig = parms.sigfig
local disp = parms.disp
if precision then
-- Ignore sigfig, disp.
success, precision = require_integer(precision, 'cvt_bad_prec')
if not success then return false, precision end
elseif sigfig then
-- Ignore disp.
success, sigfig = require_integer(sigfig, 'cvt_bad_sigfig')
if not success then return false, sigfig end
if sigfig <= 0 then
return false, { 'cvt_sigfig_pos', parms.sigfig }
end
show, exponent = make_sigfig(outvalue, sigfig)
elseif disp == '5' then
show = format('%.0f', floor((outvalue / 5) + 0.5) * 5)
else
precision = default_precision(inclean, invalue, outvalue, in_current, out_current)
end
if precision then
if precision >= 0 then
if precision > limit_abuse then
precision = limit_abuse
elseif precision <= 8 then
-- Add a fudge to handle common cases of bad rounding due to inability
-- to precisely represent some values. This makes the following work:
-- {{convert|-100.1|C|K}} and {{convert|5555000|um|m|2}}.
-- Old template uses #expr round, which invokes PHP round().
-- LATER: Investigate how PHP round() works.
outvalue = outvalue + 2e-14
end
local fmt = '%.' .. format('%d', precision) .. 'f'
show = format(fmt, outvalue)
else
precision = -precision -- #digits to zero (in addition to digits after dot)
if precision > limit_abuse then
precision = limit_abuse
end
local shift = 10 ^ precision
if shift > outvalue then
show = '0' -- like old template, user can zero all digits
else
show = format('%.0f', floor(outvalue/shift + 0.5))
exponent = #show + precision
end
end
end
-- TODO Does following work when exponent ~= nil?
-- What if show = '1000' and exponent = 1 (value = .1000*10^1 = 1)?
-- What if show = '1000' and exponent = 2 (value = .1000*10^2 = 10)?
if (show == '1' or show:match('^1%.0*$') ~= nil) and not isnegative then
-- Use match because on some systems 0.99999999999999999 is 1.0.
singular = true
end
local t = format_number(show, exponent, isnegative)
t.singular = singular
t.raw_absvalue = outvalue -- absolute value before rounding
return true, setmetatable(t, {
__index = function (self, key)
if key == 'absvalue' then
-- Calculate absolute value after rounding, if needed.
local clean, exponent = rawget(self, 'clean'), rawget(self, 'exponent')
local value = tonumber(clean) -- absolute value (any negative sign has been ignored)
if exponent then
value = value * 10^exponent
end
rawset(self, key, value)
return value
end
end })
end
-- TODO Think about when to use ' ' and when to use ' '.
-- For outputs, could use raw_absvalue (not bothering with calculated absvalue).
-- Old template always uses nbsp before a unit symbol, but seems inconsistent
-- before a unit name. Template:Convert/LoffAonSoff says
-- "Numbers less than 1,000 is not wrapped nor are unit symbols"
-- so 1000 is a threshold (use nbsp for smaller values), but no conclusive results.
-- Possibly a concern is wrapping when using {{convert}} in a table
-- (don't want to force a column to be unnecessarily wide by using nbsp).
local disp_joins = {
['or'] = { ' or ' , '' },
['sqbr-sp'] = { ' [' , ']' },
['sqbr-nbsp'] = { ' [' , ']' },
['comma'] = { ', ' , '' },
['slash-sp'] = { ' / ' , '' },
['slash-nbsp'] = { ' / ', '' },
['slash-nosp'] = { '/' , '' },
['b'] = { ' (' , ')' },
}
local function evaluate_condition(value, condition)
-- Return true or false from applying a conditional expression to value,
-- or throw an error if invalid.
-- A very limited set of expressions is supported:
-- v < 9
-- v * 9 < 9
-- where
-- 'v' is replaced with value
-- 9 is any number (as defined by Lua tonumber)
-- '<' can also be '<=' or '>' or '>='
-- In addition, the following form is supported:
-- LHS and RHS
-- where
-- LHS, RHS = any of above expressions.
local function compare(value, text)
local arithop, factor, compop, limit = text:match('^%s*v%s*([*]?)(.-)([<>]=?)(.*)$')
if arithop == nil then
error('Invalid default expression', 0)
elseif arithop == '*' then
factor = tonumber(factor)
if factor == nil then
error('Invalid default expression', 0)
end
value = value * factor
end
limit = tonumber(limit)
if limit == nil then
error('Invalid default expression', 0)
end
if compop == '<' then
return value < limit
elseif compop == '<=' then
return value <= limit
elseif compop == '>' then
return value > limit
elseif compop == '>=' then
return value >= limit
end
error('Invalid default expression', 0) -- should not occur
end
local lhs, rhs = condition:match('^(.-%W)and(%W.*)')
if lhs == nil then
return compare(value, condition)
end
return compare(value, lhs) and compare(value, rhs)
end
local function get_default(value, unit_table)
-- Return true, s where s = name of unit's default output unit,
-- or return false, t where t is an error message table.
-- Some units have a default that depends on the input value
-- (the first value if a range of values is used).
-- If '|' is in the default, the first pipe-delimited field is an
-- expression that uses 'v' to represent the input value.
-- Example: 'v < 120 | small | big | suffix' (suffix is optional)
-- evaluates 'v < 120' as a boolean with result
-- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise.
local default = default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default
if default == nil then
return false, { 'cvt_no_default', unit_table.symbol }
end
if default:find('|', 1, true) == nil then
return true, default
end
local t = {}
default = default .. '|' -- to get last item
for item in default:gmatch('%s*(.-)%s*|') do
table.insert(t, item) -- split on '|', removing leading/trailing whitespace
end
if #t == 3 or #t == 4 then
local success, result = pcall(evaluate_condition, value, t[1])
if success then
default = result and t[2] or t[3]
if #t == 4 then
default = default .. t[4]
end
return true, default
end
end
return false, { 'cvt_bad_default', unit_table.symbol }
end
local function make_id(parms, which, unit_table)
-- Return id, f where
-- id = unit name or symbol, possibly modified
-- f = true if id is a name, or false if id is a symbol
-- using 1st or 2nd values (which), and for 'in' or 'out' (unit_table.inout).
-- Result is '' if no symbol/name is to be used.
local abbr = parms.abbr
if abbr == 'values' then
return ''
end
local inout = unit_table.inout
local valinfo = unit_table.valinfo
local abbr_org = parms.abbr_org
local adj = parms.adj
local disp = parms.disp
local lk = parms.lk
local usename = unit_table.usename
local singular = valinfo[which].singular or (inout == 'in' and adj == 'on')
if usename then
-- Old template does something like this.
if lk == 'on' or lk == inout then
-- A linked unit uses the standard singular.
else
-- Set non-standard singular.
if inout == 'in' then
if adj ~= 'on' and (abbr_org == 'out' or disp == 'flip') then
local value = valinfo[which].value
singular = (0 < value and value < 1.0001)
end
else
if (abbr_org == 'on') or
(disp == nil and (abbr_org == nil or abbr_org == 'out')) or
(disp == 'flip' and abbr_org == 'in') then
singular = (valinfo[which].absvalue < 1.0001 and
not valinfo[which].is_scientific)
end
end
end
end
if unit_table.engscale then
singular = false -- so 1 e3kg gives 1 thousand kilograms (plural)
end
local key_name = (singular and not parms.is_range_x) and 'name1' or 'name2'
local key_symbol = 'symbol'
if unit_table.sp_us then
key_name = key_name .. '_us'
key_symbol = 'sym_us'
end
local want_name
if usename then
want_name = true
else
if abbr_org == nil then
if disp == 'or' or disp == 'slash' then
want_name = true
end
if unit_table.utype == 'temperature' or unit_table.utype == 'temperature change' then
if not (unit_table.exception == 'temperature') then
want_name = false
end
end
end
if want_name == nil then
if abbr == 'on' or abbr == inout or (abbr == 'mos' and inout == 'out') then
want_name = false
else
want_name = true
end
end
end
local id = unit_table[want_name and key_name or key_symbol]
if lk == 'on' or lk == inout then
local link = link_exceptions[unit_table.symbol] or unit_table.link
if link then
local customary_units = {
'[[United States customary units|US]] ',
'[[United States customary units|U.S.]] '
}
local customary = customary_units[unit_table.customary]
if customary then
-- Omit any "US"/"U.S." from start of id since that will be inserted.
for _, prefix in ipairs({ 'US ', 'US ', 'U.S. ', 'U.S. ', }) do
local plen = #prefix
if id:sub(1, plen) == prefix then
id = id:sub(plen + 1)
break
end
end
else
customary = ''
end
id = customary .. '[[' .. link .. '|' .. id .. ']]'
end
end
return id, want_name
end
local function decorate_value(parms, unit_table, which)
-- If needed, update unit_table.valinfo so values will be shown with
-- extra information.
-- Currently, this is used only for engineering notation, but might include
-- prefacing a value with a currency symbol like "$", or handling "per" units.
-- For consistency with the old template (but different from fmtpower),
-- the style to display powers of 10 includes "display:none" to allow some
-- browsers to copy, for example, "10³" as "10^3", rather than as "103".
local engscale = unit_table.engscale
if engscale then
local info = unit_table.valinfo[which]
local abbr = parms.abbr
if abbr == 'on' or abbr == unit_table.inout then
info.show = info.show ..
'<span style="margin-left:0.2em">×<span style="margin-left:0.1em">10</span></span><s style="display:none">^</s><sup>'
.. engscale.exponent .. '</sup>'
else
local number_id
local lk = parms.lk
if lk == 'on' or lk == 'in' then
number_id = engscale[2] or engscale[1]
else
number_id = engscale[1]
end
info.show = info.show .. ' ' .. number_id
end
end
end
local function process_input(parms, in_current)
-- Processing required once per conversion.
-- Return block of text to represent input (value/unit).
local id1, want_name = make_id(parms, 1, in_current)
local extra = ''
local result
local adj = parms.adj
local disp = parms.disp
if disp == 'output only' or
disp == 'output number only' or disp == 'number' or
disp == 'u2' or disp == 'unit2' then
result = ''
parms.joins = { '', '' }
elseif disp == 'unit' then
if adj == 'on' then
result = hyphenated(id1)
else
result = id1
end
parms.joins = { '', '' }
else
local abbr = parms.abbr
local mos = (abbr == 'mos')
local range = parms.range
local id = (range == nil) and id1 or make_id(parms, 2, in_current)
if id ~= '' then
if adj == 'on' then
mos = false -- if hyphenated, suppress repeat of unit in a range
extra = '-' .. hyphenated(id) .. parms.mid
else
extra = ' ' .. id
end
end
local valinfo = in_current.valinfo
if range == nil then
decorate_value(parms, in_current, 1)
result = valinfo[1].show
else
local rtext = range[1]
if type(rtext) == 'table' then
rtext = rtext[abbr] or rtext['off']
end
if mos then
decorate_value(parms, in_current, 1)
decorate_value(parms, in_current, 2)
result = valinfo[1].show .. ' ' .. id1 .. rtext .. valinfo[2].show
elseif parms.is_range_x and not want_name then
if abbr == 'in' or abbr == 'on' then
decorate_value(parms, in_current, 1)
end
decorate_value(parms, in_current, 2)
result = valinfo[1].show .. ' ' .. id .. rtext .. valinfo[2].show
else
if abbr == 'in' or abbr == 'on' then
decorate_value(parms, in_current, 1)
end
decorate_value(parms, in_current, 2)
result = valinfo[1].show .. rtext .. valinfo[2].show
end
end
if disp == nil then -- special case for the most common setting
parms.joins = disp_joins['b']
elseif disp ~= 'x' then
-- Old template does this.
if disp == 'slash' then
if parms.abbr_org == nil then
disp = 'slash-nbsp'
elseif abbr == 'in' or abbr == 'out' then
disp = 'slash-sp'
else
disp = 'slash-nosp'
end
elseif disp == 'sqbr' then
if abbr == 'on' then
disp = 'sqbr-nbsp'
else
disp = 'sqbr-sp'
end
end
parms.joins = disp_joins[disp] or disp_joins['b']
end
end
return result .. extra
end
local function process_one_output(parms, out_current)
-- Processing required for each output unit.
-- Return block of text to represent output (value/unit).
local id1, want_name = make_id(parms, 1, out_current)
local extra = ''
local result
local disp = parms.disp
if disp == 'u2' or disp == 'unit2' then -- 'unit2' is not in old template
if parms.adj == 'on' then
result = hyphenated(id1)
else
result = id1
end
else
local range = parms.range
if not (disp == 'output number only' or disp == 'number') then
local id = (range == nil) and id1 or make_id(parms, 2, out_current)
if id ~= '' then
extra = ' ' .. id
end
end
local valinfo = out_current.valinfo
if range == nil then
decorate_value(parms, out_current, 1)
result = valinfo[1].show
else
local abbr = parms.abbr
local rtext = range[2]
if type(rtext) == 'table' then
rtext = rtext[abbr] or rtext['on']
end
if parms.is_range_x and not want_name then
if abbr == 'out' or abbr == 'on' then
decorate_value(parms, out_current, 1)
end
decorate_value(parms, out_current, 2)
result = valinfo[1].show .. extra .. rtext .. valinfo[2].show
else
if abbr == 'out' or abbr == 'on' then
decorate_value(parms, out_current, 1)
end
decorate_value(parms, out_current, 2)
result = valinfo[1].show .. rtext .. valinfo[2].show
end
end
end
return result .. extra
end
local function make_output_single(parms, info, in_unit_table, out_unit_table)
-- Return true, item where item = wikitext of the conversion result
-- for a single output (which is not a combination or a multiple);
-- or return false, t where t is an error message table.
local success, info1, info2
success, info1 = cvtround(parms, info[1], in_unit_table, out_unit_table)
if not success then return false, info1 end
success, info2 = cvtround(parms, info[2], in_unit_table, out_unit_table)
if not success then return false, info2 end
out_unit_table.valinfo = { info1, info2 }
return true, process_one_output(parms, out_unit_table)
end
local function make_output_multiple(parms, info, in_unit_table, out_unit_table)
-- Return true, item where item = wikitext of the conversion result
-- for an output which is a multiple (like 'ftin');
-- or return false, t where t is an error message table.
local multiple = out_unit_table.multiple -- table of scaling factors (will not be nil)
local combos = out_unit_table.combination -- table of unit tables (will not be nil)
local abbr = parms.abbr
local abbr_org = parms.abbr_org
local disp = parms.disp
local want_name = (abbr_org == nil and (disp == 'or' or disp == 'slash')) or
not (abbr == 'on' or abbr == 'out' or abbr == 'mos')
local want_link = (parms.lk == 'on' or parms.lk == 'out')
local function make_result(info)
local outvalue, sign, fmt
local results = {}
for i = 1, #combos do
local thisvalue
local out_current = combos[i]
out_current.inout = 'out'
local scale = multiple[i]
if i == 1 then -- least significant unit ('in' from 'ftin')
local success, outinfo = cvtround(parms, info, in_unit_table, out_current)
if not success then return false, outinfo end
sign = outinfo.sign
local fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or ''
fmt = '%.' .. #fraction .. 'f' -- to reproduce precision
if fraction == '' then
outvalue = floor(outinfo.raw_absvalue + 0.5) -- keep all integer digits of least significant unit
else
outvalue = outinfo.absvalue
end
end
if scale then
outvalue, thisvalue = floor(outvalue / scale), outvalue % scale
else
thisvalue = outvalue
end
local id
if want_name then
id = out_current[(thisvalue == 1) and 'name1' or 'name2']
else
id = out_current['symbol']
end
if want_link then
local link = out_current.link
if link then
id = '[[' .. link .. '|' .. id .. ']]'
end
end
local strval = (thisvalue == 0) and '0' or with_separator(format(fmt, thisvalue))
table.insert(results, strval .. ' ' .. id)
if outvalue == 0 then
break
end
fmt = '%.0f' -- only least significant unit can have a fraction
end
local reversed, count = {}, #results
for i = 1, count do
reversed[i] = results[count + 1 - i]
end
return sign .. table.concat(reversed, ' ')
end
local result = make_result(info[1])
local range = parms.range
if range then
local rtext = range[2]
if type(rtext) == 'table' then
rtext = rtext[abbr] or rtext['on']
end
result = result .. rtext .. make_result(info[2])
end
return true, result
end
local function process(parms, in_unit_table)
-- Return true, s where s = final wikitext result,
-- or return false, t where t is an error message table.
local success, out_unit_table
local info = in_unit_table.valinfo
local invalue1 = info[1].value
local out_unit = parms.out_unit
if out_unit == nil or out_unit == '' then
success, out_unit = get_default(invalue1, in_unit_table)
if not success then return false, out_unit end
end
success, out_unit_table = lookup(out_unit, parms.sp, 'any_combination')
if not success then return false, out_unit_table end
if in_unit_table.utype ~= out_unit_table.utype then
return false, { 'cvt_mismatch', in_unit_table.utype, out_unit_table.utype }
end
local outputs = {}
local combos -- nil (for 'ft' or 'ftin'), or table of unit tables (for 'm ft')
if out_unit_table.multiple == nil then -- nil ('ft' or 'm ft'), or table of factors ('ftin')
combos = out_unit_table.combination
end
local imax = combos and #combos or 1 -- 1 (single unit) or number of unit tables
for i = 1, imax do
local success, item
local out_current = combos and combos[i] or out_unit_table
out_current.inout = 'out'
if out_current.multiple == nil then
success, item = make_output_single(parms, info, in_unit_table, out_current)
else
success, item = make_output_multiple(parms, info, in_unit_table, out_current)
end
if not success then return false, item end
table.insert(outputs, item)
end
local disp = parms.disp
local in_block = process_input(parms, in_unit_table)
local out_block = (disp == 'unit') and '' or table.concat(outputs, '; ')
if disp == 'flip' then
in_block, out_block = out_block, in_block
end
local wikitext = in_block .. parms.joins[1] .. out_block .. parms.joins[2]
if parms.sortable == 'on' then
wikitext = ntsh(invalue1, parms.debug) .. wikitext
end
return true, wikitext
end
local function convert(frame)
set_config(frame)
local result
local success, parms, in_unit_table = get_parms(frame:getParent())
if success then
success, result = process(parms, in_unit_table)
else
result = parms
end
if success then
return result
end
return messages.message(result, lang)
end
return { convert = convert }