{{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.
The template that invokes this module is:
- Template:Convert/sandboxlua
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)
Many testcase pages are available. Currently, the testcases compare the output from the current Template:Convert with the output from Template:Convert/sandboxlua.
- Template:Convert/testcases – template tests
Units are defined in the wikitext of the master list of units.
- Module:Convert/documentation/conversion data/doc – 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.
Sandbox
When making a change, copy the current modules to the sandbox pages, then edit the sandbox copies:
- Module:Convert/sandbox
- Module:Convert/data/sandbox
- Module:Convert/text/sandbox
- Module:Convert/extra/sandbox
Use the following template to test the results (example {{convert/sandbox|123|lb|kg}}
):
- Template:Convert/sandbox
Template:Convert/sandbox invokes Module:Convert/sandbox with parameter |sandbox=on
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.
- Module:Convert/tester – module to run tests by comparing template output with fixed text
- Module:Convert/sandbox/testcases – templates to be tested, with expected outputs (uses the tester module)
- Module talk:Convert/sandbox/testcases – view test results
It is not necessary to save the testcases page before viewing test results. For example, Module:Convert/sandbox/testcases could be edited to change the tests. While still editing that page, paste "Module talk:Convert/sandbox/testcases
" (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 figureswarnings = off
– on if invalid options should show a warningsandbox = off
– on if the sandbox modules should be used
Other configuration is available in the translation_table
at Module:Convert/text. For example, bn:Module:Convert/text uses:
group = 2
– group numbers 3 digits, then 2 digitsplural_suffix = ''
– disable plural unit names (do not append "s")- Tables to translate digits to and from the English digits used in calculations.
Module:Convert/text contains all text used for input parameters and for output messages and categories. For example, lk=on
may be used at en.wikipedia to link each displayed unit to its article. The text module could be edited to replace "lk
" and "on
" with any desired text.
-- 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 abs = math.abs
local floor = math.floor
local format = string.format
local log10 = math.log10
local ustring = mw.ustring
local ulen = ustring.len
local usub = ustring.sub
-- Configuration options to keep magic values in one location.
-- Conversion data and message text are defined in separate modules.
local config, maxsigfig
local numdot, numsep -- each must be a single byte for simple regex search/replace
local default_exceptions, link_exceptions, all_units
local SIprefixes, all_categories, all_messages, customary_units, disp_joins
local en_option_name, en_option_value, eng_scales, range_aliases, range_types
local group_method = 3 -- code for how many digits are in a group
local per_word = 'per' -- for units like "miles per gallon"
local plural_suffix = 's' -- only other useful value is probably '' to disable plural unit names
local from_en_table -- to translate an output string of en digits to local language
local to_en_table -- to translate an input string of digits in local language to en
-- All units should be defined in the data module. However, to cater for quick changes
-- and experiments, any unknown unit is looked up in an extra data module, if it exists.
-- That module would be transcluded in only a small number of pages, so there should be
-- little server overhead from making changes, and changes should propagate quickly.
local extra_module -- name of module with extra units
local extra_units -- nil or table of extra units from extra_module
local function boolean(text)
-- Return true if text represents a "true" option value.
if text then
text = text:lower()
if text == 'on' or text == 'yes' then
return true
end
end
return false
end
local function from_en(text)
-- Input is a string representing a number in en digits with '.' decimal mark,
-- without digit grouping (which is done just after calling this).
-- Return the translation of the string with numdot and digits in local language.
if numdot ~= '.' then
text = text:gsub('%.', numdot)
end
if from_en_table then
text = text:gsub('%d', from_en_table)
end
return text
end
local function to_en(text)
-- Input is a string representing a number in the local language with
-- an optional numdot decimal mark and numsep digit grouping.
-- Return the translation of the string with '.' mark and en digits,
-- and no separators (they have to be removed here to handle cases like
-- numsep = '.' and numdot = ',' with input "1.234.567,8").
if numsep ~= '' then
text = text:gsub('[' .. numsep .. ']', '') -- use '[x]' in case x is '.'
end
if numdot ~= '.' then
text = text:gsub('[' .. numdot .. ']', '.')
end
if to_en_table then
text = ustring.gsub(text, '%d', to_en_table)
end
return text
end
local spell_module -- name of module that can spell numbers
local speller -- function from that module to handle spelling (set if spelling is wanted)
local function set_config(frame)
-- Set configuration options from template #invoke or defaults.
config = frame.args
numdot = config.numdot or '.' -- decimal mark before fractional digits
numsep = config.numsep or ',' -- group separator for numbers (',', '.', '')
maxsigfig = config.maxsigfig or 14 -- maximum number of significant figures
-- Scribunto sets the global variable 'mw'.
-- A testing program can set the global variable 'is_test_run'.
local data_module, text_module, data_code, text_code
if is_test_run then
local langcode = mw.language.getContentLanguage().code
data_module = "convertdata-" .. langcode
text_module = "converttext-" .. langcode
extra_module = "convertextra-" .. langcode
spell_module = "ConvertNumeric"
else
local sandbox = boolean(config.sandbox) and '/sandbox' or ''
data_module = "Module:Convert/data" .. sandbox
text_module = "Module:Convert/text" .. sandbox
extra_module = "Module:Convert/extra" .. sandbox
spell_module = "Module:ConvertNumeric"
end
data_code = mw.loadData(data_module)
text_code = mw.loadData(text_module)
default_exceptions = data_code.default_exceptions
link_exceptions = data_code.link_exceptions
all_units = data_code.all_units
SIprefixes = text_code.SIprefixes
all_categories = text_code.all_categories
all_messages = text_code.all_messages
customary_units = text_code.customary_units
disp_joins = text_code.disp_joins
en_option_name = text_code.en_option_name
en_option_value = text_code.en_option_value
eng_scales = text_code.eng_scales
range_aliases = text_code.range_aliases
range_types = text_code.range_types
local translation = text_code.translation_table
if translation then
if translation.group then
group_method = translation.group
end
if translation.per_word then
per_word = translation.per_word
end
if translation.plural_suffix then
plural_suffix = translation.plural_suffix
end
from_en_table = translation.from_en
local use_workaround = true
if use_workaround then
-- 2013-07-05 workaround bug by making a copy of the required table.
-- mw.ustring.gsub fails with a table (to_en_table) as the replacement,
-- if the table is accessed via mw.loadData.
local source = translation.to_en
if source then
to_en_table = {}
for k, v in pairs(source) do
to_en_table[k] = v
end
end
else
to_en_table = translation.to_en
end
end
end
local function collection()
-- Return a table to hold items.
return {
n = 0,
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
}
end
local function split(text, delimiter)
-- Return a numbered table with fields from splitting text.
-- The delimiter is used in a regex without escaping (for example, '.' would fail).
-- Each field has any leading/trailing whitespace removed.
local t = {}
text = text .. delimiter -- to get last item
for item in text:gmatch('%s*(.-)%s*' .. delimiter) do
table.insert(t, item)
end
return t
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
local function wanted_category(cat)
-- Return cat if it is wanted in current namespace, otherwise return nil.
-- This is so tracking categories only include pages that need correction.
-- Default wanted namespaces are 0 (article) and 10 (template).
local title = mw.title.getCurrentTitle()
if title then
local namespace = title.namespace
for _, v in ipairs(split(config.nscat or '0,10', ',')) do
if namespace == tonumber(v) then
return cat
end
end
end
end
local function message(mcode)
-- Return wikitext for an error message, including category if specified
-- for the message type.
-- mcode = numbered table specifying the message:
-- mcode[1] = 'cvt_xxx' (string used as a key to get message info)
-- mcode[2] = 'parm1' (string to replace first %s if any in message)
-- mcode[3] = 'parm2' (string to replace second %s if any in message)
-- mcode[4] = 'parm3' (string to replace third %s if any in message)
local msg = all_messages[mcode[1]]
if msg then
local title = format(msg[1] or 'Missing message',
mcode[2] or '?',
mcode[3] or '?',
mcode[4] or '?')
local text = msg[2] or 'Missing message'
local cat = wanted_category(all_categories[msg[3]]) or ''
local anchor = msg[4] or ''
local fmt = all_messages['cvt_format'] or 'convert: bug'
local regex, replace = msg.regex, msg.replace
if regex and replace then
title = title:gsub(regex, replace)
end
title = title:gsub('"', '"')
return format(fmt, anchor, title, text, cat)
end
return 'Convert internal error: unknown message'
end
local function add_warning(parms, mcode, text)
-- If enabled, add a warning that will be displayed after the convert result.
-- To reduce output noise, only the first warning is displayed.
if boolean(config.warnings) then
if parms.warnings == nil then
parms.warnings = message({ mcode, text })
end
end
end
local function spell_number(parms, number, numerator, denominator)
-- Return result of spelling (number, numerator, denominator), or
-- return nil if spelling is not available or not supported for given text.
-- Examples (each value must be a string or nil):
-- number numerator denominator output
-- ------ --------- ----------- -------------------
-- "1.23" nil nil one point two three
-- "1" "2" "3" one and two thirds
-- nil "2" "3" two thirds
if not speller then
local function get_speller(module)
return require(module).spell_number
end
local success
success, speller = pcall(get_speller, spell_module)
if not success or type(speller) ~= 'function' then
add_warning(parms, 'cvt_no_spell')
return nil
end
end
local case = parms.opt_spell_upper
parms.opt_spell_upper = nil -- only uppercase first number in a multiple unit
local sp = not parms.opt_sp_us
local adj = parms.opt_adjectival
return speller(number, numerator, denominator, case, sp, adj)
end
------------------------------------------------------------------------
-- BEGIN: Code required only for built-in units.
-- LATER: If need much more code, move to another module to simplify this module.
local function speed_of_sound(altitude)
-- This is for the Mach built-in unit of speed.
-- Return speed of sound in metres per second at given altitude in feet.
-- If no altitude given, use default (zero altitude = sea level).
-- Table gives speed of sound in miles per hour at various altitudes:
-- altitude = -17,499 to 302,499 feet
-- mach_table[a + 4] = s where
-- a = (altitude / 5000) rounded to nearest integer (-3 to 60)
-- s = speed of sound (mph) at that altitude
-- LATER: Should calculate result from an interpolation between the next
-- lower and higher altitudes in table, rather than rounding to nearest.
-- From: http://www.aerospaceweb.org/question/atmosphere/q0112.shtml
local mach_table = { -- a =
799.5, 787.0, 774.2, 761.207051, -- -3 to 0
748.0, 734.6, 721.0, 707.0, 692.8, 678.3, 663.5, 660.1, 660.1, 660.1, -- 1 to 10
660.1, 660.1, 660.1, 662.0, 664.3, 666.5, 668.9, 671.1, 673.4, 675.6, -- 11 to 20
677.9, 683.7, 689.9, 696.0, 702.1, 708.1, 714.0, 719.9, 725.8, 731.6, -- 21 to 30
737.3, 737.7, 737.7, 736.2, 730.5, 724.6, 718.8, 712.9, 707.0, 701.1, -- 31 to 40
695.0, 688.9, 682.8, 676.6, 670.4, 664.1, 657.8, 652.9, 648.3, 643.7, -- 41 to 50
639.1, 634.4, 629.6, 624.8, 620.0, 615.2, 613.2, 613.2, 613.2, 613.5, -- 51 to 60
}
altitude = altitude or 0
local a = (altitude < 0) and -altitude or altitude
a = floor(a / 5000 + 0.5)
if altitude < 0 then
a = -a
end
if a < -3 then
a = -3
elseif a > 60 then
a = 60
end
return mach_table[a + 4] * 0.44704 -- mph converted to m/s
end
-- END: Code required only for built-in units.
------------------------------------------------------------------------
local function check_mismatch(unit1, unit2)
-- If unit1 cannot be converted to unit2, return an error message table.
-- This allows conversion between units of the same type, and between
-- Nm (normally torque) and ftlb (energy), as in gun-related articles.
-- This works because Nm is the base unit (scale = 1) for both the
-- primary type (torque), and the alternate type (energy, where Nm = J).
-- A match occurs if the primary types are the same, or if unit1 matches
-- the alternate type of unit2, and vice versa. That provides a whitelist
-- of which conversions are permitted between normally incompatible types.
if unit1.utype == unit2.utype or
(unit1.utype == unit2.alttype and unit1.alttype == unit2.utype) then
return nil
end
return { 'cvt_mismatch', unit1.utype, unit2.utype }
end
local function override_from(out_table, in_table, fields)
-- Copy the specified fields from in_table to out_table, but do not
-- copy nil fields (keep any corresponding field in out_table).
for _, field in ipairs(fields) do
if in_table[field] then
out_table[field] = in_table[field]
end
end
end
local function shallow_copy(t)
-- Return a shallow copy of table t.
-- Do not need the features and overhead of the Scribunto mw.clone().
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 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 .. plural_suffix
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 plural_suffix.
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 .. plural_suffix
else
value = self.name2
end
elseif key == 'link' then
value = self.name1
elseif key == 'builtin' then
value = false
else
return 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
-- prefix_position is a byte (not character) position, so use Lua's sub().
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 .. plural_suffix
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 .. plural_suffix
else
value = self.name2
end
elseif key == 'link' then
value = self.name1
elseif key == 'builtin' then
value = false
else
return nil
end
rawset(self, key, value)
return value
end
}
local unit_per_mt = {
-- Metatable to get values for a "per" unit of form "x/y".
-- This is never called to determine a unit name or link because "per" units
-- are handled as a special case.
__index = function (self, key)
local value
if key == 'symbol' then
local per = self.per
local unit1, unit2 = per[1], per[2]
if unit1 then
value = unit1[key] .. '/' .. unit2[key]
else
value = '/' .. unit2[key]
end
elseif key == 'sym_us' then
value = self.symbol
elseif key == 'scale' then
local per = self.per
local unit1, unit2 = per[1], per[2]
value = (unit1 and unit1.scale or 1) * self.scalemultiplier / unit2.scale
elseif key == 'builtin' then
value = false
else
return nil
end
rawset(self, key, value)
return value
end
}
local function lookup(unitcode, opt_sp_us, what, utable, fails, depth)
-- 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 opt_sp_us is true for US spelling of SI prefixes and
-- the symbol and name of the unit. If true, 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 output multiple
-- 'only_multiple' : single unit or output 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 output multiple unit tables is included in the result.
-- For compatibility with the old template, an underscore in a unitcode is
-- replaced with a space so usage like {{convert|350|board_feet}} works.
-- Wikignomes may also put two spaces or " " in combinations, so
-- replace underscore, " ", and multiple spaces with a single space.
utable = utable or all_units
fails = fails or {}
depth = depth and depth + 1 or 1
if depth > 9 then
-- There are ways to mistakenly define units which result in infinite
-- recursion when lookup() is called. That gives a long delay and very
-- confusing error messages, so the depth parameter is used as a guard.
return false, { 'cvt_lookup', unitcode }
end
if unitcode == nil or unitcode == '' then
return false, { 'cvt_no_unit' }
end
unitcode = unitcode:gsub('_', ' '):gsub(' ', ' '):gsub(' +', ' ')
local t = utable[unitcode]
if t then
if t.shouldbe then
return false, { 'cvt_should_be', t.shouldbe }
end
local force_sp_us = opt_sp_us
if t.sp_us then
force_sp_us = true
opt_sp_us = true
end
local target = t.target -- nil, or unitcode is an alias for this target
if target then
local success, result = lookup(target, opt_sp_us, what, utable, fails, depth)
if not success then return false, result end
override_from(result, t, { 'customary', 'default', 'link', 'symbol', 'symlink' })
local multiplier = t.multiplier
if multiplier then
result.multiplier = tostring(multiplier)
result.scale = result.scale * multiplier
end
return true, result
end
local per = t.per -- nil/false, or a numbered table for "x/y" units
if per then
local result = { utype = t.utype, per = {} }
result.scalemultiplier = t.multiplier or 1
override_from(result, t, { 'invert', 'iscomplex', 'default', 'link', 'symbol', 'symlink' })
result.symbol_raw = (result.symbol or false) -- to distinguish between a defined exception and a metatable calculation
local cvt = result.per
local prefix
for i, v in ipairs(per) do
if i == 1 and (v == '$' or v == '£') then
prefix = v
else
local success, t = lookup(v, opt_sp_us, 'no_combination', utable, fails, depth)
if not success then return false, t end
cvt[i] = t
if t.sp_us then -- if the top or bottom unit forces sp=us, set the per unit to use the correct name/symbol
force_sp_us = true
end
end
end
if prefix then
result.vprefix = prefix
else
result.vprefix = false -- to avoid calling __index
end
result.sp_us = force_sp_us
return true, setmetatable(result, unit_per_mt)
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 result = { utype = t.utype, multiple = multiple, combination = {} }
local cvt = result.combination
for i, v in ipairs(combo) do
local success, t = lookup(v, opt_sp_us, multiple and 'no_combination' or 'only_multiple', utable, fails, depth)
if not success then return false, t end
cvt[i] = t
end
return true, result
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 = SIprefixes[1] or 2, 1, -1 do
-- Look for an SI prefix; should never occur with an alias.
-- Check for longer prefix first ('dam' is decametre).
-- SIprefixes[1] = prefix maximum #characters (as seen by mw.ustring.sub).
local prefix = usub(unitcode, 1, plen)
local si = SIprefixes[prefix]
if si then
local t = utable[usub(unitcode, plen+1)]
if t and t.prefixes then
local result = shallow_copy(t)
if opt_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 "e3e6cuft",
-- and not if the unit is a combination or multiple,
-- and not if the unit has an offset or is a built-in.
-- Only en digits are accepted.
local exponent, baseunit = unitcode:match('^e(%d+)(.*)')
if exponent then
local engscale = eng_scales[exponent]
if engscale then
local success, result = lookup(baseunit, opt_sp_us, 'no_combination', utable, fails, depth)
if not success then return false, result end
if not (result.offset or result.builtin or result.engscale) then
result.defkey = unitcode -- key to lookup default exception
result.engscale = engscale
result.scale = result.scale * 10 ^ tonumber(exponent)
return true, result
end
end
end
-- Accept user-defined combinations like "acre+m2+ha" or "acre m2 ha" for output.
-- If '+' is used, each unit code can include a space, and any error is fatal.
-- If ' ' is used and if each space-separated word is a unit code, it is a combo,
-- but errors are not fatal so the unit code can be looked up as an extra unit.
local err_is_fatal
local combo = collection()
if unitcode:find('+', 1, true) then
err_is_fatal = true
for item in (unitcode .. '+'):gmatch('%s*(.-)%s*%+') do
if item ~= '' then
combo:add(item)
end
end
elseif unitcode:find('%s') then
for item in unitcode:gmatch('%S+') do
combo:add(item)
end
end
if combo.n > 1 then
local function lookup_combo()
if what == 'no_combination' or what == 'only_multiple' then
return false, { 'cvt_bad_unit', unitcode }
end
local result = { combination = {} }
local cvt = result.combination
for i, v in ipairs(combo) do
local success, t = lookup(v, opt_sp_us, 'no_combination', utable, fails, depth)
if not success then return false, t end
if i == 1 then
result.utype = t.utype
else
local mismatch = check_mismatch(result, t)
if mismatch then
return false, mismatch
end
end
cvt[i] = t
end
return true, result
end
local success, result = lookup_combo()
if success or err_is_fatal then
return success, result
end
end
if not extra_units then
local success, extra = pcall(function () return require(extra_module).extra_units end)
if success and type(extra) == 'table' then
extra_units = extra
end
end
if extra_units then
-- A unit in one data table might refer to a unit in the other table, so
-- switch between them, relying on fails or depth to terminate loops.
if not fails[unitcode] then
fails[unitcode] = true
local other = (utable == all_units) and extra_units or all_units
local success, result = lookup(unitcode, opt_sp_us, what, other, fails, depth)
if success then
return true, result
end
end
end
return false, { 'cvt_unknown', unitcode }
end
local function valid_number(num)
-- Return true if num is a valid number.
-- In Scribunto (different from some standard Lua), when expressed as a string,
-- overflow or other problems are indicated with text like "inf" or "nan"
-- which are regarded as invalid here (each contains "n").
if type(num) == 'number' and tostring(num):find('n', 1, true) == nil then
return true
end
end
local function ntsh(num, 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 == true, output is in a box (not hidden).
-- This implements Template:Ntsh (number table sorting, hidden).
local result, style
if not valid_number(num) then
if num < 0 then
result = '1000000000000000000'
else
result = '9000000000000000000'
end
elseif num == 0 then
result = '5000000000000000000'
else
local mag = floor(log10(abs(num)) + 1e-14)
local prefix
if num > 0 then
prefix = 7000 + mag
else
prefix = 2999 - mag
num = num + 10^(mag+1)
end
result = format('%d', prefix) .. format('%015.0f', floor(num * 10^(14-mag)))
end
if debug then
style = 'border:1px solid'
else
style = 'display:none'
end
return '<span style="' .. style .. '">' .. result .. '</span>'
end
local function hyphenated(name, parts)
-- Return a hyphenated form of given name (for adjectival usage).
-- The name may be linked and the target of the link must not be changed.
-- Hypothetical examples:
-- [[long ton|ton]] → [[long ton|ton]] (no change)
-- [[tonne|long ton]] → [[tonne|long-ton]]
-- [[metric ton|long ton]] → [[metric ton|long-ton]]
-- [[long ton]] → [[long ton|long-ton]]
-- Input can also have multiple links in a single name like:
-- [[United States customary units|U.S.]] [[US gallon|gallon]]
-- [[mile]]s per [[United States customary units|U.S.]] [[quart]]
-- [[long ton]]s per [[short ton]]
-- Assume that links cannot be nested (never like "[[abc[[def]]ghi]]").
-- 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)"].
if name:find(' ', 1, true) then
if parts then
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
parts = collection()
for before, item, after in name:gmatch('([^[]*)(%[%[[^[]*%]%])([^[]*)') do
if item:find(' ', 1, true) then
local prefix
local plen = item:find('|', 1, true)
if plen then
prefix = item:sub(1, plen)
item = item:sub(plen + 1, -3)
else
prefix = item:sub(1, -3) .. '|'
item = item:sub(3, -3)
end
item = prefix .. hyphenated(item, parts) .. ']]'
end
parts:add(before:gsub(' ', '-') .. item .. after:gsub(' ', '-'))
end
if parts.n == 0 then
-- No link like "[[...]]" was found in the original name.
parts:add(hyphenated(name, parts))
end
return table.concat(parts)
end
return name
end
local function hyphenated_maybe(parms, want_name, sep, id, inout)
-- Return s, f where
-- s = id, possibly modified
-- f = true if hyphenated
-- Possible modifications: hyphenate; prepend '-'; append mid text.
if id == nil or id == '' then
return ''
end
local mid
if parms.opt_adjectival then
if inout == (parms.opt_flip and 'out' or 'in') then
mid = parms.mid
end
if want_name then
return '-' .. hyphenated(id) .. (mid or ''), true
end
end
return sep .. id .. (mid or '')
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 digit_grouper(method, gaps)
-- Return a table to hold groups of digits which can be joined with
-- suitable separators (such as commas).
-- Each group is separately translated to the local language because
-- gap separators include digits which should not be translated.
-- Parameter method is a number or nil:
-- 3 for 3-digit grouping, or
-- 2 for 3-then-2 grouping.
-- Parameter gaps is true to use <span> gaps (numsep ignored).
return {
n = 0,
add = function (self, digits)
self.n = self.n + 1
self[self.n] = from_en(digits)
end,
join = function (self, rhs)
-- Concatenate in reverse order.
if gaps then
local result = ''
for i = 1, self.n - 1 do
result = '<span style="margin-left: 0.25em">' .. self[i] .. '</span>' .. result
end
return '<span style="white-space: nowrap">' .. self[self.n] .. result .. from_en(rhs) .. '</span>'
else
local result = self[1]
for i = 2, self.n do
result = self[i] .. numsep .. result
end
return result .. from_en(rhs)
end
end,
step = 3,
next_position = function (self, previous)
-- Return position of digit just before next group.
-- Digits are grouped from right-to-left (least significant first).
local result = previous - self.step
if method == 2 then
self.step = 2
end
return (result < 0) and 0 or result
end,
}
end
local function with_separator(parms, text)
-- Input text is a number in en digits and with '.' decimal mark.
-- Return an equivalent of text, formatted for display:
-- with a custom decimal mark instead of '.', if wanted
-- with thousand separators inserted, if wanted
-- digits in local language
-- 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 the decimal mark, and not after 'e' or 'E').
if parms.opt_nocomma or numsep == '' then
return from_en(text)
end
local last = text:match('()[.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 or (last == 4 and parms.opt_comma5) then
return from_en(text)
end
local groups = digit_grouper(group_method, parms.opt_gaps)
local i = last
while i > 0 do
local position = groups:next_position(i)
groups:add(text:sub(position+1, i))
i = position
end
return groups:join(text:sub(last+1))
end
-- Input values can use values like 1.23e12, but are never displayed
-- using scientific notation like 1.23×10¹².
-- Very small or very large output values use scientific notation.
-- Use format(fmtpower, significand, '10', exponent) where each arg is a string.
local fmtpower = '%s<span style="margin:0 .15em 0 .25em">×</span>%s<sup>%s</sup>'
local function with_exponent(show, exponent)
-- Return wikitext to display the implied value in scientific notation.
-- Input uses en digits; output uses digits in local language.
if #show > 1 then
show = show:sub(1, 1) .. '.' .. show:sub(2)
end
return format(fmtpower, from_en(show), from_en('10'), use_minus(from_en(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(log10(value))
if frac >= 0 then
frac = frac - 1
exp = exp + 1
end
local digits = format('%.0f', 10^(frac + sigfig))
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(parms, show, exponent, isnegative)
-- Parameter show is a number in en digits and with '.' decimal mark.
-- Return t where t is a table with fields:
-- show = wikitext formatted to display implied value
-- (digits in local language)
-- is_scientific = true if show uses scientific notation
-- clean = unformatted show (possibly adjusted and with inserted '.')
-- (en digits)
-- 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:
-- * Is for an output value and is spelled if wanted and possible.
-- * Includes a Unicode minus if isnegative.
-- * Uses a custom decimal mark, if wanted.
-- * Has digits grouped where necessary, if wanted.
-- * Uses scientific notation for very small or large values
-- (which forces output to not be spelled).
-- * 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*)(%.?)(.*)')
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.' .. zeros:sub(1, maxlen)
end
elseif #zeros >= 4 then
show = figs
exponent = -#zeros
elseif #figs > maxlen then
show = '0.' .. 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.' .. string.rep('0', -exponent) .. show
else
show = show:sub(1, exponent) .. '.' .. 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
local formatted_show = sign .. with_separator(parms, show)
if parms.opt_spell_out then
formatted_show = spell_number(parms, sign .. show) or formatted_show
end
return {
clean = show,
sign = sign,
show = formatted_show,
}
end
-- Fraction output format.
-- 2013-07-20 Trying new styles proposed at [[Template talk:Convert]].
local fracfmt = {
{ -- Like {{frac}} (fraction slash).
-- 1/2 : sign, numerator, denominator
-- 1+2/3 : signed_wholenumber, numerator, denominator
'<span class="frac nowrap">%s<sup>%s</sup>⁄<sub>%s</sub></span>',
'<span class="frac nowrap">%s<sup> %s</sup>⁄<sub>%s</sub></span>',
},
{ -- Like {{sfrac}} (fraction horizontal bar).
-- 1//2 : sign, numerator, denominator (sign should probably be before the fraction, but then it can wrap, and html is already too long)
-- 1+2//3 : signed_wholenumber, numerator, denominator
'<span class="sfrac nowrap" style="display:inline-block; vertical-align:-0.5em; font-size:85%%; text-align:center;"><span style="display:block; line-height:1em; padding:0 0.1em;">%s%s</span><span style="display:none;">/</span><span style="display:block; line-height:1em; padding:0 0.1em; border-top:1px solid;">%s</span></span>',
'<span class="sfrac nowrap">%s<span style="display:none;"> </span><span style="display:inline-block; vertical-align:-0.5em; font-size:85%%; text-align:center;"><span style="display:block; line-height:1em; padding:0 0.1em;">%s</span><span style="display:none;">/</span><span style="display:block; line-height:1em; padding:0 0.1em; border-top:1px solid;">%s</span></span></span>',
},
{ -- Like old {{convert}} template.
-- 1///2 : sign, numerator, denominator
-- 1+2///3: signed_wholenumber, sign, numerator, denominator
'<span style="white-space:nowrap">%s<sup>%s</sup>⁄<sub>%s</sub></span>',
'<span class="frac nowrap">%s<s style="display:none">%s</s><sup>%s</sup>⁄<sub>%s</sub></span>',
},
}
local function extract_fraction(parms, text, negative)
-- If text represents a fraction, return
-- value, altvalue, show, spelled, denominator
-- where
-- value is a number (value of the fraction in argument text)
-- altvalue is an alternate interpretation of any fraction for the hands
-- unit where "14.1+3/4" means 14 hands 1.75 inches!
-- show is a string (formatted text for display of an input value,
-- and is spelled if wanted and possible)
-- spelled is true if show was spelled
-- denominator is value of the denominator in the fraction
-- Otherwise, return nil.
-- Input uses en digits and '.' decimal mark (input has been translated).
-- Output uses digits in local language and custom decimal mark, if any.
--
-- 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 numstr, whole, value, altvalue
local lhs, slash, denstr = text:match('^%s*([^/]-)%s*(/+)%s*(.-)%s*$')
local denominator = tonumber(denstr)
if denominator == nil then return nil end
local 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 == '-')
local numerator = tonumber(numstr)
if numerator == nil then return nil end
-- Spelling of silly inputs like "-2+3/8" or "2+3/+8" (mixed or excess signs) is not supported.
local do_spell
if negative == negfrac or wholestr == nil then
value = whole + numerator / denominator
altvalue = whole + numerator / (denominator * 10)
do_spell = parms.opt_spell_in
if do_spell then
if not (numstr:match('^%d') and denstr:match('^%d')) then -- if either has a sign
do_spell = false
end
end
else
value = whole - numerator / denominator
altvalue = whole - numerator / (denominator * 10)
numstr = change_sign(numstr)
do_spell = false
end
if not valid_number(value) then
return nil -- overflow or similar
end
numstr = use_minus(numstr)
denstr = use_minus(denstr)
local style = #slash -- kludge: 1, 2, or 3 slashes can be used to select style
if style > 3 then style = 3 end
local wikitext
if wholestr then
if negative then
wholestr = change_sign(wholestr)
end
local fmt = fracfmt[style][2]
if style < 3 then
wikitext = format(fmt, use_minus(from_en(wholestr)), from_en(numstr), from_en(denstr))
else
local sign = negative and MINUS or '+'
wikitext = format(fmt, use_minus(from_en(wholestr)), sign, from_en(numstr), from_en(denstr))
end
else
local sign = negative and MINUS or ''
wikitext = format(fracfmt[style][1], sign, from_en(numstr), from_en(denstr))
end
if do_spell then
local numsign = (wholestr or not negative) and '' or '-'
wikitext = spell_number(parms, wholestr, numsign .. numstr, denstr) or wikitext
end
return value, altvalue, wikitext, do_spell, denominator
end
local function extract_number(parms, text, another, no_fraction)
-- Return true, info if can extract a number from text,
-- where info is a table with the result,
-- or return false, t where t is an error message table.
-- Input can use en digits or digits in local language.
-- Parameter another = true if the expected value is not the first.
-- 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
-- altvalue = a valid number, usually same as value but different
-- if fraction used (for hands unit)
-- 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
-- (en digits and '.' decimal mark)
-- show = text formatted for output
-- (digits in local language and custom decimal mark)
-- The resulting show:
-- * Is for an input value and is spelled if wanted and possible.
-- * Has a rounded value, if wanted.
-- * Has digits grouped where necessary, if wanted.
-- * If negative, a Unicode minus is used; otherwise the sign is
-- '+' (if the input text used '+'), or is '' (if no sign in input).
text = strip(text or '')
local clean = to_en(text)
if clean == '' then
return false, { another and 'cvt_no_num2' or 'cvt_no_num' }
end
local isnegative, propersign = false, '' -- most common case
local singular, show, denominator
local value = tonumber(clean)
local altvalue
if value then
local sign = clean:sub(1, 1)
if sign == '+' or sign == '-' then
propersign = (sign == '+') and '+' or MINUS
clean = clean:sub(2)
end
if value < 0 then
isnegative = true
value = -value
end
else
local valstr
for _, prefix in ipairs({ '-', MINUS, '−' }) do
-- Including '-' means inputs like '- 2' (with space) are accepted as -2.
-- It also sets isnegative in case input is a fraction like '-2-3/4'.
local plen = #prefix
if clean:sub(1, plen) == prefix then
valstr = clean:sub(plen + 1)
break
end
end
if valstr then
isnegative = true
propersign = MINUS
clean = valstr
value = tonumber(clean)
end
if value == nil then
local spelled
if not no_fraction then
value, altvalue, show, spelled, denominator = extract_fraction(parms, clean, isnegative)
end
if value == nil then
return false, { 'cvt_bad_num', text }
end
if spelled and value <= 1 then
singular = true -- for example, "one half mile" (singular unit)
else
singular = false -- any numeric fraction (even with value 1) is regarded as plural
end
end
end
if not valid_number(value) then -- for example, "1e310" may overflow
return false, { 'cvt_invalid_num' }
end
if show == nil then
singular = (value == 1 and not isnegative)
local precision = parms.input_precision
if precision and 0 <= precision and precision <= 8 then
value = value + 2e-14 -- fudge for some common cases of bad rounding
local fmt = '%.' .. format('%d', precision) .. 'f'
show = fmt:format(value)
else
show = clean
end
show = propersign .. with_separator(parms, show)
if parms.opt_spell_in then
show = spell_number(parms, propersign .. clean) or show
end
end
if isnegative and (value ~= 0) then
value = -value
end
return true, {
value = value,
altvalue = altvalue or value,
singular = singular,
clean = clean,
show = show,
denominator = denominator,
}
end
local function get_number(text)
-- Return v, f where:
-- v = nil (text is not a number)
-- or
-- v = value of text (text is a number)
-- f = true if value is an integer
-- Input can use en digits or digits in local language,
-- but no separators, no Unicode minus, and no fraction.
if text then
local number = tonumber(to_en(text))
if number then
local integer, fraction = math.modf(number)
return number, (fraction == 0)
end
end
end
local function preunits(count, preunit1, preunit2)
-- If count is 1:
-- ignore preunit2
-- return p1
-- else:
-- preunit1 is used for preunit2 if the latter is empty
-- return p1, p2
-- where:
-- p1 is text to insert before the input unit
-- p2 is text to insert before the output unit
-- p1 or p2 may be nil to mean "no preunit"
-- Using '+ ' gives output like "5+ feet" (no preceding space).
local function withspace(text, i)
-- Insert space at beginning if i == 1, or at end if i == -1.
-- However, no space is inserted if there is a space or ' '
-- or '-' at that position ('-' is for adjectival text).
local current = text:sub(i, i)
if current == ' ' or current == '-' then
return text
end
if i == 1 then
current = text:sub(1, 6)
else
current = text:sub(-6, -1)
end
if current == ' ' then
return text
end
if i == 1 then
return ' ' .. text
end
return text .. ' '
end
preunit1 = preunit1 or ''
local trim1 = strip(preunit1)
if count == 1 then
if trim1 == '' then
return nil
end
return withspace(withspace(preunit1, 1), -1)
end
preunit2 = preunit2 or ''
local trim2 = strip(preunit2)
if trim1 == '' and trim2 == '' then
return nil, nil
end
if trim1 ~= '+' then
preunit1 = withspace(preunit1, 1)
end
if trim2 == ' ' then -- trick to make preunit2 empty
preunit2 = nil
elseif trim2 == '' then
preunit2 = preunit1
elseif trim2 ~= '+' then
preunit2 = withspace(preunit2, 1)
end
return preunit1, preunit2
end
local function range_text(range, want_name, parms, before, after)
-- Return before .. rtext .. after
-- where rtext is the text that separates two values in a range.
local rtext, adj_text, exception
if type(range) == 'table' then
-- Table must specify range text for abbr=off and for abbr=on,
-- and may specify range text for 'adj=on',
-- and may specify exception = true.
rtext = range[want_name and 'off' or 'on']
adj_text = range['adj']
exception = range['exception']
else
rtext = range
end
if parms.opt_adjectival then
if want_name or (exception and parms.abbr_org == 'on') then
rtext = adj_text or rtext:gsub(' ', '-'):gsub(' ', '-')
end
end
if rtext == '–' and after:sub(1, #MINUS) == MINUS then
rtext = ' – '
end
return before .. rtext .. after
end
local function get_composite(parms, iparm, total, in_unit_table)
-- Look for a composite input unit. For example, "{{convert|1|yd|2|ft|3|in}}"
-- would result in a call to this function with
-- iparm = 3 (parms[iparm] = "2", just after the first unit)
-- total = 1 (number of yards)
-- in_unit_table = (unit table for "yd")
-- Return true, iparm, unit where
-- iparm = index just after the composite units (7 in above example)
-- unit = composite unit table holding all input units,
-- or return true if no composite unit is present in parms,
-- or return false, t where t is an error message table.
local default, subinfo
local composite_units, count = { in_unit_table }, 1
local fixups = {}
local subunit = in_unit_table
while subunit.subdivs do -- subdivs is nil or a table of allowed subdivisions
local subcode = strip(parms[iparm+1])
local subdiv = subunit.subdivs[subcode]
if not subdiv then
break
end
local success
success, subunit = lookup(subcode, parms.opt_sp_us, 'no_combination')
if not success then return false, subunit end -- should never occur
success, subinfo = extract_number(parms, parms[iparm])
if not success then return false, subinfo end
iparm = iparm + 2
subunit.inout = 'in'
subunit.valinfo = { subinfo }
-- Recalculate total as a number of subdivisions.
-- subdiv[1] = number of subdivisions per previous unit (integer > 1).
total = total * subdiv[1] + subinfo.value
if not default then -- set by the first subdiv with a default defined
default = subdiv.default
end
count = count + 1
composite_units[count] = subunit
if subdiv.unit or subdiv.name then
fixups[count] = { unit = subdiv.unit, name = subdiv.name, valinfo = subunit.valinfo }
end
end
if count == 1 then
return true -- no error and no composite unit
end
for i, fixup in pairs(fixups) do
local unit = fixup.unit
local name = fixup.name
if not unit or (count > 2 and name) then
composite_units[i].fixed_name = name
else
local success, alternate = lookup(unit, parms.opt_sp_us, 'no_combination')
if not success then return false, alternate end -- should never occur
alternate.inout = 'in'
alternate.valinfo = fixup.valinfo
composite_units[i] = alternate
end
end
return true, iparm, {
utype = in_unit_table.utype,
scale = subunit.scale, -- scale of last (least significant) unit
valinfo = { { value = total, clean = subinfo.clean, denominator = subinfo.denominator } },
composite = composite_units,
default = default or in_unit_table.default
}
end
local function translate_parms(parms, kv_pairs)
-- Update fields in parms by translating each key:value in kv_pairs to terms
-- used by this module (may involve translating from local language to English).
-- Also, checks are performed which may display warnings, if enabled.
-- Return true if successful or return false, t where t is an error message table.
if kv_pairs.adj and kv_pairs.sing then
-- For en.wiki (before translation), warn if attempt to use adj and sing
-- as the latter is a deprecated alias for the former.
if kv_pairs.adj ~= kv_pairs.sing and kv_pairs.sing ~= '' then
add_warning(parms, 'cvt_unknown_option', 'sing=' .. kv_pairs.sing)
end
kv_pairs.sing = nil
end
for loc_name, loc_value in pairs(kv_pairs) do
local en_name = en_option_name[loc_name]
if en_name then
local en_value
if en_name == 'sigfig' then
if loc_value == '' then
add_warning(parms, 'cvt_empty_option', loc_name)
else
local number, is_integer = get_number(loc_value)
if number and is_integer and number > 0 then
en_value = number
else
add_warning(parms, 'cvt_bad_sigfig', loc_value)
end
end
else
en_value = en_option_value[en_name][loc_value]
if en_value == nil then
if loc_value == '' then
add_warning(parms, 'cvt_empty_option', loc_name)
else
-- loc_value can no longer be nil here (at one time, that could occur
-- with aliases like |sing=off|adj=on), but am retaining safety check.
local text = loc_value and (loc_name .. '=' .. loc_value) or loc_name
add_warning(parms, 'cvt_unknown_option', text)
end
elseif en_value == '' then
en_value = nil -- an ignored option like adj=off
elseif type(en_value) == 'string' and en_value:sub(1, 4) == 'opt_' then
for _, v in ipairs(split(en_value, ',')) do
parms[v] = true
end
en_value = nil
end
end
parms[en_name] = en_value
else
add_warning(parms, 'cvt_unknown_option', loc_name .. '=' .. loc_value)
end
end
if parms.adj then
if parms.adj:sub(1, 2) == 'ri' then
-- It is known that adj is 'ri1' or 'ri2' or 'ri3', so precision is valid.
-- Only en digits are accepted.
parms.input_precision = tonumber(parms.adj:sub(-1))
parms.adj = nil
end
end
if parms.abbr then
parms.abbr_org = parms.abbr -- original abbr that was set, before any flip
else
parms.abbr = 'out' -- default is to abbreviate output only (use symbol, not name)
end
if parms.opt_flip then
local function swap_in_out(option)
local value = parms[option]
if value == 'in' then
parms[option] = 'out'
elseif value == 'out' then
parms[option] = 'in'
end
end
swap_in_out('abbr')
swap_in_out('lk')
if parms.opt_spell_in then
-- For simplicity, and because it does not appear to be needed,
-- user cannot set an option to spell the output.
parms.opt_spell_in = nil
parms.opt_spell_out = true
end
end
if parms.opt_table or parms.opt_tablecen then
if parms.abbr_org == nil and parms.lk == nil then
parms.opt_values = true
end
local align = format('align="%s"', parms.opt_table and 'right' or 'center')
parms.table_joins = { align .. '|', '\n|' .. align .. '|' }
end
if parms.opt_lang_en then
from_en_table = nil
end
return true
end
local function get_values(parms)
-- If successful, update parms and return true, v, i where
-- v = table of input values
-- i = index to next entry in parms after those processed here
-- or return false, t where t is an error message table.
local valinfo = collection() -- numbered table of input values
local range = collection() -- numbered table of range items (having, for example, 2 range items requires 3 input values)
local had_nocomma -- true if removed "nocomma" kludge from second parameter (like "tonocomma")
local parm2 = strip(parms[2])
if parm2 and parm2:sub(-7, -1) == 'nocomma' then
parms[2] = strip(parm2:sub(1, -8))
parms.opt_nocomma = true
had_nocomma = true
end
local i = 1
while true do
local success, info = extract_number(parms, parms[i], i > 1) -- need to set parms.opt_nocomma before calling this
if not success then return false, info end
i = i + 1
valinfo:add(info)
local next = strip(parms[i])
local range_item = range_types[next] or range_types[range_aliases[next]]
if not range_item then
break
end
i = i + 1
range:add(range_item)
parms.is_range_x = (type(range_item) == 'table') and range_item.is_range_x or nil
end
if range.n > 0 then
if range.n > 30 then -- limit abuse, although 4 is a more likely upper limit
return false, { 'cvt_invalid_num' } -- misleading message but it will do
end
parms.range = range
elseif had_nocomma then
return false, { 'cvt_unknown', parm2 }
end
return true, valinfo, i
end
local function get_parms(pframe)
-- If successful, return true, parms, unit where
-- parms 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.
-- The returned input unit table may be for a fake unit using the specified
-- unit code as the symbol and name, and with bad_mcode = message code table.
-- 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 parms = {} -- arguments passed to template, after translation
local kv_pairs = {} -- table of input key:value pairs where key is a name; needed because cannot iterate parms and add new fields to it
for k, v in pairs(pframe.args) do
if type(k) == 'number' or k == 'test' then -- parameter "test" is reserved for testing and is not translated
parms[k] = v
else
kv_pairs[k] = v
end
end
local success, msg = translate_parms(parms, kv_pairs)
if not success then return false, msg end
local success, valinfo, i = get_values(parms)
if not success then return false, valinfo end
local in_unit = strip(parms[i])
i = i + 1
local success, in_unit_table = lookup(in_unit, parms.opt_sp_us, 'no_combination')
if not success then
if in_unit == nil then
in_unit = ''
end
in_unit_table = setmetatable({ symbol = in_unit, name2 = in_unit, utype = "length", scale = 1, bad_mcode = in_unit_table }, unit_mt)
end
if parms.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 = valinfo
in_unit_table.inout = 'in' -- this is an input unit
if not parms.range then
local success, inext, composite_unit = get_composite(parms, i, valinfo[1].value, in_unit_table)
if not success then return false, inext end
if composite_unit then
in_unit_table = composite_unit
i = inext
end
end
if in_unit_table.builtin == 'mach' then
-- As with old template, a number following Mach as the input unit is the altitude,
-- and there is no way to specify an altitude for the output unit.
-- Could put more code in this function to get any output unit and check for
-- an altitude following that unit.
local success, info = extract_number(parms, parms[i], false, true)
if success then
i = i + 1
in_unit_table.altitude = info.value
end
end
local next = strip(parms[i])
i = i + 1
local precision, is_bad_precision
local function set_precision(text)
local number, is_integer = get_number(text)
if number then
if is_integer then
precision = number
else
precision = text
is_bad_precision = true
end
return true -- text was used for precision, good or bad
end
end
if not set_precision(next) then
parms.out_unit = next
if set_precision(strip(parms[i])) then
i = i + 1
end
end
if parms.opt_adj_mid then
parms.opt_adjectival = true
next = parms[i]
i = i + 1
if next then -- mid-text words
if next:sub(1, 1) == '-' then
parms.mid = next
else
parms.mid = ' ' .. next
end
end
end
if parms.opt_one_preunit then
parms[parms.opt_flip and 'preunit2' or 'preunit1'] = preunits(1, parms[i])
i = i + 1
end
if parms.disp == 'x' then
-- Following is reasonably compatible with the old template.
local first = parms[i] or ''
local second = parms[i+1] or ''
i = i + 2
if strip(first) == '' then -- user can enter ' ' rather than ' ' to avoid the default
first = ' [ ' .. first
second = ' ]' .. second
end
parms.joins = { first, second }
elseif parms.opt_two_preunits then
local p1, p2 = preunits(2, parms[i], parms[i+1])
i = i + 2
if parms.preunit1 then
-- To simplify documentation, allow unlikely use of adj=pre with disp=preunit
-- (however, an output unit must be specified with adj=pre and with disp=preunit).
parms.preunit1 = parms.preunit1 .. p1
parms.preunit2 = p2
else
parms.preunit1, parms.preunit2 = p1, p2
end
end
if precision == nil then
if set_precision(strip(parms[i])) then
i = i + 1
end
end
if is_bad_precision then
add_warning(parms, 'cvt_bad_prec', precision)
else
parms.precision = precision
end
return true, parms, in_unit_table
end
local function default_precision(invalue, inclean, denominator, outvalue, in_current, out_current, extra)
-- Return a default value for precision (an integer like 2, 0, -2).
-- If denominator is not nil, it is the value of the denominator in inclean.
-- Code follows procedures used in old template.
local fudge = 1e-14 -- {{Order of magnitude}} adds this, so we do too
local prec, minprec, adjust
local utype = out_current.utype
local subunit_ignore_trailing_zero
local subunit_more_precision -- kludge for "in" used in input like "|2|ft|6|in"
local composite = in_current.composite
if composite then
subunit_ignore_trailing_zero = true -- input "|2|st|10|lb" has precision 0, not -1
if composite[#composite].exception == 'subunit_more_precision' then
subunit_more_precision = true -- do not use standard precision with input like "|2|ft|6|in"
end
end
if denominator and denominator > 0 then
prec = math.max(log10(denominator), 1)
else
-- Count digits after decimal mark, handling cases like '12.345e6'.
local exponent
local integer, dot, fraction, expstr = inclean:match('^(%d*)(%.?)(%d*)(.*)')
local e = expstr:sub(1, 1)
if e == 'e' or e == 'E' then
exponent = tonumber(expstr:sub(2))
end
if dot == '' then
prec = subunit_ignore_trailing_zero and 0 or -integer:match('0*$'):len()
else
prec = #fraction
end
if exponent then
-- So '1230' and '1.23e3' both give prec = -1, and '0.00123' and '1.23e-3' give 5.
prec = prec - exponent
end
end
if in_current.istemperature and out_current.istemperature then
-- Converting between common temperatures (°C, °F, °R, K); not keVT, MK.
-- Kelvin value can be almost zero, or small but negative due to precision problems.
-- Also, an input value like -300 C (below absolute zero) gives negative kelvins.
-- Calculate minimum precision from absolute value.
adjust = 0
local kelvin = abs((invalue - in_current.offset) * in_current.scale)
if kelvin < 1e-8 then -- assume nonzero due to input or calculation precision problem
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.exception == 'integer_more_precision' and floor(invalue) == invalue then
-- With certain output units that sometimes give poor results
-- with default rounding, use more precision when the input
-- value is equal to an integer. An example of a poor result
-- is when input 50 gives a smaller output than input 49.5.
-- Experiment shows this helps, but it does not eliminate all
-- surprises because it is not clear whether "50" should be
-- interpreted as "from 45 to 55" or "from 49.5 to 50.5".
adjust = -log10(in_current.scale)
elseif subunit_more_precision then
-- Conversion like "{{convert|6|ft|1|in|cm}}" (where subunit is "in")
-- has a non-standard adjust value, to give more output precision.
adjust = log10(out_current.scale) + 2
else
adjust = log10(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 extra then
adjust = extra.adjust or adjust
minprec = extra.minprec or minprec
end
return math.max(floor(prec + adjust), minprec)
end
local function convert(invalue, inclean, in_current, out_current)
-- Convert given input value from one unit to another.
-- Return output_value (a number) if a simple convert, or
-- return f, t where
-- f = true, t = table of information with results, or
-- f = false, t = error message table.
local inscale = in_current.scale
local outscale = out_current.scale
if not in_current.iscomplex and not out_current.iscomplex then
return invalue * (inscale / outscale) -- minimize overhead for most common case
end
if in_current.invert then
-- Fuel efficiency (there are no built-ins for this type of unit).
if in_current.invert * out_current.invert < 0 then
return 1 / (invalue * inscale * outscale)
end
return invalue * (inscale / outscale)
elseif in_current.offset then
-- Temperature (there are no built-ins for this type of unit).
return (invalue - in_current.offset) * (inscale / outscale) + out_current.offset
else
-- Built-in unit.
local in_builtin = in_current.builtin
local out_builtin = out_current.builtin
if in_builtin and out_builtin then
if in_builtin == out_builtin then
return invalue
end
-- There are no cases (yet) where need to convert from one
-- built-in unit to another, so this should never occur.
return false, { 'cvt_bug_convert' }
end
if in_builtin == 'mach' or out_builtin == 'mach' then
local adjust
if in_builtin == 'mach' then
inscale = speed_of_sound(in_current.altitude)
adjust = outscale / 0.1
else
outscale = speed_of_sound(out_current.altitude)
adjust = 0.1 / inscale
end
return true, {
outvalue = invalue * (inscale / outscale),
adjust = log10(adjust) + log10(2),
}
elseif in_builtin == 'hand' then
-- 1 hand = 4 inches; 1.2 hands = 6 inches.
-- Fractions of a hand are only defined for the first digit, and
-- the first fractional digit should be a number of inches (1, 2 or 3).
-- However, this code interprets the entire fraction as the number
-- of inches / 10 (so 1.75 inches would be 0.175 hands).
-- A value like 12.3 hands is exactly 12*4 + 3 inches; base default precision on that.
local integer, fraction = math.modf(invalue)
local outvalue = (integer + 2.5 * fraction) * (inscale / outscale)
local inch_value = 4 * integer + 10 * fraction -- equivalent number of inches
local fracstr = inclean:match('%.(.*)') or ''
local fmt
if fracstr == '' then
fmt = '%.0f'
else
fmt = '%.' .. format('%d', #fracstr - 1) .. 'f'
end
return true, {
invalue = inch_value,
inclean = format(fmt, inch_value),
outvalue = outvalue,
minprec = 0,
}
end
end
return false, { 'cvt_bug_convert' } -- should never occur
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 with the result of 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.
-- Input info.clean uses en digits (it has been translated, if necessary).
-- Output show uses en or non-en digits as appropriate, or can be spelled.
local invalue, inclean
if info then
invalue, inclean = info.value, info.clean
if in_current.builtin == 'hand' then
invalue = info.altvalue
end
end
if invalue == nil or invalue == '' then
return true, nil
end
if out_current.builtin == 'hand' then
-- Convert to hands, then convert the fractional part to inches.
-- Code is not correct when output is spelled, and it ignores any requested
-- precision if the output uses scientific notation (very large, or very
-- small). Not worth more complexity as these cases should be very rare.
if parms.abbr_org == nil then
out_current.usename = true -- default is to show name not symbol
end
local dummy_unit_table = { scale = out_current.scale }
local success, outinfo = cvtround(parms, info, in_current, dummy_unit_table)
if not success then return false, outinfo end
local fmt
if outinfo.is_scientific then
fmt = '%.1f'
else
local fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or '' -- outinfo.show is in local language
if fraction == '' then
if not outinfo.use_default_precision then
return true, outinfo
end
fmt = '%.0f'
else
fmt = '%.' .. format('%d', ulen(fraction) - 1) .. 'f'
end
end
local hands, inches = math.modf(outinfo.raw_absvalue)
inches = format(fmt, inches * 4)
if inches:sub(1, 1) == '4' then
hands = hands + 1
inches = '0' .. inches:sub(2)
if tonumber(inches) == 0 then
inches = '0'
end
end
if inches:sub(2, 2) == '.' then
inches = inches:sub(1, 1) .. inches:sub(3)
end
outinfo.show = outinfo.sign .. with_separator(parms, format('%d', hands)) .. numdot .. from_en(inches)
return true, outinfo
end
local outvalue, extra = convert(invalue, inclean, in_current, out_current)
if extra then
if not outvalue then return false, extra end
invalue = extra.invalue or invalue
inclean = extra.inclean or inclean
outvalue = extra.outvalue
end
if not valid_number(outvalue) then
return false, { 'cvt_invalid_num' }
end
local isnegative
if outvalue < 0 then
isnegative = true
outvalue = -outvalue
end
local success, use_default_precision, show, exponent
local precision = parms.precision
if not precision then
local sigfig = parms.sigfig
if sigfig then
show, exponent = make_sigfig(outvalue, sigfig)
elseif parms.opt_round5 then
show = format('%.0f', floor((outvalue / 5) + 0.5) * 5)
else
use_default_precision = true
precision = default_precision(invalue, inclean, info.denominator, outvalue, in_current, out_current, extra)
end
end
if precision then
if precision >= 0 then
if 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'
local success
success, show = pcall(format, fmt, outvalue)
if not success then
return false, { 'cvt_big_prec', tostring(precision) }
end
else
precision = -precision -- #digits to zero (in addition to any digits after dot)
local shift = 10 ^ precision
show = format('%.0f', outvalue/shift)
if show ~= '0' then
exponent = #show + precision
end
end
end
local t = format_number(parms, show, exponent, isnegative)
-- Set singular using match because on some systems 0.99999999999999999 is 1.0.
t.singular = ((show == '1' or show:match('^1%.0*$') ~= nil) and not isnegative)
t.raw_absvalue = outvalue -- absolute value before rounding
t.use_default_precision = use_default_precision
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
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)
-- only en digits are accepted
-- '<' 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 bang-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.
-- Input must use en digits and '.' decimal mark.
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 = split(default, '!')
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 linked_pages -- to record linked pages so will not link to the same page more than once
local function make_link(link, id, link_key)
-- Return wikilink "[[link|id]]", possibly abbreviated as in examples:
-- [[Mile|mile]] --> [[mile]]
-- [[Mile|miles]] --> [[mile]]s
-- However, just id is returned if:
-- * no link given (so caller does not need to check if a link was defined); or
-- * link has previously been used during the current convert (to avoid overlinking).
-- Linking with a unit uses the unit table as the link key, which fails to detect
-- overlinking for conversions like (each links "mile" twice):
-- {{convert|1|impgal/mi|USgal/mi|lk=on}}
-- {{convert|1|l/km|impgal/mi USgal/mi|lk=on}}
link_key = link_key or link -- use key if given (the key, but not the link, may be known when need to cancel a link record)
if link == nil or link == '' or linked_pages[link_key] then
return id
end
linked_pages[link_key] = true
-- Following only works for language en, but it should be safe on other wikis,
-- and overhead of doing it generally does not seem worthwhile.
local l = link:sub(1, 1):lower() .. link:sub(2)
if link == id or l == id then
return '[[' .. id .. ']]'
elseif link .. 's' == id or l .. 's' == id then
return '[[' .. id:sub(1, -2) .. ']]s'
else
return '[[' .. link .. '|' .. id .. ']]'
end
end
local function linked_id(unit_table, key_id, want_link)
-- Return final unit id (symbol or name), optionally with a wikilink,
-- and update unit_table.sep if required.
-- key_id is one of: 'symbol', 'sym_us', 'name1', 'name1_us', 'name2', 'name2_us'.
local abbr_on = (key_id == 'symbol' or key_id == 'sym_us')
if abbr_on and want_link then
local symlink = rawget(unit_table, 'symlink')
if symlink then
return symlink -- for exceptions that have the linked symbol built-in
end
end
local multiplier = rawget(unit_table, 'multiplier')
local per = unit_table.per
if per then
local unit1 = per[1] -- top unit_table, or nil
local unit2 = per[2] -- bottom unit_table
if abbr_on then
if not unit1 then
unit_table.sep = '' -- no separator in "$2/acre"
end
if not want_link then
local symbol = unit_table.symbol_raw
if symbol then
return symbol -- for exceptions that have the symbol built-in
end
end
end
local key_id2 -- unit2 is always singular
if key_id == 'name2' then
key_id2 = 'name1'
elseif key_id == 'name2_us' then
key_id2 = 'name1_us'
else
key_id2 = key_id
end
local result
if abbr_on then
result = '/'
elseif unit1 then
result = ' ' .. per_word .. ' '
else
result = per_word .. ' '
end
if want_link and unit_table.link then
result = (unit1 and unit1[key_id] or '') .. result .. unit2[key_id2]
return make_link(unit_table.link, result, unit_table)
end
if unit1 then
result = linked_id(unit1, key_id, want_link) .. result
end
return result .. linked_id(unit2, key_id2, want_link)
end
if multiplier then
-- A multiplier (like "100" in "100km") forces the unit to be plural.
multiplier = from_en(multiplier)
if abbr_on then
multiplier = multiplier .. ' '
else
multiplier = multiplier .. ' '
if key_id == 'name1' then
key_id = 'name2'
elseif key_id == 'name1_us' then
key_id = 'name2_us'
end
end
else
multiplier = ''
end
local id = unit_table.fixed_name or unit_table[key_id]
if want_link then
local link = link_exceptions[unit_table.symbol] or unit_table.link
if link then
local before = ''
local i = unit_table.customary
if i == 1 and unit_table.sp_us then
i = 2 -- show "U.S." not "US"
end
if i == 3 and abbr_on then
i = 4 -- abbreviate "imperial" to "imp"
end
local customary = customary_units[i]
if customary then
-- LATER: This works for language en only, but it's esoteric so ignore for now.
local pertext
if id:sub(1, 1) == '/' then
-- Want unit "/USgal" to display as "/U.S. gal", not "U.S. /gal".
pertext = '/'
id = id:sub(2)
elseif id:sub(1, 4) == 'per ' then
-- Similarly want "per U.S. gallon", not "U.S. per gallon" (but in practice this is unlikely to be used).
pertext = 'per '
id = id:sub(5)
else
pertext = ''
end
-- Omit any "US"/"U.S."/"imp"/"imperial" from start of id since that will be inserted.
local removes = (i < 3) and { 'US ', 'US ', 'U.S. ', 'U.S. ' } or { 'imp ', 'imp ', 'imperial ' }
for _, prefix in ipairs(removes) do
local plen = #prefix
if id:sub(1, plen) == prefix then
id = id:sub(plen + 1)
break
end
end
before = pertext .. make_link(customary.link, customary[1]) .. ' '
end
id = before .. make_link(link, id, unit_table)
end
end
return multiplier .. id
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.
-- In addition, set unit_table.sep = ' ' or ' ' or ''
-- (the separator that caller will normally insert before the id).
if parms.opt_values then
unit_table.sep = ''
return ''
end
local inout = unit_table.inout
local valinfo = unit_table.valinfo
local abbr_org = parms.abbr_org
local adjectival = parms.opt_adjectival
local disp = parms.disp
local lk = parms.lk
local usename = unit_table.usename
local singular = valinfo[which].singular
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.
local flipped = parms.opt_flip
if inout == 'in' then
if not adjectival and (abbr_org == 'out' or flipped) then
local value = valinfo[which].value
singular = (0 < value and value < 1.0001)
end
else
if (abbr_org == 'on') or
(not flipped and (abbr_org == nil or abbr_org == 'out')) or
(flipped and abbr_org == 'in') then
singular = (valinfo[which].absvalue < 1.0001 and
not valinfo[which].is_scientific)
end
end
end
end
local want_name
if usename then
want_name = true
else
if abbr_org == nil then
if disp == 'br' or disp == 'or' or disp == 'slash' then
want_name = true
end
if unit_table.usesymbol then
want_name = false
end
end
if want_name == nil then
local abbr = parms.abbr
if abbr == 'on' or abbr == inout or (abbr == 'mos' and inout == 'out') then
want_name = false
else
want_name = true
end
end
end
local key
if want_name then
if parms.opt_use_nbsp then
unit_table.sep = ' '
else
unit_table.sep = ' '
end
if parms.opt_singular then
local value
if inout == 'in' then
value = valinfo[which].value
else
value = valinfo[which].absvalue
end
if value then -- some unusual units do not always set value field
value = abs(value)
singular = (0 < value and value < 1.0001)
end
end
if unit_table.engscale or parms.is_range_x then
-- engscale: so "|1|e3kg" gives "1 thousand kilograms" (plural)
-- is_range_x: so "|0.5|x|0.9|mi" gives "0.5 by 0.9 miles" (plural)
singular = false
end
key = (adjectival or singular) and 'name1' or 'name2'
if unit_table.sp_us then
key = key .. '_us'
end
else
unit_table.sep = ' '
key = unit_table.sp_us and 'sym_us' or 'symbol'
end
return linked_id(unit_table, key, lk == 'on' or lk == inout), want_name
end
local function decorate_value(parms, unit_table, which)
-- If needed, update unit_table so values will be shown with extra information.
-- 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 inout = unit_table.inout
local info = unit_table.valinfo[which]
local abbr = parms.abbr
if abbr == 'on' or abbr == inout then
info.show = info.show ..
'<span style="margin-left:0.2em">×<span style="margin-left:0.1em">' ..
from_en('10') ..
'</span></span><s style="display:none">^</s><sup>' ..
from_en(tostring(engscale.exponent)) .. '</sup>'
else
local number_id
local lk = parms.lk
if lk == 'on' or lk == inout then
number_id = make_link(engscale.link, engscale[1])
else
number_id = engscale[1]
end
-- WP:NUMERAL recommends " " in values like "12 million".
info.show = info.show .. (parms.opt_adjectival and '-' or ' ') .. number_id
end
end
local prefix = unit_table.vprefix
if prefix then
local info = unit_table.valinfo[which]
info.show = prefix .. info.show
end
end
local function process_input(parms, in_current)
-- Processing required once per conversion.
-- Return block of text to represent input (value/unit).
if parms.opt_output_only or parms.opt_output_number_only or parms.opt_output_unit_only then
parms.joins = { '', '' }
return ''
end
local first_unit
local composite = in_current.composite -- nil or table of units
if composite then
first_unit = composite[1]
else
first_unit = in_current
end
local id1, want_name = make_id(parms, 1, first_unit)
local sep = first_unit.sep -- separator between value and unit, set by make_id
local preunit = parms.preunit1
if preunit then
sep = '' -- any separator is included in preunit
else
preunit = ''
end
if parms.opt_input_unit_only then
parms.joins = { '', '' }
if composite then
local parts = { id1 }
for i, unit in ipairs(composite) do
if i > 1 then
table.insert(parts, (make_id(parms, 1, unit)))
end
end
id1 = table.concat(parts, ' ')
end
if want_name and parms.opt_adjectival then
return preunit .. hyphenated(id1)
end
return preunit .. id1
end
local abbr = parms.abbr
local disp = parms.disp
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
if parms.opt_also_symbol and not composite then
local join1 = parms.joins[1]
if join1 == ' (' or join1 == ' [' then
parms.joins = { join1 .. first_unit[first_unit.sp_us and 'sym_us' or 'symbol'] .. ', ', parms.joins[2] }
end
end
if in_current.builtin == 'mach' then
local prefix = id1 .. ' '
local range = parms.range
local valinfo = first_unit.valinfo
local result = prefix .. valinfo[1].show
if range then
-- For simplicity and because more not needed, handle one range item only.
local prefix2 = make_id(parms, 2, first_unit) .. ' '
result = range_text(range[1], want_name, parms, result, prefix2 .. valinfo[2].show)
end
return preunit .. result
end
if composite then
-- Simplify: assume there is no range, and no decoration.
local mid = ''
local sep1 = ' '
local sep2 = ' '
if parms.opt_adjectival then
if not parms.opt_flip then
mid = parms.mid or ''
end
if want_name then
sep1 = '-'
sep2 = '-'
end
end
local parts = { first_unit.valinfo[1].show .. sep1 .. id1 }
for i, unit in ipairs(composite) do
if i > 1 then
table.insert(parts, unit.valinfo[1].show .. sep1 .. (make_id(parms, 1, unit)))
end
end
return table.concat(parts, sep2) .. mid
end
local result, mos
local range = parms.range
if range then
mos = (abbr == 'mos')
if not (mos or (parms.is_range_x and not want_name)) then
linked_pages[first_unit] = nil -- so the second and only id will be linked, if wanted
end
end
local id = (range == nil) and id1 or make_id(parms, 2, first_unit)
local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, 'in')
if mos and was_hyphenated then
mos = false -- suppress repeat of unit in a range
if linked_pages[first_unit] then
linked_pages[first_unit] = nil
id = make_id(parms, 2, first_unit)
extra = hyphenated_maybe(parms, want_name, sep, id, 'in')
end
end
local valinfo = first_unit.valinfo
if range then
if range.n == 1 then
-- Like {{convert|1|x|2|ft}} (one range item; two values).
-- Do what old template did.
local sep1 = first_unit.sep
if mos then
decorate_value(parms, in_current, 1)
decorate_value(parms, in_current, 2)
result = valinfo[1].show .. sep1 .. id1
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 .. sep1 .. id1
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
end
result = range_text(range[1], want_name, parms, result, valinfo[2].show)
else
-- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify.
decorate_value(parms, in_current, 1)
result = valinfo[1].show
for i = 1, range.n do
decorate_value(parms, in_current, i+1)
result = range_text(range[i], want_name, parms, result, valinfo[i+1].show)
end
end
else
decorate_value(parms, first_unit, 1)
result = valinfo[1].show
end
return result .. preunit .. 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 sep = out_current.sep -- set by make_id
local preunit = parms.preunit2
if preunit then
sep = '' -- any separator is included in preunit
else
preunit = ''
end
if parms.opt_output_unit_only then
if want_name and parms.opt_adjectival then
return preunit .. hyphenated(id1)
end
return preunit .. id1
end
if out_current.builtin == 'mach' then
local prefix = id1 .. ' '
local range = parms.range
local valinfo = out_current.valinfo
local result = prefix .. valinfo[1].show
if range then
-- For simplicity and because more not needed, handle one range item only.
result = range_text(range[1], want_name, parms, result, prefix .. valinfo[2].show)
end
return preunit .. result
end
local result
local range = parms.range
if range then
if not (parms.is_range_x and not want_name) then
linked_pages[out_current] = nil -- so the second and only id will be linked, if wanted
end
end
local id = (range == nil) and id1 or make_id(parms, 2, out_current)
local extra = hyphenated_maybe(parms, want_name, sep, id, 'out')
local valinfo = out_current.valinfo
if range then
if range.n == 1 then
local sep1 = out_current.sep
local abbr = parms.abbr
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 .. sep1 .. id1
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
end
result = range_text(range[1], want_name, parms, result, valinfo[2].show)
else
-- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify.
decorate_value(parms, out_current, 1)
result = valinfo[1].show
for i = 1, range.n do
decorate_value(parms, out_current, i+1)
result = range_text(range[i], want_name, parms, result, valinfo[i+1].show)
end
end
else
decorate_value(parms, out_current, 1)
result = valinfo[1].show
end
if parms.opt_output_number_only then
return result
end
return result .. preunit .. extra
end
local function make_output_single(parms, 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.
out_unit_table.valinfo = collection()
local range = parms.range
for i = 1, (range and (range.n + 1) or 1) do
local success, info = cvtround(parms, in_unit_table.valinfo[i], in_unit_table, out_unit_table)
if not success then return false, info end
out_unit_table.valinfo:add(info)
end
return true, process_one_output(parms, out_unit_table)
end
local function make_output_multiple(parms, 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 mid = ''
local sep1 = ' '
local sep2 = ' '
if parms.opt_adjectival then
if parms.opt_flip then
mid = parms.mid or ''
end
if want_name then
sep1 = '-'
sep2 = '-'
end
end
local function make_result(info)
local fmt, outvalue, sign
local results = {}
for i = 1, #combos do
local thisvalue, strforce
local out_current = combos[i]
out_current.inout = 'out'
local scale = multiple[i]
if i == 1 then -- least significant unit ('in' from 'ftin')
local fraction
local success, outinfo = cvtround(parms, info, in_unit_table, out_current)
if not success then return false, outinfo end
sign = outinfo.sign
if outinfo.is_scientific then
strforce = outinfo.show
fraction = ''
else
fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or '' -- outinfo.show is in local language
end
fmt = '%.' .. ulen(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 = make_link(link, id, out_current)
end
end
local strval
if strforce and outvalue == 0 then
sign = '' -- any sign is in strforce
strval = strforce -- show small values in scientific notation; will only use least significant unit
else
strval = (thisvalue == 0) and from_en('0') or with_separator(parms, format(fmt, thisvalue))
end
table.insert(results, strval .. sep1 .. 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 true, sign .. table.concat(reversed, sep2)
end
local valinfo = in_unit_table.valinfo
local success, result = make_result(valinfo[1])
if not success then return false, result end
local range = parms.range
if range then
for i = 1, range.n do
local success, result2 = make_result(valinfo[i+1])
if not success then return false, result2 end
result = range_text(range[i], want_name, parms, result, result2)
end
end
return true, result .. mid
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.
linked_pages = {}
local success, bad_output, out_unit_table
local bad_input_mcode = in_unit_table.bad_mcode -- nil if input unit is valid
local invalue1 = in_unit_table.valinfo[1].value
local out_unit = parms.out_unit
if out_unit == nil or out_unit == '' then
if bad_input_mcode then
bad_output = ''
else
success, out_unit = get_default(invalue1, in_unit_table)
if not success then
bad_output = out_unit
end
end
end
if not bad_output then
success, out_unit_table = lookup(out_unit, parms.opt_sp_us, 'any_combination')
if success then
local mismatch = check_mismatch(in_unit_table, out_unit_table)
if mismatch then
bad_output = mismatch
end
else
bad_output = out_unit_table
end
end
local flipped = parms.opt_flip and not bad_input_mcode
local parts = {}
for part = 1, 2 do
-- The LHS (parts[1]) is normally the input, but is the output if flipped.
-- Process LHS first so it will be linked, if wanted.
-- Linking to the same item is suppressed in the RHS to avoid overlinking.
if (part == 1 and not flipped) or (part == 2 and flipped) then
parts[part] = process_input(parms, in_unit_table)
elseif bad_output then
if bad_output ~= '' then
parts[part] = message(bad_output)
end
else
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, in_unit_table, out_current)
else
success, item = make_output_multiple(parms, in_unit_table, out_current)
end
if not success then return false, item end
table.insert(outputs, item)
end
parts[part] = parms.opt_input_unit_only and '' or table.concat(outputs, '; ')
end
end
if parms.opt_sortable then
parts[1] = ntsh(invalue1, parms.opt_sortable_debug) .. parts[1]
end
local wikitext
if bad_input_mcode then
wikitext = parts[1] .. message(bad_input_mcode)
elseif parms.table_joins then
wikitext = parms.table_joins[1] .. parts[1] .. parms.table_joins[2] .. parts[2]
else
wikitext = parts[1] .. parms.joins[1] .. parts[2] .. parms.joins[2]
end
if parms.warnings and not bad_input_mcode then
wikitext = wikitext .. parms.warnings
end
return true, wikitext
end
local function main_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 message(result)
end
return { convert = main_convert }