Module:Convert: Difference between revisions

From Zoophilia Wiki
Jump to navigationJump to search
meta>Johnuniq
"per" units can have a scale multiplier (for the units that used to be "mass per unit area" but are now "pressure")
meta>Johnuniq
new style: 4-column tabs
Line 32: Line 32:


local function boolean(text)
local function boolean(text)
    -- Return true if text represents a "true" option value.
-- Return true if text represents a "true" option value.
    if text then
if text then
        text = text:lower()
text = text:lower()
        if text == 'on' or text == 'yes' then
if text == 'on' or text == 'yes' then
            return true
return true
        end
end
    end
end
    return false
return false
end
end


local function from_en(text)
local function from_en(text)
    -- Input is a string representing a number in en digits with '.' decimal mark,
-- Input is a string representing a number in en digits with '.' decimal mark,
    -- without digit grouping (which is done just after calling this).
-- without digit grouping (which is done just after calling this).
    -- Return the translation of the string with numdot and digits in local language.
-- Return the translation of the string with numdot and digits in local language.
    if numdot ~= '.' then
if numdot ~= '.' then
        text = text:gsub('%.', numdot)
text = text:gsub('%.', numdot)
    end
end
    if from_en_table then
if from_en_table then
        text = text:gsub('%d', from_en_table)
text = text:gsub('%d', from_en_table)
    end
end
    return text
return text
end
end


local function to_en(text)
local function to_en(text)
    -- Input is a string representing a number in the local language with
-- Input is a string representing a number in the local language with
    -- an optional numdot decimal mark and numsep digit grouping.
-- an optional numdot decimal mark and numsep digit grouping.
    -- Return the translation of the string with '.' mark and en digits,
-- Return the translation of the string with '.' mark and en digits,
    -- and no separators (they have to be removed here to handle cases like
-- and no separators (they have to be removed here to handle cases like
    -- numsep = '.' and numdot = ',' with input "1.234.567,8").
-- numsep = '.' and numdot = ',' with input "1.234.567,8").
    if numsep ~= '' then
if numsep ~= '' then
        text = text:gsub('[' .. numsep .. ']', '')  -- use '[x]' in case x is '.'
text = text:gsub('[' .. numsep .. ']', '')  -- use '[x]' in case x is '.'
    end
end
    if numdot ~= '.' then
if numdot ~= '.' then
        text = text:gsub('[' .. numdot .. ']', '.')
text = text:gsub('[' .. numdot .. ']', '.')
    end
end
    if to_en_table then
if to_en_table then
        text = ustring.gsub(text, '%d', to_en_table)
text = ustring.gsub(text, '%d', to_en_table)
    end
end
    return text
return text
end
end


Line 77: Line 77:


local function set_config(frame)
local function set_config(frame)
    -- Set configuration options from template #invoke or defaults.
-- Set configuration options from template #invoke or defaults.
    config = frame.args
config = frame.args
    numdot = config.numdot or '.'      -- decimal mark before fractional digits
numdot = config.numdot or '.'      -- decimal mark before fractional digits
    numsep = config.numsep or ','      -- group separator for numbers (',', '.', '')
numsep = config.numsep or ','      -- group separator for numbers (',', '.', '')
    maxsigfig = config.maxsigfig or 14  -- maximum number of significant figures
maxsigfig = config.maxsigfig or 14  -- maximum number of significant figures
    -- Scribunto sets the global variable 'mw'.
-- Scribunto sets the global variable 'mw'.
    -- A testing program can set the global variable 'is_test_run'.
-- A testing program can set the global variable 'is_test_run'.
    local data_module, text_module, data_code, text_code
local data_module, text_module, data_code, text_code
    if is_test_run then
if is_test_run then
        local langcode = mw.language.getContentLanguage().code
local langcode = mw.language.getContentLanguage().code
        data_module = "convertdata-" .. langcode
data_module = "convertdata-" .. langcode
        text_module = "converttext-" .. langcode
text_module = "converttext-" .. langcode
        extra_module = "convertextra-" .. langcode
extra_module = "convertextra-" .. langcode
        spell_module = "ConvertNumeric"
spell_module = "ConvertNumeric"
    else
else
        local sandbox = boolean(config.sandbox) and '/sandbox' or ''
local sandbox = boolean(config.sandbox) and '/sandbox' or ''
        data_module = "Module:Convert/data" .. sandbox
data_module = "Module:Convert/data" .. sandbox
        text_module = "Module:Convert/text" .. sandbox
text_module = "Module:Convert/text" .. sandbox
        extra_module = "Module:Convert/extra" .. sandbox
extra_module = "Module:Convert/extra" .. sandbox
        spell_module = "Module:ConvertNumeric"
spell_module = "Module:ConvertNumeric"
    end
end
    data_code = mw.loadData(data_module)
data_code = mw.loadData(data_module)
    text_code = mw.loadData(text_module)
text_code = mw.loadData(text_module)
    default_exceptions = data_code.default_exceptions
default_exceptions = data_code.default_exceptions
    link_exceptions = data_code.link_exceptions
link_exceptions = data_code.link_exceptions
    all_units = data_code.all_units
all_units = data_code.all_units
    SIprefixes = text_code.SIprefixes
SIprefixes = text_code.SIprefixes
    all_categories = text_code.all_categories
all_categories = text_code.all_categories
    all_messages = text_code.all_messages
all_messages = text_code.all_messages
    customary_units = text_code.customary_units
customary_units = text_code.customary_units
    disp_joins = text_code.disp_joins
disp_joins = text_code.disp_joins
    en_option_name = text_code.en_option_name
en_option_name = text_code.en_option_name
    en_option_value = text_code.en_option_value
en_option_value = text_code.en_option_value
    eng_scales = text_code.eng_scales
eng_scales = text_code.eng_scales
    range_aliases = text_code.range_aliases
range_aliases = text_code.range_aliases
    range_types = text_code.range_types
range_types = text_code.range_types
    local translation = text_code.translation_table
local translation = text_code.translation_table
    if translation then
if translation then
        if translation.group then
if translation.group then
            group_method = translation.group
group_method = translation.group
        end
end
        if translation.per_word then
if translation.per_word then
            per_word = translation.per_word
per_word = translation.per_word
        end
end
        if translation.plural_suffix then
if translation.plural_suffix then
            plural_suffix = translation.plural_suffix
plural_suffix = translation.plural_suffix
        end
end
        from_en_table = translation.from_en
from_en_table = translation.from_en
        local use_workaround = true
local use_workaround = true
        if use_workaround then
if use_workaround then
            -- 2013-07-05 workaround bug by making a copy of the required table.
-- 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,
-- mw.ustring.gsub fails with a table (to_en_table) as the replacement,
            -- if the table is accessed via mw.loadData.
-- if the table is accessed via mw.loadData.
            local source = translation.to_en
local source = translation.to_en
            if source then
if source then
                to_en_table = {}
to_en_table = {}
                for k, v in pairs(source) do
for k, v in pairs(source) do
                    to_en_table[k] = v
to_en_table[k] = v
                end
end
            end
end
        else
else
            to_en_table = translation.to_en
to_en_table = translation.to_en
        end
end
    end
end
end
end


local function collection()
local function collection()
    -- Return a table to hold items.
-- Return a table to hold items.
    return {
return {
        n = 0,
n = 0,
        add = function (self, item)
add = function (self, item)
            self.n = self.n + 1
self.n = self.n + 1
            self[self.n] = item
self[self.n] = item
        end,
end,
    }
}
end
end


local function split(text, delimiter)
local function split(text, delimiter)
    -- Return a numbered table with fields from splitting text.
-- Return a numbered table with fields from splitting text.
    -- The delimiter is used in a regex without escaping (for example, '.' would fail).
-- The delimiter is used in a regex without escaping (for example, '.' would fail).
    -- Each field has any leading/trailing whitespace removed.
-- Each field has any leading/trailing whitespace removed.
    local t = {}
local t = {}
    text = text .. delimiter  -- to get last item
text = text .. delimiter  -- to get last item
    for item in text:gmatch('%s*(.-)%s*' .. delimiter) do
for item in text:gmatch('%s*(.-)%s*' .. delimiter) do
        table.insert(t, item)
table.insert(t, item)
    end
end
    return t
return t
end
end


local function strip(text)
local function strip(text)
    -- If text is a string, return its content with no leading/trailing
-- If text is a string, return its content with no leading/trailing
    -- whitespace. Otherwise return nil (a nil argument gives a nil result).
-- whitespace. Otherwise return nil (a nil argument gives a nil result).
    if type(text) == 'string' then
if type(text) == 'string' then
        return text:match("^%s*(.-)%s*$")
return text:match("^%s*(.-)%s*$")
    end
end
end
end


local function wanted_category(cat)
local function wanted_category(cat)
    -- Return cat if it is wanted in current namespace, otherwise return nil.
-- Return cat if it is wanted in current namespace, otherwise return nil.
    -- This is so tracking categories only include pages that need correction.
-- This is so tracking categories only include pages that need correction.
    -- Default wanted namespaces are 0 (article) and 10 (template).
-- Default wanted namespaces are 0 (article) and 10 (template).
    local title = mw.title.getCurrentTitle()
local title = mw.title.getCurrentTitle()
    if title then
if title then
        local namespace = title.namespace
local namespace = title.namespace
        for _, v in ipairs(split(config.nscat or '0,10', ',')) do
for _, v in ipairs(split(config.nscat or '0,10', ',')) do
            if namespace == tonumber(v) then
if namespace == tonumber(v) then
                return cat
return cat
            end
end
        end
end
    end
end
end
end


local function message(mcode)
local function message(mcode)
    -- Return wikitext for an error message, including category if specified
-- Return wikitext for an error message, including category if specified
    -- for the message type.
-- for the message type.
    -- mcode = numbered table specifying the message:
-- mcode = numbered table specifying the message:
    --    mcode[1] = 'cvt_xxx' (string used as a key to get message info)
--    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[2] = 'parm1' (string to replace first %s if any in message)
    --    mcode[3] = 'parm2' (string to replace second %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)
--    mcode[4] = 'parm3' (string to replace third %s if any in message)
    local msg = all_messages[mcode[1]]
local msg = all_messages[mcode[1]]
    if msg then
if msg then
        local title = format(msg[1] or 'Missing message',
local title = format(msg[1] or 'Missing message',
            mcode[2] or '?',
mcode[2] or '?',
            mcode[3] or '?',
mcode[3] or '?',
            mcode[4] or '?')
mcode[4] or '?')
        local text = msg[2] or 'Missing message'
local text = msg[2] or 'Missing message'
        local cat = wanted_category(all_categories[msg[3]]) or ''
local cat = wanted_category(all_categories[msg[3]]) or ''
        local anchor = msg[4] or ''
local anchor = msg[4] or ''
        local fmt = all_messages['cvt_format'] or 'convert: bug'
local fmt = all_messages['cvt_format'] or 'convert: bug'
        local regex, replace = msg.regex, msg.replace
local regex, replace = msg.regex, msg.replace
        if regex and replace then
if regex and replace then
            title = title:gsub(regex, replace)
title = title:gsub(regex, replace)
        end
end
        title = title:gsub('"', '"')
title = title:gsub('"', '"')
        return format(fmt, anchor, title, text, cat)
return format(fmt, anchor, title, text, cat)
    end
end
    return 'Convert internal error: unknown message'
return 'Convert internal error: unknown message'
end
end


local function add_warning(parms, mcode, text)
local function add_warning(parms, mcode, text)
    -- If enabled, add a warning that will be displayed after the convert result.
-- If enabled, add a warning that will be displayed after the convert result.
    -- To reduce output noise, only the first warning is displayed.
-- To reduce output noise, only the first warning is displayed.
    if boolean(config.warnings) then
if boolean(config.warnings) then
        if parms.warnings == nil then
if parms.warnings == nil then
            parms.warnings = message({ mcode, text })
parms.warnings = message({ mcode, text })
        end
end
    end
end
end
end


local function spell_number(parms, number, numerator, denominator)
local function spell_number(parms, number, numerator, denominator)
    -- Return result of spelling (number, numerator, denominator), or
-- Return result of spelling (number, numerator, denominator), or
    -- return nil if spelling is not available or not supported for given text.
-- return nil if spelling is not available or not supported for given text.
    -- Examples (each value must be a string or nil):
-- Examples (each value must be a string or nil):
    --  number  numerator  denominator  output
--  number  numerator  denominator  output
    --  ------  ---------  -----------  -------------------
--  ------  ---------  -----------  -------------------
    --  "1.23"    nil        nil        one point two three
--  "1.23"    nil        nil        one point two three
    --    "1"      "2"        "3"        one and two thirds
--    "1"      "2"        "3"        one and two thirds
    --    nil      "2"        "3"        two thirds
--    nil      "2"        "3"        two thirds
    if not speller then
if not speller then
        local function get_speller(module)
local function get_speller(module)
            return require(module).spell_number
return require(module).spell_number
        end
end
        local success
local success
        success, speller = pcall(get_speller, spell_module)
success, speller = pcall(get_speller, spell_module)
        if not success or type(speller) ~= 'function' then
if not success or type(speller) ~= 'function' then
            add_warning(parms, 'cvt_no_spell')
add_warning(parms, 'cvt_no_spell')
            return nil
return nil
        end
end
    end
end
    local case = parms.opt_spell_upper
local case = parms.opt_spell_upper
    parms.opt_spell_upper = nil  -- only uppercase first number in a multiple unit
parms.opt_spell_upper = nil  -- only uppercase first number in a multiple unit
    local sp = not parms.opt_sp_us
local sp = not parms.opt_sp_us
    local adj = parms.opt_adjectival
local adj = parms.opt_adjectival
    return speller(number, numerator, denominator, case, sp, adj)
return speller(number, numerator, denominator, case, sp, adj)
end
end


Line 258: Line 258:
-- LATER: If need much more code, move to another module to simplify this module.
-- LATER: If need much more code, move to another module to simplify this module.
local function speed_of_sound(altitude)
local function speed_of_sound(altitude)
    -- This is for the Mach built-in unit of speed.
-- This is for the Mach built-in unit of speed.
    -- Return speed of sound in metres per second at given altitude in feet.
-- Return speed of sound in metres per second at given altitude in feet.
    -- If no altitude given, use default (zero altitude = sea level).
-- If no altitude given, use default (zero altitude = sea level).
    -- Table gives speed of sound in miles per hour at various altitudes:
-- Table gives speed of sound in miles per hour at various altitudes:
    --  altitude = -17,499 to 302,499 feet
--  altitude = -17,499 to 302,499 feet
    -- mach_table[a + 4] = s where
-- mach_table[a + 4] = s where
    --  a = (altitude / 5000) rounded to nearest integer (-3 to 60)
--  a = (altitude / 5000) rounded to nearest integer (-3 to 60)
    --  s = speed of sound (mph) at that altitude
--  s = speed of sound (mph) at that altitude
    -- LATER: Should calculate result from an interpolation between the next
-- LATER: Should calculate result from an interpolation between the next
    -- lower and higher altitudes in table, rather than rounding to nearest.
-- lower and higher altitudes in table, rather than rounding to nearest.
    -- From: http://www.aerospaceweb.org/question/atmosphere/q0112.shtml
-- From: http://www.aerospaceweb.org/question/atmosphere/q0112.shtml
    local mach_table = {                                                      -- a =
local mach_table = {                                                      -- a =
        799.5, 787.0, 774.2, 761.207051,                                      -- -3 to  0
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
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
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
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
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
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
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
altitude = altitude or 0
    local a = (altitude < 0) and -altitude or altitude
local a = (altitude < 0) and -altitude or altitude
    a = floor(a / 5000 + 0.5)
a = floor(a / 5000 + 0.5)
    if altitude < 0 then
if altitude < 0 then
        a = -a
a = -a
    end
end
    if a < -3 then
if a < -3 then
        a = -3
a = -3
    elseif a > 60 then
elseif a > 60 then
        a = 60
a = 60
    end
end
    return mach_table[a + 4] * 0.44704  -- mph converted to m/s
return mach_table[a + 4] * 0.44704  -- mph converted to m/s
end
end
-- END: Code required only for built-in units.
-- END: Code required only for built-in units.
Line 295: Line 295:


local function check_mismatch(unit1, unit2)
local function check_mismatch(unit1, unit2)
    -- If unit1 cannot be converted to unit2, return an error message table.
-- If unit1 cannot be converted to unit2, return an error message table.
    -- This allows conversion between units of the same type, and between
-- This allows conversion between units of the same type, and between
    -- Nm (normally torque) and ftlb (energy), as in gun-related articles.
-- Nm (normally torque) and ftlb (energy), as in gun-related articles.
    -- This works because Nm is the base unit (scale = 1) for both the
-- This works because Nm is the base unit (scale = 1) for both the
    -- primary type (torque), and the alternate type (energy, where Nm = J).
-- 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
-- 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
-- the alternate type of unit2, and vice versa. That provides a whitelist
    -- of which conversions are permitted between normally incompatible types.
-- of which conversions are permitted between normally incompatible types.
    if unit1.utype == unit2.utype or
if unit1.utype == unit2.utype or
      (unit1.utype == unit2.alttype and unit1.alttype == unit2.utype) then
(unit1.utype == unit2.alttype and unit1.alttype == unit2.utype) then
        return nil
return nil
    end
end
    return { 'cvt_mismatch', unit1.utype, unit2.utype }
return { 'cvt_mismatch', unit1.utype, unit2.utype }
end
end


local function override_from(out_table, in_table, fields)
local function override_from(out_table, in_table, fields)
    -- Copy the specified fields from in_table to out_table, but do not
-- Copy the specified fields from in_table to out_table, but do not
    -- copy nil fields (keep any corresponding field in out_table).
-- copy nil fields (keep any corresponding field in out_table).
    for _, field in ipairs(fields) do
for _, field in ipairs(fields) do
        if in_table[field] then
if in_table[field] then
            out_table[field] = in_table[field]
out_table[field] = in_table[field]
        end
end
    end
end
end
end


local function shallow_copy(t)
local function shallow_copy(t)
    -- Return a shallow copy of table t.
-- Return a shallow copy of table t.
    -- Do not need the features and overhead of the Scribunto mw.clone().
-- Do not need the features and overhead of the Scribunto mw.clone().
    local result = {}
local result = {}
    for k, v in pairs(t) do
for k, v in pairs(t) do
        result[k] = v
result[k] = v
    end
end
    return result
return result
end
end


local unit_mt = {
local unit_mt = {
    -- Metatable to get missing values for a unit that does not accept SI prefixes,
-- 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.
-- 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
-- In the latter case, and before use, fields symbol, name1, name1_us
    -- must be set from _symbol, _name1, _name1_us respectively.
-- must be set from _symbol, _name1, _name1_us respectively.
    __index = function (self, key)
__index = function (self, key)
        local value
local value
        if key == 'name1' or key == 'sym_us' then
if key == 'name1' or key == 'sym_us' then
            value = self.symbol
value = self.symbol
        elseif key == 'name2' then
elseif key == 'name2' then
            value = self.name1 .. plural_suffix
value = self.name1 .. plural_suffix
        elseif key == 'name1_us' then
elseif key == 'name1_us' then
            value = self.name1
value = self.name1
            if not rawget(self, 'name2_us') then
if not rawget(self, 'name2_us') then
                -- If name1_us is 'foot', do not make name2_us by appending plural_suffix.
-- If name1_us is 'foot', do not make name2_us by appending plural_suffix.
                self.name2_us = self.name2
self.name2_us = self.name2
            end
end
        elseif key == 'name2_us' then
elseif key == 'name2_us' then
            local raw1_us = rawget(self, 'name1_us')
local raw1_us = rawget(self, 'name1_us')
            if raw1_us then
if raw1_us then
                value = raw1_us .. plural_suffix
value = raw1_us .. plural_suffix
            else
else
                value = self.name2
value = self.name2
            end
end
        elseif key == 'link' then
elseif key == 'link' then
            value = self.name1
value = self.name1
        elseif key == 'builtin' then
elseif key == 'builtin' then
            value = false
value = false
        else
else
            return nil
return nil
        end
end
        rawset(self, key, value)
rawset(self, key, value)
        return value
return value
    end
end
}
}


local unit_prefixed_mt = {
local unit_prefixed_mt = {
    -- Metatable to get missing values for a unit that accepts SI prefixes,
-- Metatable to get missing values for a unit that accepts SI prefixes,
    -- and where a prefix has been used.
-- and where a prefix has been used.
    -- Before use, fields si_name, si_prefix must be defined.
-- Before use, fields si_name, si_prefix must be defined.
    __index = function (self, key)
__index = function (self, key)
        local value
local value
        if key == 'symbol' then
if key == 'symbol' then
            value = self.si_prefix .. self._symbol
value = self.si_prefix .. self._symbol
        elseif key == 'sym_us' then
elseif key == 'sym_us' then
            value = self.symbol  -- always the same as sym_us for prefixed units
value = self.symbol  -- always the same as sym_us for prefixed units
        elseif key == 'name1' then
elseif key == 'name1' then
            -- prefix_position is a byte (not character) position, so use Lua's sub().
-- prefix_position is a byte (not character) position, so use Lua's sub().
            local pos = rawget(self, 'prefix_position') or 1
local pos = rawget(self, 'prefix_position') or 1
            value = self._name1
value = self._name1
            value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
        elseif key == 'name2' then
elseif key == 'name2' then
            value = self.name1 .. plural_suffix
value = self.name1 .. plural_suffix
        elseif key == 'name1_us' then
elseif key == 'name1_us' then
            value = rawget(self, '_name1_us')
value = rawget(self, '_name1_us')
            if value then
if value then
                local pos = rawget(self, 'prefix_position') or 1
local pos = rawget(self, 'prefix_position') or 1
                value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
            else
else
                value = self.name1
value = self.name1
            end
end
        elseif key == 'name2_us' then
elseif key == 'name2_us' then
            if rawget(self, '_name1_us') then
if rawget(self, '_name1_us') then
                value = self.name1_us .. plural_suffix
value = self.name1_us .. plural_suffix
            else
else
                value = self.name2
value = self.name2
            end
end
        elseif key == 'link' then
elseif key == 'link' then
            value = self.name1
value = self.name1
        elseif key == 'builtin' then
elseif key == 'builtin' then
            value = false
value = false
        else
else
            return nil
return nil
        end
end
        rawset(self, key, value)
rawset(self, key, value)
        return value
return value
    end
end
}
}


local unit_per_mt = {
local unit_per_mt = {
    -- Metatable to get values for a "per" unit of form "x/y".
-- 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
-- This is never called to determine a unit name or link because "per" units
    -- are handled as a special case.
-- are handled as a special case.
    __index = function (self, key)
__index = function (self, key)
        local value
local value
        if key == 'symbol' then
if key == 'symbol' then
            local per = self.per
local per = self.per
            local unit1, unit2 = per[1], per[2]
local unit1, unit2 = per[1], per[2]
            if unit1 then
if unit1 then
                value = unit1[key] .. '/' .. unit2[key]
value = unit1[key] .. '/' .. unit2[key]
            else
else
                value = '/' .. unit2[key]
value = '/' .. unit2[key]
            end
end
        elseif key == 'sym_us' then
elseif key == 'sym_us' then
            value = self.symbol
value = self.symbol
        elseif key == 'scale' then
elseif key == 'scale' then
            local per = self.per
local per = self.per
            local unit1, unit2 = per[1], per[2]
local unit1, unit2 = per[1], per[2]
            value = (unit1 and unit1.scale or 1) * self.scalemultiplier / unit2.scale
value = (unit1 and unit1.scale or 1) * self.scalemultiplier / unit2.scale
        elseif key == 'builtin' then
elseif key == 'builtin' then
            value = false
value = false
        else
else
            return nil
return nil
        end
end
        rawset(self, key, value)
rawset(self, key, value)
        return value
return value
    end
end
}
}


local function lookup(unitcode, opt_sp_us, what, utable, fails, depth)
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,
-- 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.
-- or return false, t where t is an error message table.
    -- Parameter opt_sp_us is true for US spelling of SI prefixes and
-- 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
-- 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).
-- sp_us = true (that field may also have been in the unit definition).
    -- Parameter 'what' determines whether combination units are accepted:
-- Parameter 'what' determines whether combination units are accepted:
    --  'no_combination'  : single unit only
--  'no_combination'  : single unit only
    --  'any_combination' : single unit or combination or output multiple
--  'any_combination' : single unit or combination or output multiple
    --  'only_multiple'  : single unit or output multiple only
--  'only_multiple'  : single unit or output multiple only
    -- Parameter unitcode is a symbol (like 'g'), with an optional SI prefix (like 'kg').
-- 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;
-- If, for example, 'kg' is in this table, that entry is used;
    -- otherwise the prefix ('k') is applied to the base unit ('g').
-- otherwise the prefix ('k') is applied to the base unit ('g').
    -- If unitcode is a known combination code (and if allowed by what),
-- If unitcode is a known combination code (and if allowed by what),
    -- a table of output multiple unit tables is included in the result.
-- a table of output multiple unit tables is included in the result.
    -- For compatibility with the old template, an underscore in a unitcode is
-- For compatibility with the old template, an underscore in a unitcode is
    -- replaced with a space so usage like {{convert|350|board_feet}} works.
-- replaced with a space so usage like {{convert|350|board_feet}} works.
    -- Wikignomes may also put two spaces or "&nbsp;" in combinations, so
-- Wikignomes may also put two spaces or "&nbsp;" in combinations, so
    -- replace underscore, "&nbsp;", and multiple spaces with a single space.
-- replace underscore, "&nbsp;", and multiple spaces with a single space.
    utable = utable or all_units
utable = utable or all_units
    fails = fails or {}
fails = fails or {}
    depth = depth and depth + 1 or 1
depth = depth and depth + 1 or 1
    if depth > 9 then
if depth > 9 then
        -- There are ways to mistakenly define units which result in infinite
-- There are ways to mistakenly define units which result in infinite
        -- recursion when lookup() is called. That gives a long delay and very
-- recursion when lookup() is called. That gives a long delay and very
        -- confusing error messages, so the depth parameter is used as a guard.
-- confusing error messages, so the depth parameter is used as a guard.
        return false, { 'cvt_lookup', unitcode }
return false, { 'cvt_lookup', unitcode }
    end
end
    if unitcode == nil or unitcode == '' then
if unitcode == nil or unitcode == '' then
        return false, { 'cvt_no_unit' }
return false, { 'cvt_no_unit' }
    end
end
    unitcode = unitcode:gsub('_', ' '):gsub('&nbsp;', ' '):gsub('  +', ' ')
unitcode = unitcode:gsub('_', ' '):gsub('&nbsp;', ' '):gsub('  +', ' ')
    local t = utable[unitcode]
local t = utable[unitcode]
    if t then
if t then
        if t.shouldbe then
if t.shouldbe then
            return false, { 'cvt_should_be', t.shouldbe }
return false, { 'cvt_should_be', t.shouldbe }
        end
end
        local force_sp_us = opt_sp_us
local force_sp_us = opt_sp_us
        if t.sp_us then
if t.sp_us then
            force_sp_us = true
force_sp_us = true
            opt_sp_us = true
opt_sp_us = true
        end
end
        local target = t.target  -- nil, or unitcode is an alias for this target
local target = t.target  -- nil, or unitcode is an alias for this target
        if target then
if target then
            local success, result = lookup(target, opt_sp_us, what, utable, fails, depth)
local success, result = lookup(target, opt_sp_us, what, utable, fails, depth)
            if not success then return false, result end
if not success then return false, result end
            override_from(result, t, { 'customary', 'default', 'link', 'symbol', 'symlink' })
override_from(result, t, { 'customary', 'default', 'link', 'symbol', 'symlink' })
            local multiplier = t.multiplier
local multiplier = t.multiplier
            if multiplier then
if multiplier then
                result.multiplier = tostring(multiplier)
result.multiplier = tostring(multiplier)
                result.scale = result.scale * multiplier
result.scale = result.scale * multiplier
            end
end
            return true, result
return true, result
        end
end
        local per = t.per  -- nil/false, or a numbered table for "x/y" units
local per = t.per  -- nil/false, or a numbered table for "x/y" units
        if per then
if per then
            local result = { utype = t.utype, per = {} }
local result = { utype = t.utype, per = {} }
            result.scalemultiplier = t.multiplier or 1
result.scalemultiplier = t.multiplier or 1
            override_from(result, t, { 'invert', 'iscomplex', 'default', 'link', 'symbol', 'symlink' })
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
result.symbol_raw = (result.symbol or false)  -- to distinguish between a defined exception and a metatable calculation
            local cvt = result.per
local cvt = result.per
            local prefix
local prefix
            for i, v in ipairs(per) do
for i, v in ipairs(per) do
                if i == 1 and (v == '$' or v == '£') then
if i == 1 and (v == '$' or v == '£') then
                    prefix = v
prefix = v
                else
else
                    local success, t = lookup(v, opt_sp_us, 'no_combination', utable, fails, depth)
local success, t = lookup(v, opt_sp_us, 'no_combination', utable, fails, depth)
                    if not success then return false, t end
if not success then return false, t end
                    cvt[i] = t
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
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
force_sp_us = true
                    end
end
                end
end
            end
end
            if prefix then
if prefix then
                result.vprefix = prefix
result.vprefix = prefix
            else
else
                result.vprefix = false  -- to avoid calling __index
result.vprefix = false  -- to avoid calling __index
            end
end
            result.sp_us = force_sp_us
result.sp_us = force_sp_us
            return true, setmetatable(result, unit_per_mt)
return true, setmetatable(result, unit_per_mt)
        end
end
        local combo = t.combination  -- nil or a table of unitcodes
local combo = t.combination  -- nil or a table of unitcodes
        if combo then
if combo then
            local multiple = t.multiple
local multiple = t.multiple
            if what == 'no_combination' or (what == 'only_multiple' and multiple == nil) then
if what == 'no_combination' or (what == 'only_multiple' and multiple == nil) then
                return false, { 'cvt_bad_unit', unitcode }
return false, { 'cvt_bad_unit', unitcode }
            end
end
            -- Recursively create a combination table containing the
-- Recursively create a combination table containing the
            -- converter table of each unitcode.
-- converter table of each unitcode.
            local result = { utype = t.utype, multiple = multiple, combination = {} }
local result = { utype = t.utype, multiple = multiple, combination = {} }
            local cvt = result.combination
local cvt = result.combination
            for i, v in ipairs(combo) do
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)
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
if not success then return false, t end
                cvt[i] = t
cvt[i] = t
            end
end
            return true, result
return true, result
        end
end
        local result = shallow_copy(t)
local result = shallow_copy(t)
        result.sp_us = force_sp_us
result.sp_us = force_sp_us
        if result.prefixes then
if result.prefixes then
            result.symbol = result._symbol
result.symbol = result._symbol
            result.name1 = result._name1
result.name1 = result._name1
            result.name1_us = result._name1_us
result.name1_us = result._name1_us
        end
end
        return true, setmetatable(result, unit_mt)
return true, setmetatable(result, unit_mt)
    end
end
    for plen = SIprefixes[1] or 2, 1, -1 do
for plen = SIprefixes[1] or 2, 1, -1 do
        -- Look for an SI prefix; should never occur with an alias.
-- Look for an SI prefix; should never occur with an alias.
        -- Check for longer prefix first ('dam' is decametre).
-- Check for longer prefix first ('dam' is decametre).
        -- SIprefixes[1] = prefix maximum #characters (as seen by mw.ustring.sub).
-- SIprefixes[1] = prefix maximum #characters (as seen by mw.ustring.sub).
        local prefix = usub(unitcode, 1, plen)
local prefix = usub(unitcode, 1, plen)
        local si = SIprefixes[prefix]
local si = SIprefixes[prefix]
        if si then
if si then
            local t = utable[usub(unitcode, plen+1)]
local t = utable[usub(unitcode, plen+1)]
            if t and t.prefixes then
if t and t.prefixes then
                local result = shallow_copy(t)
local result = shallow_copy(t)
                if opt_sp_us then
if opt_sp_us then
                    result.sp_us = true
result.sp_us = true
                end
end
                if result.sp_us and si.name_us then
if result.sp_us and si.name_us then
                    result.si_name = si.name_us
result.si_name = si.name_us
                else
else
                    result.si_name = si.name
result.si_name = si.name
                end
end
                result.si_prefix = si.prefix or prefix
result.si_prefix = si.prefix or prefix
                result.scale = t.scale * 10 ^ (si.exponent * t.prefixes)
result.scale = t.scale * 10 ^ (si.exponent * t.prefixes)
                return true, setmetatable(result, unit_prefixed_mt)
return true, setmetatable(result, unit_prefixed_mt)
            end
end
        end
end
    end
end
    -- Accept any unit with an engineering notation prefix like "e6cuft"
-- Accept any unit with an engineering notation prefix like "e6cuft"
    -- (million cubic feet), but not chained prefixes like "e3e6cuft",
-- (million cubic feet), but not chained prefixes like "e3e6cuft",
    -- and not if the unit is a combination or multiple,
-- and not if the unit is a combination or multiple,
    -- and not if the unit has an offset or is a built-in.
-- and not if the unit has an offset or is a built-in.
    -- Only en digits are accepted.
-- Only en digits are accepted.
    local exponent, baseunit = unitcode:match('^e(%d+)(.*)')
local exponent, baseunit = unitcode:match('^e(%d+)(.*)')
    if exponent then
if exponent then
        local engscale = eng_scales[exponent]
local engscale = eng_scales[exponent]
        if engscale then
if engscale then
            local success, result = lookup(baseunit, opt_sp_us, 'no_combination', utable, fails, depth)
local success, result = lookup(baseunit, opt_sp_us, 'no_combination', utable, fails, depth)
            if not success then return false, result end
if not success then return false, result end
            if not (result.offset or result.builtin or result.engscale) then
if not (result.offset or result.builtin or result.engscale) then
                result.defkey = unitcode  -- key to lookup default exception
result.defkey = unitcode  -- key to lookup default exception
                result.engscale = engscale
result.engscale = engscale
                result.scale = result.scale * 10 ^ tonumber(exponent)
result.scale = result.scale * 10 ^ tonumber(exponent)
                return true, result
return true, result
            end
end
        end
end
    end
end
    -- Accept user-defined combo like "MT+LT" (convert output to MT and LT).
-- Accept user-defined combo like "MT+LT" (convert output to MT and LT).
    local combo = collection()
local combo = collection()
    for item in string.gmatch(unitcode .. '+', '%s*(.-)%s*%+') do
for item in string.gmatch(unitcode .. '+', '%s*(.-)%s*%+') do
        if item ~= '' then
if item ~= '' then
            combo:add(item)
combo:add(item)
        end
end
    end
end
    if combo.n > 1 then
if combo.n > 1 then
        if what == 'no_combination' or what == 'only_multiple' then
if what == 'no_combination' or what == 'only_multiple' then
            return false, { 'cvt_bad_unit', unitcode }
return false, { 'cvt_bad_unit', unitcode }
        end
end
        local result = { combination = {} }
local result = { combination = {} }
        local cvt = result.combination
local cvt = result.combination
        for i, v in ipairs(combo) do
for i, v in ipairs(combo) do
            local success, t = lookup(v, opt_sp_us, 'no_combination', utable, fails, depth)
local success, t = lookup(v, opt_sp_us, 'no_combination', utable, fails, depth)
            if not success then return false, t end
if not success then return false, t end
            if i == 1 then
if i == 1 then
                result.utype = t.utype
result.utype = t.utype
            else
else
                local mismatch = check_mismatch(result, t)
local mismatch = check_mismatch(result, t)
                if mismatch then
if mismatch then
                    return false, mismatch
return false, mismatch
                end
end
            end
end
            cvt[i] = t
cvt[i] = t
        end
end
        return true, result
return true, result
    end
end
    if not extra_units then
if not extra_units then
        local success, extra = pcall(function () return require(extra_module).extra_units end)
local success, extra = pcall(function () return require(extra_module).extra_units end)
        if success and type(extra) == 'table' then
if success and type(extra) == 'table' then
            extra_units = extra
extra_units = extra
        end
end
    end
end
    if extra_units then
if extra_units then
        -- A unit in one data table might refer to a unit in the other table, so
-- 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.
-- switch between them, relying on fails or depth to terminate loops.
        local failed = fails[unitcode]
local failed = fails[unitcode]
        if not failed then
if not failed then
            fails[unitcode] = true
fails[unitcode] = true
            local other = (utable == all_units) and extra_units or all_units
local other = (utable == all_units) and extra_units or all_units
            local success, result = lookup(unitcode, opt_sp_us, what, other, fails, depth)
local success, result = lookup(unitcode, opt_sp_us, what, other, fails, depth)
            if success then
if success then
                return true, result
return true, result
            end
end
        end
end
    end
end
    return false, { 'cvt_unknown', unitcode }
return false, { 'cvt_unknown', unitcode }
end
end


local function valid_number(num)
local function valid_number(num)
    -- Return true if num is a valid number.
-- Return true if num is a valid number.
    -- In Scribunto (different from some standard Lua), when expressed as a string,
-- In Scribunto (different from some standard Lua), when expressed as a string,
    -- overflow or other problems are indicated with text like "inf" or "nan"
-- overflow or other problems are indicated with text like "inf" or "nan"
    -- which are regarded as invalid here (each contains "n").
-- which are regarded as invalid here (each contains "n").
    if type(num) == 'number' and tostring(num):find('n', 1, true) == nil then
if type(num) == 'number' and tostring(num):find('n', 1, true) == nil then
        return true
return true
    end
end
end
end


local function ntsh(num, debug)
local function ntsh(num, debug)
    -- Return html text to be used for a hidden sort key so that
-- Return html text to be used for a hidden sort key so that
    -- the given number will be sorted in numeric order.
-- the given number will be sorted in numeric order.
    -- If debug == true, output is in a box (not hidden).
-- If debug == true, output is in a box (not hidden).
    -- This implements Template:Ntsh (number table sorting, hidden).
-- This implements Template:Ntsh (number table sorting, hidden).
    local result, style
local result, style
    if not valid_number(num) then
if not valid_number(num) then
        if num < 0 then
if num < 0 then
            result = '1000000000000000000'
result = '1000000000000000000'
        else
else
            result = '9000000000000000000'
result = '9000000000000000000'
        end
end
    elseif num == 0 then
elseif num == 0 then
        result = '5000000000000000000'
result = '5000000000000000000'
    else
else
        local mag = floor(log10(abs(num)) + 1e-14)
local mag = floor(log10(abs(num)) + 1e-14)
        local prefix
local prefix
        if num > 0 then
if num > 0 then
            prefix = 7000 + mag
prefix = 7000 + mag
        else
else
            prefix = 2999 - mag
prefix = 2999 - mag
            num = num + 10^(mag+1)
num = num + 10^(mag+1)
        end
end
        result = format('%d', prefix) .. format('%015.0f', floor(num * 10^(14-mag)))
result = format('%d', prefix) .. format('%015.0f', floor(num * 10^(14-mag)))
    end
end
    if debug then
if debug then
        style = 'border:1px solid'
style = 'border:1px solid'
    else
else
        style = 'display:none'
style = 'display:none'
    end
end
    return '<span style="' .. style .. '">' .. result .. '</span>'
return '<span style="' .. style .. '">' .. result .. '</span>'
end
end


local function hyphenated(name, parts)
local function hyphenated(name, parts)
    -- Return a hyphenated form of given name (for adjectival usage).
-- 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.
-- The name may be linked and the target of the link must not be changed.
    -- Hypothetical examples:
-- Hypothetical examples:
    --  [[long ton|ton]]        →  [[long ton|ton]]          (no change)
--  [[long ton|ton]]        →  [[long ton|ton]]          (no change)
    --  [[tonne|long ton]]      →  [[tonne|long-ton]]
--  [[tonne|long ton]]      →  [[tonne|long-ton]]
    --  [[metric ton|long ton]]  →  [[metric ton|long-ton]]
--  [[metric ton|long ton]]  →  [[metric ton|long-ton]]
    --  [[long ton]]            →  [[long ton|long-ton]]
--  [[long ton]]            →  [[long ton|long-ton]]
    -- Input can also have multiple links in a single name like:
-- Input can also have multiple links in a single name like:
    --  [[United States customary units|U.S.]] [[US gallon|gallon]]
--  [[United States customary units|U.S.]] [[US gallon|gallon]]
    --  [[mile]]s per [[United States customary units|U.S.]] [[quart]]
--  [[mile]]s per [[United States customary units|U.S.]] [[quart]]
    --  [[long ton]]s per [[short ton]]
--  [[long ton]]s per [[short ton]]
    -- Assume that links cannot be nested (never like "[[abc[[def]]ghi]]").
-- Assume that links cannot be nested (never like "[[abc[[def]]ghi]]").
    -- This uses a simple and efficient procedure that works for most cases.
-- This uses a simple and efficient procedure that works for most cases.
    -- Some units (if used) would require more, and can later think about
-- Some units (if used) would require more, and can later think about
    -- adding a method to handle exceptions.
-- adding a method to handle exceptions.
    -- The procedure is to replace each space with a hyphen, but
-- The procedure is to replace each space with a hyphen, but
    -- not a space after ')' [for "(pre-1954&nbsp;US) nautical mile"], and
-- not a space after ')' [for "(pre-1954&nbsp;US) nautical mile"], and
    -- not spaces immediately before '(' or in '(...)' [for cases like
-- not spaces immediately before '(' or in '(...)' [for cases like
    -- "British thermal unit (ISO)" and "Calorie (International Steam Table)"].
-- "British thermal unit (ISO)" and "Calorie (International Steam Table)"].
    if name:find(' ', 1, true) then
if name:find(' ', 1, true) then
        if parts then
if parts then
            local pos
local pos
            if name:sub(1, 1) == '(' then
if name:sub(1, 1) == '(' then
                pos = name:find(')', 1, true)
pos = name:find(')', 1, true)
                if pos then
if pos then
                    return name:sub(1, pos+1) .. name:sub(pos+2):gsub(' ', '-')
return name:sub(1, pos+1) .. name:sub(pos+2):gsub(' ', '-')
                end
end
            elseif name:sub(-1, -1) == ')' then
elseif name:sub(-1, -1) == ')' then
                pos = name:find('(', 1, true)
pos = name:find('(', 1, true)
                if pos then
if pos then
                    return name:sub(1, pos-2):gsub(' ', '-') .. name:sub(pos-1)
return name:sub(1, pos-2):gsub(' ', '-') .. name:sub(pos-1)
                end
end
            end
end
            return name:gsub(' ', '-')
return name:gsub(' ', '-')
        end
end
        parts = collection()
parts = collection()
        for before, item, after in name:gmatch('([^[]*)(%[%[[^[]*%]%])([^[]*)') do
for before, item, after in name:gmatch('([^[]*)(%[%[[^[]*%]%])([^[]*)') do
            if item:find(' ', 1, true) then
if item:find(' ', 1, true) then
                local prefix
local prefix
                local plen = item:find('|', 1, true)
local plen = item:find('|', 1, true)
                if plen then
if plen then
                    prefix = item:sub(1, plen)
prefix = item:sub(1, plen)
                    item = item:sub(plen + 1, -3)
item = item:sub(plen + 1, -3)
                else
else
                    prefix = item:sub(1, -3) .. '|'
prefix = item:sub(1, -3) .. '|'
                    item = item:sub(3, -3)
item = item:sub(3, -3)
                end
end
                item = prefix .. hyphenated(item, parts) .. ']]'
item = prefix .. hyphenated(item, parts) .. ']]'
            end
end
            parts:add(before:gsub(' ', '-') .. item .. after:gsub(' ', '-'))
parts:add(before:gsub(' ', '-') .. item .. after:gsub(' ', '-'))
        end
end
        if parts.n == 0 then
if parts.n == 0 then
            -- No link like "[[...]]" was found in the original name.
-- No link like "[[...]]" was found in the original name.
            parts:add(hyphenated(name, parts))
parts:add(hyphenated(name, parts))
        end
end
        return table.concat(parts)
return table.concat(parts)
    end
end
    return name
return name
end
end


local function hyphenated_maybe(parms, want_name, sep, id, inout)
local function hyphenated_maybe(parms, want_name, sep, id, inout)
    -- Return s, f where
-- Return s, f where
    --  s = id, possibly modified
--  s = id, possibly modified
    --  f = true if hyphenated
--  f = true if hyphenated
    -- Possible modifications: hyphenate; prepend '-'; append mid text.
-- Possible modifications: hyphenate; prepend '-'; append mid text.
    if id == nil or id == '' then
if id == nil or id == '' then
        return ''
return ''
    end
end
    local mid
local mid
    if parms.opt_adjectival then
if parms.opt_adjectival then
        if inout == (parms.opt_flip and 'out' or 'in') then
if inout == (parms.opt_flip and 'out' or 'in') then
            mid = parms.mid
mid = parms.mid
        end
end
        if want_name then
if want_name then
            return '-' .. hyphenated(id) .. (mid or ''), true
return '-' .. hyphenated(id) .. (mid or ''), true
        end
end
    end
end
    return sep .. id .. (mid or '')
return sep .. id .. (mid or '')
end
end


local function change_sign(text)
local function change_sign(text)
    -- Change sign of text for correct appearance because it is negated.
-- Change sign of text for correct appearance because it is negated.
    if text:sub(1, 1) == '-' then
if text:sub(1, 1) == '-' then
        return text:sub(2)
return text:sub(2)
    end
end
    return '-' .. text
return '-' .. text
end
end


local function use_minus(text)
local function use_minus(text)
    -- Return text with Unicode minus instead of '-', if present.
-- Return text with Unicode minus instead of '-', if present.
    if text:sub(1, 1) == '-' then
if text:sub(1, 1) == '-' then
        return MINUS .. text:sub(2)
return MINUS .. text:sub(2)
    end
end
    return text
return text
end
end


local function digit_grouper(method, gaps)
local function digit_grouper(method, gaps)
    -- Return a table to hold groups of digits which can be joined with
-- Return a table to hold groups of digits which can be joined with
    -- suitable separators (such as commas).
-- suitable separators (such as commas).
    -- Each group is separately translated to the local language because
-- Each group is separately translated to the local language because
    -- gap separators include digits which should not be translated.
-- gap separators include digits which should not be translated.
    -- Parameter method is a number or nil:
-- Parameter method is a number or nil:
    --  3 for 3-digit grouping, or
--  3 for 3-digit grouping, or
    --  2 for 3-then-2 grouping.
--  2 for 3-then-2 grouping.
    -- Parameter gaps is true to use <span> gaps (numsep ignored).
-- Parameter gaps is true to use <span> gaps (numsep ignored).
    return {
return {
        n = 0,
n = 0,
        add = function (self, digits)
add = function (self, digits)
            self.n = self.n + 1
self.n = self.n + 1
            self[self.n] = from_en(digits)
self[self.n] = from_en(digits)
        end,
end,
        join = function (self, rhs)
join = function (self, rhs)
            -- Concatenate in reverse order.
-- Concatenate in reverse order.
            if gaps then
if gaps then
                local result = ''
local result = ''
                for i = 1, self.n - 1 do
for i = 1, self.n - 1 do
                    result = '<span style="margin-left: 0.25em">' .. self[i] .. '</span>' .. result
result = '<span style="margin-left: 0.25em">' .. self[i] .. '</span>' .. result
                end
end
                return '<span style="white-space: nowrap">' .. self[self.n] .. result .. from_en(rhs) .. '</span>'
return '<span style="white-space: nowrap">' .. self[self.n] .. result .. from_en(rhs) .. '</span>'
            else
else
                local result = self[1]
local result = self[1]
                for i = 2, self.n do
for i = 2, self.n do
                    result = self[i] .. numsep .. result
result = self[i] .. numsep .. result
                end
end
                return result .. from_en(rhs)
return result .. from_en(rhs)
            end
end
        end,
end,
        step = 3,
step = 3,
        next_position = function (self, previous)
next_position = function (self, previous)
            -- Return position of digit just before next group.
-- Return position of digit just before next group.
            -- Digits are grouped from right-to-left (least significant first).
-- Digits are grouped from right-to-left (least significant first).
            local result = previous - self.step
local result = previous - self.step
            if method == 2 then
if method == 2 then
                self.step = 2
self.step = 2
            end
end
            return (result < 0) and 0 or result
return (result < 0) and 0 or result
        end,
end,
    }
}
end
end


local function with_separator(parms, text)
local function with_separator(parms, text)
    -- Input text is a number in en digits and with '.' decimal mark.
-- Input text is a number in en digits and with '.' decimal mark.
    -- Return an equivalent of text, formatted for display:
-- Return an equivalent of text, formatted for display:
    --  with a custom decimal mark instead of '.', if wanted
--  with a custom decimal mark instead of '.', if wanted
    --  with thousand separators inserted, if wanted
--  with thousand separators inserted, if wanted
    --  digits in local language
--  digits in local language
    -- The given text is like '123' or '12345.6789' or '1.23e45'
-- The given text is like '123' or '12345.6789' or '1.23e45'
    -- (e notation can only occur when processing an input value).
-- (e notation can only occur when processing an input value).
    -- The text has no sign (caller inserts that later, if necessary).
-- The text has no sign (caller inserts that later, if necessary).
    -- Separator is inserted only in the integer part of the significand
-- Separator is inserted only in the integer part of the significand
    -- (not after the decimal mark, and not after 'e' or 'E').
-- (not after the decimal mark, and not after 'e' or 'E').
    if parms.opt_nocomma or numsep == '' then
if parms.opt_nocomma or numsep == '' then
        return from_en(text)
return from_en(text)
    end
end
    local last = text:match('()[.eE]')  -- () returns position
local last = text:match('()[.eE]')  -- () returns position
    if last == nil then
if last == nil then
        last = #text
last = #text
    else
else
        last = last - 1  -- index of last character before dot/e/E
last = last - 1  -- index of last character before dot/e/E
    end
end
    if last < 4 or (last == 4 and parms.opt_comma5) then
if last < 4 or (last == 4 and parms.opt_comma5) then
        return from_en(text)
return from_en(text)
    end
end
    local groups = digit_grouper(group_method, parms.opt_gaps)
local groups = digit_grouper(group_method, parms.opt_gaps)
    local i = last
local i = last
    while i > 0 do
while i > 0 do
        local position = groups:next_position(i)
local position = groups:next_position(i)
        groups:add(text:sub(position+1, i))
groups:add(text:sub(position+1, i))
        i = position
i = position
    end
end
    return groups:join(text:sub(last+1))
return groups:join(text:sub(last+1))
end
end


Line 864: Line 864:


local function with_exponent(show, exponent)
local function with_exponent(show, exponent)
    -- Return wikitext to display the implied value in scientific notation.
-- Return wikitext to display the implied value in scientific notation.
    -- Input uses en digits; output uses digits in local language.
-- Input uses en digits; output uses digits in local language.
    if #show > 1 then
if #show > 1 then
        show = show:sub(1, 1) .. '.' .. show:sub(2)
show = show:sub(1, 1) .. '.' .. show:sub(2)
    end
end
    return format(fmtpower, from_en(show), from_en('10'), use_minus(from_en(tostring(exponent))))
return format(fmtpower, from_en(show), from_en('10'), use_minus(from_en(tostring(exponent))))
end
end


local function make_sigfig(value, sigfig)
local function make_sigfig(value, sigfig)
    -- Return show, exponent that are equivalent to the result of
-- Return show, exponent that are equivalent to the result of
    -- converting the number 'value' (where value >= 0) to a string,
-- converting the number 'value' (where value >= 0) to a string,
    -- rounded to 'sigfig' significant figures.
-- rounded to 'sigfig' significant figures.
    -- The returned items are:
-- The returned items are:
    --  show: a string of digits; no sign and no dot;
--  show: a string of digits; no sign and no dot;
    --        there is an implied dot before show.
--        there is an implied dot before show.
    --  exponent: a number (an integer) to shift the implied dot.
--  exponent: a number (an integer) to shift the implied dot.
    -- Resulting value = tonumber('.' .. show) * 10^exponent.
-- Resulting value = tonumber('.' .. show) * 10^exponent.
    -- Examples:
-- Examples:
    --  make_sigfig(23.456, 3) returns '235', 2 (.235 * 10^2).
--  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.0023456, 3) returns '235', -2 (.235 * 10^-2).
    --  make_sigfig(0, 3) returns '000', 1 (.000 * 10^1).
--  make_sigfig(0, 3) returns '000', 1 (.000 * 10^1).
    if sigfig <= 0 then
if sigfig <= 0 then
        sigfig = 1
sigfig = 1
    elseif sigfig > maxsigfig then
elseif sigfig > maxsigfig then
        sigfig = maxsigfig
sigfig = maxsigfig
    end
end
    if value == 0 then
if value == 0 then
        return string.rep('0', sigfig), 1
return string.rep('0', sigfig), 1
    end
end
    local exp, frac = math.modf(log10(value))
local exp, frac = math.modf(log10(value))
    if frac >= 0 then
if frac >= 0 then
        frac = frac - 1
frac = frac - 1
        exp = exp + 1
exp = exp + 1
    end
end
    local digits = format('%.0f', 10^(frac + sigfig))
local digits = format('%.0f', 10^(frac + sigfig))
    if #digits > sigfig then
if #digits > sigfig then
        -- Overflow (for sigfig=3: like 0.9999 rounding to "1000"; need "100").
-- Overflow (for sigfig=3: like 0.9999 rounding to "1000"; need "100").
        digits = digits:sub(1, sigfig)
digits = digits:sub(1, sigfig)
        exp = exp + 1
exp = exp + 1
    end
end
    assert(#digits == sigfig, 'Bug: rounded number has wrong length')
assert(#digits == sigfig, 'Bug: rounded number has wrong length')
    return digits, exp
return digits, exp
end
end


local function format_number(parms, show, exponent, isnegative)
local function format_number(parms, show, exponent, isnegative)
    -- Parameter show is a number in en digits and with '.' decimal mark.
-- Parameter show is a number in en digits and with '.' decimal mark.
    -- Return t where t is a table with fields:
-- Return t where t is a table with fields:
    --  show = wikitext formatted to display implied value
--  show = wikitext formatted to display implied value
    --          (digits in local language)
--          (digits in local language)
    --  is_scientific = true if show uses scientific notation
--  is_scientific = true if show uses scientific notation
    --  clean = unformatted show (possibly adjusted and with inserted '.')
--  clean = unformatted show (possibly adjusted and with inserted '.')
    --          (en digits)
--          (en digits)
    --  sign = '' or MINUS
--  sign = '' or MINUS
    --  exponent = exponent (possibly adjusted)
--  exponent = exponent (possibly adjusted)
    -- The clean and exponent fields can be used to calculate the
-- The clean and exponent fields can be used to calculate the
    -- rounded absolute value, if needed.
-- rounded absolute value, if needed.
    --
--
    -- The value implied by the arguments is found from:
-- The value implied by the arguments is found from:
    --  exponent is nil; and
--  exponent is nil; and
    --  show is a string of digits (no sign), with an optional dot;
--  show is a string of digits (no sign), with an optional dot;
    --  show = '123.4' is value 123.4, '1234' is value 1234.0;
--  show = '123.4' is value 123.4, '1234' is value 1234.0;
    -- or:
-- or:
    --  exponent is an integer indicating where dot should be;
--  exponent is an integer indicating where dot should be;
    --  show is a string of digits (no sign and no dot);
--  show is a string of digits (no sign and no dot);
    --  there is an implied dot before show;
--  there is an implied dot before show;
    --  show does not start with '0';
--  show does not start with '0';
    --  show = '1234', exponent = 3 is value 0.1234*10^3 = 123.4.
--  show = '1234', exponent = 3 is value 0.1234*10^3 = 123.4.
    --
--
    -- The formatted result:
-- The formatted result:
    -- * Is for an output value and is spelled if wanted and possible.
-- * Is for an output value and is spelled if wanted and possible.
    -- * Includes a Unicode minus if isnegative.
-- * Includes a Unicode minus if isnegative.
    -- * Uses a custom decimal mark, if wanted.
-- * Uses a custom decimal mark, if wanted.
    -- * Has digits grouped where necessary, if wanted.
-- * Has digits grouped where necessary, if wanted.
    -- * Uses scientific notation for very small or large values
-- * Uses scientific notation for very small or large values
    --  (which forces output to not be spelled).
--  (which forces output to not be spelled).
    -- * Has no more than maxsigfig significant digits
-- * Has no more than maxsigfig significant digits
    --  (same as old template and {{#expr}}).
--  (same as old template and {{#expr}}).
    local sign = isnegative and MINUS or ''
local sign = isnegative and MINUS or ''
    local maxlen = maxsigfig
local maxlen = maxsigfig
    if exponent == nil then
if exponent == nil then
        local integer, dot, fraction = show:match('^(%d*)(%.?)(.*)')
local integer, dot, fraction = show:match('^(%d*)(%.?)(.*)')
        if #integer >= 10 then
if #integer >= 10 then
            show = integer .. fraction
show = integer .. fraction
            exponent = #integer
exponent = #integer
        elseif integer == '0' or integer == '' then
elseif integer == '0' or integer == '' then
            local zeros, figs = fraction:match('^(0*)([^0]?.*)')
local zeros, figs = fraction:match('^(0*)([^0]?.*)')
            if #figs == 0 then
if #figs == 0 then
                if #zeros > maxlen then
if #zeros > maxlen then
                    show = '0.' .. zeros:sub(1, maxlen)
show = '0.' .. zeros:sub(1, maxlen)
                end
end
            elseif #zeros >= 4 then
elseif #zeros >= 4 then
                show = figs
show = figs
                exponent = -#zeros
exponent = -#zeros
            elseif #figs > maxlen then
elseif #figs > maxlen then
                show = '0.' .. zeros .. figs:sub(1, maxlen)
show = '0.' .. zeros .. figs:sub(1, maxlen)
            end
end
        else
else
            maxlen = maxlen + #dot
maxlen = maxlen + #dot
            if #show > maxlen then
if #show > maxlen then
                show = show:sub(1, maxlen)
show = show:sub(1, maxlen)
            end
end
        end
end
    end
end
    if exponent then
if exponent then
        if #show > maxlen then
if #show > maxlen then
            show = show:sub(1, maxlen)
show = show:sub(1, maxlen)
        end
end
        if exponent > 10 or exponent <= -4 or (exponent == 10 and show ~= '1000000000') then
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).
-- Rounded value satisfies: value >= 1e9 or value < 1e-4 (1e9 = 0.1e10).
            return {
return {
                clean = '.' .. show,
clean = '.' .. show,
                exponent = exponent,
exponent = exponent,
                sign = sign,
sign = sign,
                show = sign .. with_exponent(show, exponent-1),
show = sign .. with_exponent(show, exponent-1),
                is_scientific = true,
is_scientific = true,
            }
}
        end
end
        if exponent >= #show then
if exponent >= #show then
            show = show .. string.rep('0', exponent - #show)  -- result has no dot
show = show .. string.rep('0', exponent - #show)  -- result has no dot
        elseif exponent <= 0 then
elseif exponent <= 0 then
            show = '0.' .. string.rep('0', -exponent) .. show
show = '0.' .. string.rep('0', -exponent) .. show
        else
else
            show = show:sub(1, exponent) .. '.' .. show:sub(exponent+1)
show = show:sub(1, exponent) .. '.' .. show:sub(exponent+1)
        end
end
    end
end
    if isnegative and show:match('^0.?0*$') then
if isnegative and show:match('^0.?0*$') then
        sign = ''  -- don't show minus if result is negative but rounds to zero
sign = ''  -- don't show minus if result is negative but rounds to zero
    end
end
    local formatted_show = sign .. with_separator(parms, show)
local formatted_show = sign .. with_separator(parms, show)
    if parms.opt_spell_out then
if parms.opt_spell_out then
        formatted_show = spell_number(parms, sign .. show) or formatted_show
formatted_show = spell_number(parms, sign .. show) or formatted_show
    end
end
    return {
return {
        clean = show,
clean = show,
        sign = sign,
sign = sign,
        show = formatted_show,
show = formatted_show,
    }
}
end
end


Line 1,006: Line 1,006:
-- 2013-07-20 Trying new styles proposed at [[Template talk:Convert]].
-- 2013-07-20 Trying new styles proposed at [[Template talk:Convert]].
local fracfmt = {
local fracfmt = {
{ -- Like {{frac}} (fraction slash).
{ -- Like {{frac}} (fraction slash).
  -- 1/2    : sign, numerator, denominator
-- 1/2    : sign, numerator, denominator
  -- 1+2/3  : signed_wholenumber, 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>',
  '<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).
{ -- 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  : 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
-- 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" 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;">&nbsp;</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>',
'<span class="sfrac nowrap">%s<span style="display:none;">&nbsp;</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.
{ -- Like old {{convert}} template.
  -- 1///2  : sign, numerator, denominator
-- 1///2  : sign, numerator, denominator
  -- 1+2///3: signed_wholenumber, 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 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>',
'<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)
local function extract_fraction(parms, text, negative)
    -- If text represents a fraction, return
-- If text represents a fraction, return
    --  value, altvalue, show, spelled, denominator
--  value, altvalue, show, spelled, denominator
    -- where
-- where
    --  value is a number (value of the fraction in argument text)
--  value is a number (value of the fraction in argument text)
    --  altvalue is an alternate interpretation of any fraction for the hands
--  altvalue is an alternate interpretation of any fraction for the hands
    --        unit where "14.1+3/4" means 14 hands 1.75 inches!
--        unit where "14.1+3/4" means 14 hands 1.75 inches!
    --  show is a string (formatted text for display of an input value,
--  show is a string (formatted text for display of an input value,
    --        and is spelled if wanted and possible)
--        and is spelled if wanted and possible)
    --  spelled is true if show was spelled
--  spelled is true if show was spelled
    --  denominator is value of the denominator in the fraction
--  denominator is value of the denominator in the fraction
    -- Otherwise, return nil.
-- Otherwise, return nil.
    -- Input uses en digits and '.' decimal mark (input has been translated).
-- Input uses en digits and '.' decimal mark (input has been translated).
    -- Output uses digits in local language and custom decimal mark, if any.
-- Output uses digits in local language and custom decimal mark, if any.
    --
--
    -- In the following, '(3/8)' represents the wikitext required to
-- In the following, '(3/8)' represents the wikitext required to
    -- display a fraction with numerator 3 and denominator 8.
-- display a fraction with numerator 3 and denominator 8.
    -- In the wikitext, Unicode minus is used for a negative value.
-- In the wikitext, Unicode minus is used for a negative value.
    --  text          value, show            value, show
--  text          value, show            value, show
    --                if not negative      if negative
--                if not negative      if negative
    --  3 / 8        0.375, '(3/8)'        -0.375, '−(3/8)'
--  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    2.375, '2(3/8)'      -1.625, '−2(−3/8)'
    --  2 - 3 / 8    1.625, '2(−3/8)'      -2.375, '−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      3.5  , '1/(20/8)'    1.5  , '−1/(−20/8)'
    --  1 - 20/8      -1.5., '1(−20/8)'    -3.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
-- Wherever an integer appears above, numbers like 1.25 or 12.5e-3
    -- (which may be negative) are also accepted (like old template).
-- (which may be negative) are also accepted (like old template).
    -- Template interprets '1.23e+2+12/24' as '123(12/24)' = 123.5!
-- Template interprets '1.23e+2+12/24' as '123(12/24)' = 123.5!
    local numstr, whole, value, altvalue
local numstr, whole, value, altvalue
    local lhs, slash, denstr = text:match('^%s*([^/]-)%s*(/+)%s*(.-)%s*$')
local lhs, slash, denstr = text:match('^%s*([^/]-)%s*(/+)%s*(.-)%s*$')
    local denominator = tonumber(denstr)
local denominator = tonumber(denstr)
    if denominator == nil then return nil end
if denominator == nil then return nil end
    local wholestr, negfrac, rhs = lhs:match('^%s*(.-[^eE])%s*([+-])%s*(.-)%s*$')
local wholestr, negfrac, rhs = lhs:match('^%s*(.-[^eE])%s*([+-])%s*(.-)%s*$')
    if wholestr == nil or wholestr == '' then
if wholestr == nil or wholestr == '' then
        wholestr = nil
wholestr = nil
        whole = 0
whole = 0
        numstr = lhs
numstr = lhs
    else
else
        whole = tonumber(wholestr)
whole = tonumber(wholestr)
        if whole == nil then return nil end
if whole == nil then return nil end
        numstr = rhs
numstr = rhs
    end
end
    negfrac = (negfrac == '-')
negfrac = (negfrac == '-')
    local numerator = tonumber(numstr)
local numerator = tonumber(numstr)
    if numerator == nil then return nil end
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.
-- Spelling of silly inputs like "-2+3/8" or "2+3/+8" (mixed or excess signs) is not supported.
    local do_spell
local do_spell
    if negative == negfrac or wholestr == nil then
if negative == negfrac or wholestr == nil then
        value = whole + numerator / denominator
value = whole + numerator / denominator
        altvalue = whole + numerator / (denominator * 10)
altvalue = whole + numerator / (denominator * 10)
        do_spell = parms.opt_spell_in
do_spell = parms.opt_spell_in
        if do_spell then
if do_spell then
            if not (numstr:match('^%d') and denstr:match('^%d')) then  -- if either has a sign
if not (numstr:match('^%d') and denstr:match('^%d')) then  -- if either has a sign
                do_spell = false
do_spell = false
            end
end
        end
end
    else
else
        value = whole - numerator / denominator
value = whole - numerator / denominator
        altvalue = whole - numerator / (denominator * 10)
altvalue = whole - numerator / (denominator * 10)
        numstr = change_sign(numstr)
numstr = change_sign(numstr)
        do_spell = false
do_spell = false
    end
end
    if not valid_number(value) then
if not valid_number(value) then
        return nil  -- overflow or similar
return nil  -- overflow or similar
    end
end
    numstr = use_minus(numstr)
numstr = use_minus(numstr)
    denstr = use_minus(denstr)
denstr = use_minus(denstr)
    local style = #slash  -- kludge: 1, 2, or 3 slashes can be used to select style
local style = #slash  -- kludge: 1, 2, or 3 slashes can be used to select style
    if style > 3 then style = 3 end
if style > 3 then style = 3 end
    local wikitext
local wikitext
    if wholestr then
if wholestr then
        if negative then
if negative then
            wholestr = change_sign(wholestr)
wholestr = change_sign(wholestr)
        end
end
        local fmt = fracfmt[style][2]
local fmt = fracfmt[style][2]
        if style < 3 then
if style < 3 then
            wikitext = format(fmt, use_minus(from_en(wholestr)), from_en(numstr), from_en(denstr))
wikitext = format(fmt, use_minus(from_en(wholestr)), from_en(numstr), from_en(denstr))
        else
else
            local sign = negative and MINUS or '+'
local sign = negative and MINUS or '+'
            wikitext = format(fmt, use_minus(from_en(wholestr)), sign, from_en(numstr), from_en(denstr))
wikitext = format(fmt, use_minus(from_en(wholestr)), sign, from_en(numstr), from_en(denstr))
        end
end
    else
else
        local sign = negative and MINUS or ''
local sign = negative and MINUS or ''
        wikitext = format(fracfmt[style][1], sign, from_en(numstr), from_en(denstr))
wikitext = format(fracfmt[style][1], sign, from_en(numstr), from_en(denstr))
    end
end
    if do_spell then
if do_spell then
        local numsign = (wholestr or not negative) and '' or '-'
local numsign = (wholestr or not negative) and '' or '-'
        wikitext = spell_number(parms, wholestr, numsign .. numstr, denstr) or wikitext
wikitext = spell_number(parms, wholestr, numsign .. numstr, denstr) or wikitext
    end
end
    return value, altvalue, wikitext, do_spell, denominator
return value, altvalue, wikitext, do_spell, denominator
end
end


local function extract_number(parms, text, another, no_fraction)
local function extract_number(parms, text, another, no_fraction)
    -- Return true, info if can extract a number from text,
-- Return true, info if can extract a number from text,
    -- where info is a table with the result,
-- where info is a table with the result,
    -- or return false, t where t is an error message table.
-- or return false, t where t is an error message table.
    -- Input can use en digits or digits in local language.
-- Input can use en digits or digits in local language.
    -- Parameter another = true if the expected value is not the first.
-- Parameter another = true if the expected value is not the first.
    -- Before processing, the input text is cleaned:
-- Before processing, the input text is cleaned:
    -- * Any thousand separators (valid or not) are removed.
-- * Any thousand separators (valid or not) are removed.
    -- * Any sign (and optional following whitespace) is replaced with
-- * Any sign (and optional following whitespace) is replaced with
    --  '-' (if negative) or '' (otherwise).
--  '-' (if negative) or '' (otherwise).
    --  That replaces Unicode minus with '-'.
--  That replaces Unicode minus with '-'.
    -- If successful, the returned info table contains named fields:
-- If successful, the returned info table contains named fields:
    --  value    = a valid number
--  value    = a valid number
    --  altvalue = a valid number, usually same as value but different
--  altvalue = a valid number, usually same as value but different
    --              if fraction used (for hands unit)
--              if fraction used (for hands unit)
    --  singular = true if value is 1 (to use singular form of units)
--  singular = true if value is 1 (to use singular form of units)
    --            = false if value is -1 (like old template)
--            = false if value is -1 (like old template)
    --  clean    = cleaned text with any separators and sign removed
--  clean    = cleaned text with any separators and sign removed
    --              (en digits and '.' decimal mark)
--              (en digits and '.' decimal mark)
    --  show    = text formatted for output
--  show    = text formatted for output
    --              (digits in local language and custom decimal mark)
--              (digits in local language and custom decimal mark)
    -- The resulting show:
-- The resulting show:
    -- * Is for an input value and is spelled if wanted and possible.
-- * Is for an input value and is spelled if wanted and possible.
    -- * Has a rounded value, if wanted.
-- * Has a rounded value, if wanted.
    -- * Has digits grouped where necessary, if wanted.
-- * Has digits grouped where necessary, if wanted.
    -- * If negative, a Unicode minus is used; otherwise the sign is
-- * If negative, a Unicode minus is used; otherwise the sign is
    --  '+' (if the input text used '+'), or is '' (if no sign in input).
--  '+' (if the input text used '+'), or is '' (if no sign in input).
    text = strip(text or '')
text = strip(text or '')
    local clean = to_en(text)
local clean = to_en(text)
    if clean == '' then
if clean == '' then
        return false, { another and 'cvt_no_num2' or 'cvt_no_num' }
return false, { another and 'cvt_no_num2' or 'cvt_no_num' }
    end
end
    local isnegative, propersign = false, ''  -- most common case
local isnegative, propersign = false, ''  -- most common case
    local singular, show, denominator
local singular, show, denominator
    local value = tonumber(clean)
local value = tonumber(clean)
    local altvalue
local altvalue
    if value then
if value then
        local sign = clean:sub(1, 1)
local sign = clean:sub(1, 1)
        if sign == '+' or sign == '-' then
if sign == '+' or sign == '-' then
            propersign = (sign == '+') and '+' or MINUS
propersign = (sign == '+') and '+' or MINUS
            clean = clean:sub(2)
clean = clean:sub(2)
        end
end
        if value < 0 then
if value < 0 then
            isnegative = true
isnegative = true
            value = -value
value = -value
        end
end
    else
else
        local valstr
local valstr
        for _, prefix in ipairs({ '-', MINUS, '&minus;' }) do
for _, prefix in ipairs({ '-', MINUS, '&minus;' }) do
            -- Including '-' means inputs like '- 2' (with space) are accepted as -2.
-- 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'.
-- It also sets isnegative in case input is a fraction like '-2-3/4'.
            local plen = #prefix
local plen = #prefix
            if clean:sub(1, plen) == prefix then
if clean:sub(1, plen) == prefix then
                valstr = clean:sub(plen + 1)
valstr = clean:sub(plen + 1)
                break
break
            end
end
        end
end
        if valstr then
if valstr then
            isnegative = true
isnegative = true
            propersign = MINUS
propersign = MINUS
            clean = valstr
clean = valstr
            value = tonumber(clean)
value = tonumber(clean)
        end
end
        if value == nil then
if value == nil then
            local spelled
local spelled
            if not no_fraction then
if not no_fraction then
                value, altvalue, show, spelled, denominator = extract_fraction(parms, clean, isnegative)
value, altvalue, show, spelled, denominator = extract_fraction(parms, clean, isnegative)
            end
end
            if value == nil then
if value == nil then
                return false, { 'cvt_bad_num', text }
return false, { 'cvt_bad_num', text }
            end
end
            if spelled and value <= 1 then
if spelled and value <= 1 then
                singular = true  -- for example, "one half mile" (singular unit)
singular = true  -- for example, "one half mile" (singular unit)
            else
else
                singular = false -- any numeric fraction (even with value 1) is regarded as plural
singular = false -- any numeric fraction (even with value 1) is regarded as plural
            end
end
        end
end
    end
end
    if not valid_number(value) then  -- for example, "1e310" may overflow
if not valid_number(value) then  -- for example, "1e310" may overflow
        return false, { 'cvt_invalid_num' }
return false, { 'cvt_invalid_num' }
    end
end
    if show == nil then
if show == nil then
        singular = (value == 1 and not isnegative)
singular = (value == 1 and not isnegative)
        local precision = parms.input_precision
local precision = parms.input_precision
        if precision and 0 <= precision and precision <= 8 then
if precision and 0 <= precision and precision <= 8 then
            value = value + 2e-14  -- fudge for some common cases of bad rounding
value = value + 2e-14  -- fudge for some common cases of bad rounding
            local fmt = '%.' .. format('%d', precision) .. 'f'
local fmt = '%.' .. format('%d', precision) .. 'f'
            show = fmt:format(value)
show = fmt:format(value)
        else
else
            show = clean
show = clean
        end
end
        show = propersign .. with_separator(parms, show)
show = propersign .. with_separator(parms, show)
        if parms.opt_spell_in then
if parms.opt_spell_in then
            show = spell_number(parms, propersign .. clean) or show
show = spell_number(parms, propersign .. clean) or show
        end
end
    end
end
    if isnegative and (value ~= 0) then
if isnegative and (value ~= 0) then
        value = -value
value = -value
    end
end
    return true, {
return true, {
        value = value,
value = value,
        altvalue = altvalue or value,
altvalue = altvalue or value,
        singular = singular,
singular = singular,
        clean = clean,
clean = clean,
        show = show,
show = show,
        denominator = denominator,
denominator = denominator,
    }
}
end
end


local function get_number(text)
local function get_number(text)
    -- Return v, f where:
-- Return v, f where:
    --  v = nil (text is not a number)
--  v = nil (text is not a number)
    -- or
-- or
    --  v = value of text (text is a number)
--  v = value of text (text is a number)
    --  f = true if value is an integer
--  f = true if value is an integer
    -- Input can use en digits or digits in local language,
-- Input can use en digits or digits in local language,
    -- but no separators, no Unicode minus, and no fraction.
-- but no separators, no Unicode minus, and no fraction.
    if text then
if text then
        local number = tonumber(to_en(text))
local number = tonumber(to_en(text))
        if number then
if number then
            local integer, fraction = math.modf(number)
local integer, fraction = math.modf(number)
            return number, (fraction == 0)
return number, (fraction == 0)
        end
end
    end
end
end
end


local function preunits(count, preunit1, preunit2)
local function preunits(count, preunit1, preunit2)
    -- If count is 1:
-- If count is 1:
    --    ignore preunit2
--    ignore preunit2
    --    return p1
--    return p1
    -- else:
-- else:
    --    preunit1 is used for preunit2 if the latter is empty
--    preunit1 is used for preunit2 if the latter is empty
    --    return p1, p2
--    return p1, p2
    -- where:
-- where:
    --    p1 is text to insert before the input unit
--    p1 is text to insert before the input unit
    --    p2 is text to insert before the output unit
--    p2 is text to insert before the output unit
    --    p1 or p2 may be nil to mean "no preunit"
--    p1 or p2 may be nil to mean "no preunit"
    -- Using '+ ' gives output like "5+ feet" (no preceding space).
-- Using '+ ' gives output like "5+ feet" (no preceding space).
    local function withspace(text, i)
local function withspace(text, i)
        -- Insert space at beginning if i == 1, or at end if i == -1.
-- Insert space at beginning if i == 1, or at end if i == -1.
        -- However, no space is inserted if there is a space or '&nbsp;'
-- However, no space is inserted if there is a space or '&nbsp;'
        -- or '-' at that position ('-' is for adjectival text).
-- or '-' at that position ('-' is for adjectival text).
        local current = text:sub(i, i)
local current = text:sub(i, i)
        if current == ' ' or current == '-' then
if current == ' ' or current == '-' then
            return text
return text
        end
end
        if i == 1 then
if i == 1 then
            current = text:sub(1, 6)
current = text:sub(1, 6)
        else
else
            current = text:sub(-6, -1)
current = text:sub(-6, -1)
        end
end
        if current == '&nbsp;' then
if current == '&nbsp;' then
            return text
return text
        end
end
        if i == 1 then
if i == 1 then
            return ' ' .. text
return ' ' .. text
        end
end
        return text .. ' '
return text .. ' '
    end
end
    preunit1 = preunit1 or ''
preunit1 = preunit1 or ''
    local trim1 = strip(preunit1)
local trim1 = strip(preunit1)
    if count == 1 then
if count == 1 then
        if trim1 == '' then
if trim1 == '' then
            return nil
return nil
        end
end
        return withspace(withspace(preunit1, 1), -1)
return withspace(withspace(preunit1, 1), -1)
    end
end
    preunit2 = preunit2 or ''
preunit2 = preunit2 or ''
    local trim2 = strip(preunit2)
local trim2 = strip(preunit2)
    if trim1 == '' and trim2 == '' then
if trim1 == '' and trim2 == '' then
        return nil, nil
return nil, nil
    end
end
    if trim1 ~= '+' then
if trim1 ~= '+' then
        preunit1 = withspace(preunit1, 1)
preunit1 = withspace(preunit1, 1)
    end
end
    if trim2 == '&#32;' then  -- trick to make preunit2 empty
if trim2 == '&#32;' then  -- trick to make preunit2 empty
        preunit2 = nil
preunit2 = nil
    elseif trim2 == '' then
elseif trim2 == '' then
        preunit2 = preunit1
preunit2 = preunit1
    elseif trim2 ~= '+' then
elseif trim2 ~= '+' then
        preunit2 = withspace(preunit2, 1)
preunit2 = withspace(preunit2, 1)
    end
end
    return preunit1, preunit2
return preunit1, preunit2
end
end


local function range_text(range, want_name, parms, before, after)
local function range_text(range, want_name, parms, before, after)
    -- Return before .. rtext .. after
-- Return before .. rtext .. after
    -- where rtext is the text that separates two values in a range.
-- where rtext is the text that separates two values in a range.
    local rtext, adj_text, exception
local rtext, adj_text, exception
    if type(range) == 'table' then
if type(range) == 'table' then
        -- Table must specify range text for abbr=off and for abbr=on,
-- Table must specify range text for abbr=off and for abbr=on,
        -- and may specify range text for 'adj=on',
-- and may specify range text for 'adj=on',
        -- and may specify exception = true.
-- and may specify exception = true.
        rtext = range[want_name and 'off' or 'on']
rtext = range[want_name and 'off' or 'on']
        adj_text = range['adj']
adj_text = range['adj']
        exception = range['exception']
exception = range['exception']
    else
else
        rtext = range
rtext = range
    end
end
    if parms.opt_adjectival then
if parms.opt_adjectival then
        if want_name or (exception and parms.abbr_org == 'on') then
if want_name or (exception and parms.abbr_org == 'on') then
            rtext = adj_text or rtext:gsub(' ', '-'):gsub('&nbsp;', '-')
rtext = adj_text or rtext:gsub(' ', '-'):gsub('&nbsp;', '-')
        end
end
    end
end
    if rtext == '–' and after:sub(1, #MINUS) == MINUS then
if rtext == '–' and after:sub(1, #MINUS) == MINUS then
        rtext = '&nbsp;– '
rtext = '&nbsp;– '
    end
end
    return before .. rtext .. after
return before .. rtext .. after
end
end


local function get_composite(parms, iparm, total, in_unit_table)
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}}"
-- Look for a composite input unit. For example, "{{convert|1|yd|2|ft|3|in}}"
    -- would result in a call to this function with
-- would result in a call to this function with
    --  iparm = 3 (parms[iparm] = "2", just after the first unit)
--  iparm = 3 (parms[iparm] = "2", just after the first unit)
    --  total = 1 (number of yards)
--  total = 1 (number of yards)
    --  in_unit_table = (unit table for "yd")
--  in_unit_table = (unit table for "yd")
    -- Return true, iparm, unit where
-- Return true, iparm, unit where
    --  iparm = index just after the composite units (7 in above example)
--  iparm = index just after the composite units (7 in above example)
    --  unit = composite unit table holding all input units,
--  unit = composite unit table holding all input units,
    -- or return true if no composite unit is present in parms,
-- or return true if no composite unit is present in parms,
    -- or return false, t where t is an error message table.
-- or return false, t where t is an error message table.
    local default, subinfo
local default, subinfo
    local composite_units, count = { in_unit_table }, 1
local composite_units, count = { in_unit_table }, 1
    local fixups = {}
local fixups = {}
    local subunit = in_unit_table
local subunit = in_unit_table
    while subunit.subdivs do  -- subdivs is nil or a table of allowed subdivisions
while subunit.subdivs do  -- subdivs is nil or a table of allowed subdivisions
        local subcode = strip(parms[iparm+1])
local subcode = strip(parms[iparm+1])
        local subdiv = subunit.subdivs[subcode]
local subdiv = subunit.subdivs[subcode]
        if not subdiv then
if not subdiv then
            break
break
        end
end
        local success
local success
        success, subunit = lookup(subcode, parms.opt_sp_us, 'no_combination')
success, subunit = lookup(subcode, parms.opt_sp_us, 'no_combination')
        if not success then return false, subunit end  -- should never occur
if not success then return false, subunit end  -- should never occur
        success, subinfo = extract_number(parms, parms[iparm])
success, subinfo = extract_number(parms, parms[iparm])
        if not success then return false, subinfo end
if not success then return false, subinfo end
        iparm = iparm + 2
iparm = iparm + 2
        subunit.inout = 'in'
subunit.inout = 'in'
        subunit.valinfo = { subinfo }
subunit.valinfo = { subinfo }
        -- Recalculate total as a number of subdivisions.
-- Recalculate total as a number of subdivisions.
        -- subdiv[1] = number of subdivisions per previous unit (integer > 1).
-- subdiv[1] = number of subdivisions per previous unit (integer > 1).
        total = total * subdiv[1] + subinfo.value
total = total * subdiv[1] + subinfo.value
        if not default then  -- set by the first subdiv with a default defined
if not default then  -- set by the first subdiv with a default defined
            default = subdiv.default
default = subdiv.default
        end
end
        count = count + 1
count = count + 1
        composite_units[count] = subunit
composite_units[count] = subunit
        if subdiv.unit or subdiv.name then
if subdiv.unit or subdiv.name then
            fixups[count] = { unit = subdiv.unit, name = subdiv.name, valinfo = subunit.valinfo }
fixups[count] = { unit = subdiv.unit, name = subdiv.name, valinfo = subunit.valinfo }
        end
end
    end
end
    if count == 1 then
if count == 1 then
        return true  -- no error and no composite unit
return true  -- no error and no composite unit
    end
end
    for i, fixup in pairs(fixups) do
for i, fixup in pairs(fixups) do
        local unit = fixup.unit
local unit = fixup.unit
        local name = fixup.name
local name = fixup.name
        if not unit or (count > 2 and name) then
if not unit or (count > 2 and name) then
            composite_units[i].fixed_name = name
composite_units[i].fixed_name = name
        else
else
            local success, alternate = lookup(unit, parms.opt_sp_us, 'no_combination')
local success, alternate = lookup(unit, parms.opt_sp_us, 'no_combination')
            if not success then return false, alternate end  -- should never occur
if not success then return false, alternate end  -- should never occur
            alternate.inout = 'in'
alternate.inout = 'in'
            alternate.valinfo = fixup.valinfo
alternate.valinfo = fixup.valinfo
            composite_units[i] = alternate
composite_units[i] = alternate
        end
end
    end
end
    return true, iparm, {
return true, iparm, {
        utype = in_unit_table.utype,
utype = in_unit_table.utype,
        scale = subunit.scale,  -- scale of last (least significant) unit
scale = subunit.scale,  -- scale of last (least significant) unit
        valinfo = { { value = total, clean = subinfo.clean, denominator = subinfo.denominator } },
valinfo = { { value = total, clean = subinfo.clean, denominator = subinfo.denominator } },
        composite = composite_units,
composite = composite_units,
        default = default or in_unit_table.default
default = default or in_unit_table.default
    }
}
end
end


local function translate_parms(parms, kv_pairs)
local function translate_parms(parms, kv_pairs)
    -- Update fields in parms by translating each key:value in kv_pairs to terms
-- 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).
-- used by this module (may involve translating from local language to English).
    -- Also, checks are performed which may display warnings, if enabled.
-- 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.
-- Return true if successful or return false, t where t is an error message table.
    if kv_pairs.adj and kv_pairs.sing then
if kv_pairs.adj and kv_pairs.sing then
        -- For en.wiki (before translation), warn if attempt to use adj and sing
-- For en.wiki (before translation), warn if attempt to use adj and sing
        -- as the latter is a deprecated alias for the former.
-- as the latter is a deprecated alias for the former.
        if kv_pairs.adj ~= kv_pairs.sing and kv_pairs.sing ~= '' then
if kv_pairs.adj ~= kv_pairs.sing and kv_pairs.sing ~= '' then
            add_warning(parms, 'cvt_unknown_option', 'sing=' .. kv_pairs.sing)
add_warning(parms, 'cvt_unknown_option', 'sing=' .. kv_pairs.sing)
        end
end
        kv_pairs.sing = nil
kv_pairs.sing = nil
    end
end
    for loc_name, loc_value in pairs(kv_pairs) do
for loc_name, loc_value in pairs(kv_pairs) do
        local en_name = en_option_name[loc_name]
local en_name = en_option_name[loc_name]
        if en_name then
if en_name then
            local en_value
local en_value
            if en_name == 'sigfig' then
if en_name == 'sigfig' then
                if loc_value == '' then
if loc_value == '' then
                    add_warning(parms, 'cvt_empty_option', loc_name)
add_warning(parms, 'cvt_empty_option', loc_name)
                else
else
                    local number, is_integer = get_number(loc_value)
local number, is_integer = get_number(loc_value)
                    if number and is_integer and number > 0 then
if number and is_integer and number > 0 then
                        en_value = number
en_value = number
                    else
else
                        add_warning(parms, 'cvt_bad_sigfig', loc_value)
add_warning(parms, 'cvt_bad_sigfig', loc_value)
                    end
end
                end
end
            else
else
                en_value = en_option_value[en_name][loc_value]
en_value = en_option_value[en_name][loc_value]
                if en_value == nil then
if en_value == nil then
                    if loc_value == '' then
if loc_value == '' then
                        add_warning(parms, 'cvt_empty_option', loc_name)
add_warning(parms, 'cvt_empty_option', loc_name)
                    else
else
                        -- loc_value can no longer be nil here (at one time, that could occur
-- 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.
-- with aliases like |sing=off|adj=on), but am retaining safety check.
                        local text = loc_value and (loc_name .. '=' .. loc_value) or loc_name
local text = loc_value and (loc_name .. '=' .. loc_value) or loc_name
                        add_warning(parms, 'cvt_unknown_option', text)
add_warning(parms, 'cvt_unknown_option', text)
                    end
end
                elseif en_value == '' then
elseif en_value == '' then
                    en_value = nil  -- an ignored option like adj=off
en_value = nil  -- an ignored option like adj=off
                elseif type(en_value) == 'string' and en_value:sub(1, 4) == 'opt_' then
elseif type(en_value) == 'string' and en_value:sub(1, 4) == 'opt_' then
                    for _, v in ipairs(split(en_value, ',')) do
for _, v in ipairs(split(en_value, ',')) do
                        parms[v] = true
parms[v] = true
                    end
end
                    en_value = nil
en_value = nil
                end
end
            end
end
            parms[en_name] = en_value
parms[en_name] = en_value
        else
else
            add_warning(parms, 'cvt_unknown_option', loc_name .. '=' .. loc_value)
add_warning(parms, 'cvt_unknown_option', loc_name .. '=' .. loc_value)
        end
end
    end
end
    if parms.adj then
if parms.adj then
        if parms.adj:sub(1, 2) == 'ri' then
if parms.adj:sub(1, 2) == 'ri' then
            -- It is known that adj is 'ri1' or 'ri2' or 'ri3', so precision is valid.
-- It is known that adj is 'ri1' or 'ri2' or 'ri3', so precision is valid.
            -- Only en digits are accepted.
-- Only en digits are accepted.
            parms.input_precision = tonumber(parms.adj:sub(-1))
parms.input_precision = tonumber(parms.adj:sub(-1))
            parms.adj = nil
parms.adj = nil
        end
end
    end
end
    if parms.abbr then
if parms.abbr then
        parms.abbr_org = parms.abbr  -- original abbr that was set, before any flip
parms.abbr_org = parms.abbr  -- original abbr that was set, before any flip
    else
else
        parms.abbr = 'out'  -- default is to abbreviate output only (use symbol, not name)
parms.abbr = 'out'  -- default is to abbreviate output only (use symbol, not name)
    end
end
    if parms.opt_flip then
if parms.opt_flip then
        local function swap_in_out(option)
local function swap_in_out(option)
            local value = parms[option]
local value = parms[option]
            if value == 'in' then
if value == 'in' then
                parms[option] = 'out'
parms[option] = 'out'
            elseif value == 'out' then
elseif value == 'out' then
                parms[option] = 'in'
parms[option] = 'in'
            end
end
        end
end
        swap_in_out('abbr')
swap_in_out('abbr')
        swap_in_out('lk')
swap_in_out('lk')
        if parms.opt_spell_in then
if parms.opt_spell_in then
            -- For simplicity, and because it does not appear to be needed,
-- For simplicity, and because it does not appear to be needed,
            -- user cannot set an option to spell the output.
-- user cannot set an option to spell the output.
            parms.opt_spell_in = nil
parms.opt_spell_in = nil
            parms.opt_spell_out = true
parms.opt_spell_out = true
        end
end
    end
end
    if parms.opt_table or parms.opt_tablecen then
if parms.opt_table or parms.opt_tablecen then
        if parms.abbr_org == nil and parms.lk == nil then
if parms.abbr_org == nil and parms.lk == nil then
            parms.opt_values = true
parms.opt_values = true
        end
end
        local align = format('align="%s"', parms.opt_table and 'right' or 'center')
local align = format('align="%s"', parms.opt_table and 'right' or 'center')
        parms.table_joins = { align .. '|', '\n|' .. align .. '|' }
parms.table_joins = { align .. '|', '\n|' .. align .. '|' }
    end
end
    if parms.opt_lang_en then
if parms.opt_lang_en then
        from_en_table = nil
from_en_table = nil
    end
end
    return true
return true
end
end


local function get_values(parms)
local function get_values(parms)
    -- If successful, update parms and return true, v, i where
-- If successful, update parms and return true, v, i where
    --  v = table of input values
--  v = table of input values
    --  i = index to next entry in parms after those processed here
--  i = index to next entry in parms after those processed here
    -- or return false, t where t is an error message table.
-- or return false, t where t is an error message table.
    local valinfo = collection()  -- numbered table of input values
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 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 had_nocomma  -- true if removed "nocomma" kludge from second parameter (like "tonocomma")
    local parm2 = strip(parms[2])
local parm2 = strip(parms[2])
    if parm2 and parm2:sub(-7, -1) == 'nocomma' then
if parm2 and parm2:sub(-7, -1) == 'nocomma' then
        parms[2] = strip(parm2:sub(1, -8))
parms[2] = strip(parm2:sub(1, -8))
        parms.opt_nocomma = true
parms.opt_nocomma = true
        had_nocomma = true
had_nocomma = true
    end
end
    local i = 1
local i = 1
    while true do
while true do
        local success, info = extract_number(parms, parms[i], i > 1)  -- need to set parms.opt_nocomma before calling this
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
if not success then return false, info end
        i = i + 1
i = i + 1
        valinfo:add(info)
valinfo:add(info)
        local next = strip(parms[i])
local next = strip(parms[i])
        local range_item = range_types[next] or range_types[range_aliases[next]]
local range_item = range_types[next] or range_types[range_aliases[next]]
        if not range_item then
if not range_item then
            break
break
        end
end
        i = i + 1
i = i + 1
        range:add(range_item)
range:add(range_item)
        parms.is_range_x = (type(range_item) == 'table') and range_item.is_range_x or nil
parms.is_range_x = (type(range_item) == 'table') and range_item.is_range_x or nil
    end
end
    if range.n > 0 then
if range.n > 0 then
        if range.n > 30 then  -- limit abuse, although 4 is a more likely upper limit
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
return false, { 'cvt_invalid_num' }  -- misleading message but it will do
        end
end
        parms.range = range
parms.range = range
    elseif had_nocomma then
elseif had_nocomma then
        return false, { 'cvt_unknown', parm2 }
return false, { 'cvt_unknown', parm2 }
    end
end
    return true, valinfo, i
return true, valinfo, i
end
end


local function get_parms(pframe)
local function get_parms(pframe)
    -- If successful, return true, parms, unit where
-- If successful, return true, parms, unit where
    --  parms is a table of all arguments passed to the template
--  parms is a table of all arguments passed to the template
    --        converted to named arguments, and
--        converted to named arguments, and
    --  unit is the input unit table;
--  unit is the input unit table;
    -- or return false, t where t is an error message 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
-- 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.
-- unit code as the symbol and name, and with bad_mcode = message code table.
    -- MediaWiki removes leading and trailing whitespace from the values of
-- MediaWiki removes leading and trailing whitespace from the values of
    -- named arguments. However, the values of numbered arguments include any
-- named arguments. However, the values of numbered arguments include any
    -- whitespace entered in the template, and whitespace is used by some
-- whitespace entered in the template, and whitespace is used by some
    -- parameters (example: the numbered parameters associated with "disp=x").
-- parameters (example: the numbered parameters associated with "disp=x").
    local parms = {}  -- arguments passed to template, after translation
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
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
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
if type(k) == 'number' or k == 'test' then  -- parameter "test" is reserved for testing and is not translated
            parms[k] = v
parms[k] = v
        else
else
            kv_pairs[k] = v
kv_pairs[k] = v
        end
end
    end
end
    local success, msg = translate_parms(parms, kv_pairs)
local success, msg = translate_parms(parms, kv_pairs)
    if not success then return false, msg end
if not success then return false, msg end
    local success, valinfo, i = get_values(parms)
local success, valinfo, i = get_values(parms)
    if not success then return false, valinfo end
if not success then return false, valinfo end
    local in_unit = strip(parms[i])
local in_unit = strip(parms[i])
    i = i + 1
i = i + 1
    local success, in_unit_table = lookup(in_unit, parms.opt_sp_us, 'no_combination')
local success, in_unit_table = lookup(in_unit, parms.opt_sp_us, 'no_combination')
    if not success then
if not success then
        if in_unit == nil then
if in_unit == nil then
            in_unit = ''
in_unit = ''
        end
end
        in_unit_table = setmetatable({ symbol = in_unit, name2 = in_unit, utype = "length", scale = 1, bad_mcode = in_unit_table }, unit_mt)
in_unit_table = setmetatable({ symbol = in_unit, name2 = in_unit, utype = "length", scale = 1, bad_mcode = in_unit_table }, unit_mt)
    end
end
    if parms.test == 'msg' then
if parms.test == 'msg' then
        -- Am testing the messages produced when no output unit is specified, and
-- Am testing the messages produced when no output unit is specified, and
        -- the input unit has a missing or invalid default.
-- the input unit has a missing or invalid default.
        -- Set two units for testing that.
-- Set two units for testing that.
        -- LATER: Remove this code.
-- LATER: Remove this code.
        if in_unit == 'chain' then
if in_unit == 'chain' then
            in_unit_table.default = nil  -- no default
in_unit_table.default = nil  -- no default
        elseif in_unit == 'rd' then
elseif in_unit == 'rd' then
            in_unit_table.default  = "ft!X!m"  -- an invalid expression
in_unit_table.default  = "ft!X!m"  -- an invalid expression
        end
end
    end
end
    in_unit_table.valinfo = valinfo
in_unit_table.valinfo = valinfo
    in_unit_table.inout = 'in'  -- this is an input unit
in_unit_table.inout = 'in'  -- this is an input unit
    if not parms.range then
if not parms.range then
        local success, inext, composite_unit = get_composite(parms, i, valinfo[1].value, in_unit_table)
local success, inext, composite_unit = get_composite(parms, i, valinfo[1].value, in_unit_table)
        if not success then return false, inext end
if not success then return false, inext end
        if composite_unit then
if composite_unit then
            in_unit_table = composite_unit
in_unit_table = composite_unit
            i = inext
i = inext
        end
end
    end
end
    if in_unit_table.builtin == 'mach' then
if in_unit_table.builtin == 'mach' then
        -- As with old template, a number following Mach as the input unit is the altitude,
-- 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.
-- 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
-- Could put more code in this function to get any output unit and check for
        -- an altitude following that unit.
-- an altitude following that unit.
        local success, info = extract_number(parms, parms[i], false, true)
local success, info = extract_number(parms, parms[i], false, true)
        if success then
if success then
            i = i + 1
i = i + 1
            in_unit_table.altitude = info.value
in_unit_table.altitude = info.value
        end
end
    end
end
    local next = strip(parms[i])
local next = strip(parms[i])
    i = i + 1
i = i + 1
    local precision, is_bad_precision
local precision, is_bad_precision
    local function set_precision(text)
local function set_precision(text)
        local number, is_integer = get_number(text)
local number, is_integer = get_number(text)
        if number then
if number then
            if is_integer then
if is_integer then
                precision = number
precision = number
            else
else
                precision = text
precision = text
                is_bad_precision = true
is_bad_precision = true
            end
end
            return true  -- text was used for precision, good or bad
return true  -- text was used for precision, good or bad
        end
end
    end
end
    if not set_precision(next) then
if not set_precision(next) then
        parms.out_unit = next
parms.out_unit = next
        if set_precision(strip(parms[i])) then
if set_precision(strip(parms[i])) then
            i = i + 1
i = i + 1
        end
end
    end
end
    if parms.opt_adj_mid then
if parms.opt_adj_mid then
        parms.opt_adjectival = true
parms.opt_adjectival = true
        next = parms[i]
next = parms[i]
        i = i + 1
i = i + 1
        if next then  -- mid-text words
if next then  -- mid-text words
            if next:sub(1, 1) == '-' then
if next:sub(1, 1) == '-' then
                parms.mid = next
parms.mid = next
            else
else
                parms.mid = ' ' .. next
parms.mid = ' ' .. next
            end
end
        end
end
    end
end
    if parms.opt_one_preunit then
if parms.opt_one_preunit then
        parms[parms.opt_flip and 'preunit2' or 'preunit1'] = preunits(1, parms[i])
parms[parms.opt_flip and 'preunit2' or 'preunit1'] = preunits(1, parms[i])
        i = i + 1
i = i + 1
    end
end
    if parms.disp == 'x' then
if parms.disp == 'x' then
        -- Following is reasonably compatible with the old template.
-- Following is reasonably compatible with the old template.
        local first = parms[i] or ''
local first = parms[i] or ''
        local second = parms[i+1] or ''
local second = parms[i+1] or ''
        i = i + 2
i = i + 2
        if strip(first) == '' then  -- user can enter '&#32;' rather than ' ' to avoid the default
if strip(first) == '' then  -- user can enter '&#32;' rather than ' ' to avoid the default
            first = ' [&nbsp;' .. first
first = ' [&nbsp;' .. first
            second = '&nbsp;]' .. second
second = '&nbsp;]' .. second
        end
end
        parms.joins = { first, second }
parms.joins = { first, second }
    elseif parms.opt_two_preunits then
elseif parms.opt_two_preunits then
        local p1, p2 = preunits(2, parms[i], parms[i+1])
local p1, p2 = preunits(2, parms[i], parms[i+1])
        i = i + 2
i = i + 2
        if parms.preunit1 then
if parms.preunit1 then
            -- To simplify documentation, allow unlikely use of adj=pre with disp=preunit
-- 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).
-- (however, an output unit must be specified with adj=pre and with disp=preunit).
            parms.preunit1 = parms.preunit1 .. p1
parms.preunit1 = parms.preunit1 .. p1
            parms.preunit2 = p2
parms.preunit2 = p2
        else
else
            parms.preunit1, parms.preunit2 = p1, p2
parms.preunit1, parms.preunit2 = p1, p2
        end
end
    end
end
    if precision == nil then
if precision == nil then
        if set_precision(strip(parms[i])) then
if set_precision(strip(parms[i])) then
            i = i + 1
i = i + 1
        end
end
    end
end
    if is_bad_precision then
if is_bad_precision then
        add_warning(parms, 'cvt_bad_prec', precision)
add_warning(parms, 'cvt_bad_prec', precision)
    else
else
        parms.precision = precision
parms.precision = precision
    end
end
    return true, parms, in_unit_table
return true, parms, in_unit_table
end
end


local function default_precision(invalue, inclean, denominator, outvalue, in_current, out_current, extra)
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).
-- 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.
-- If denominator is not nil, it is the value of the denominator in inclean.
    -- Code follows procedures used in old template.
-- Code follows procedures used in old template.
    local fudge = 1e-14  -- {{Order of magnitude}} adds this, so we do too
local fudge = 1e-14  -- {{Order of magnitude}} adds this, so we do too
    local prec, minprec, adjust
local prec, minprec, adjust
    local utype = out_current.utype
local utype = out_current.utype
    local subunit_ignore_trailing_zero
local subunit_ignore_trailing_zero
    local subunit_more_precision  -- kludge for "in" used in input like "|2|ft|6|in"
local subunit_more_precision  -- kludge for "in" used in input like "|2|ft|6|in"
    local composite = in_current.composite
local composite = in_current.composite
    if composite then
if composite then
        subunit_ignore_trailing_zero = true  -- input "|2|st|10|lb" has precision 0, not -1
subunit_ignore_trailing_zero = true  -- input "|2|st|10|lb" has precision 0, not -1
        if composite[#composite].exception == 'subunit_more_precision' then
if composite[#composite].exception == 'subunit_more_precision' then
            subunit_more_precision = true  -- do not use standard precision with input like "|2|ft|6|in"
subunit_more_precision = true  -- do not use standard precision with input like "|2|ft|6|in"
        end
end
    end
end
    if denominator and denominator > 0 then
if denominator and denominator > 0 then
        prec = math.max(log10(denominator), 1)
prec = math.max(log10(denominator), 1)
    else
else
        -- Count digits after decimal mark, handling cases like '12.345e6'.
-- Count digits after decimal mark, handling cases like '12.345e6'.
        local exponent
local exponent
        local integer, dot, fraction, expstr = inclean:match('^(%d*)(%.?)(%d*)(.*)')
local integer, dot, fraction, expstr = inclean:match('^(%d*)(%.?)(%d*)(.*)')
        local e = expstr:sub(1, 1)
local e = expstr:sub(1, 1)
        if e == 'e' or e == 'E' then
if e == 'e' or e == 'E' then
            exponent = tonumber(expstr:sub(2))
exponent = tonumber(expstr:sub(2))
        end
end
        if dot == '' then
if dot == '' then
            prec = subunit_ignore_trailing_zero and 0 or -integer:match('0*$'):len()
prec = subunit_ignore_trailing_zero and 0 or -integer:match('0*$'):len()
        else
else
            prec = #fraction
prec = #fraction
        end
end
        if exponent then
if exponent then
            -- So '1230' and '1.23e3' both give prec = -1, and '0.00123' and '1.23e-3' give 5.
-- So '1230' and '1.23e3' both give prec = -1, and '0.00123' and '1.23e-3' give 5.
            prec = prec - exponent
prec = prec - exponent
        end
end
    end
end
    if in_current.istemperature and out_current.istemperature then
if in_current.istemperature and out_current.istemperature then
        -- Converting between common temperatures (°C, °F, °R, K); not keVT, MK.
-- 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.
-- 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.
-- Also, an input value like -300 C (below absolute zero) gives negative kelvins.
        -- Calculate minimum precision from absolute value.
-- Calculate minimum precision from absolute value.
        adjust = 0
adjust = 0
        local kelvin = abs((invalue - in_current.offset) * in_current.scale)
local kelvin = abs((invalue - in_current.offset) * in_current.scale)
        if kelvin < 1e-8 then  -- assume nonzero due to input or calculation precision problem
if kelvin < 1e-8 then  -- assume nonzero due to input or calculation precision problem
            minprec = 2
minprec = 2
        else
else
            minprec = 2 - floor(log10(kelvin) + fudge)  -- 3 sigfigs in kelvin
minprec = 2 - floor(log10(kelvin) + fudge)  -- 3 sigfigs in kelvin
        end
end
    else
else
        if invalue == 0 or outvalue <= 0 then
if invalue == 0 or outvalue <= 0 then
            -- We are never called with a negative outvalue, but it might be zero.
-- We are never called with a negative outvalue, but it might be zero.
            -- This is special-cased to avoid calculation exceptions.
-- This is special-cased to avoid calculation exceptions.
            return 0
return 0
        end
end
        if out_current.exception == 'integer_more_precision' and floor(invalue) == invalue then
if out_current.exception == 'integer_more_precision' and floor(invalue) == invalue then
            -- With certain output units that sometimes give poor results
-- With certain output units that sometimes give poor results
            -- with default rounding, use more precision when the input
-- with default rounding, use more precision when the input
            -- value is equal to an integer. An example of a poor result
-- value is equal to an integer. An example of a poor result
            -- is when input 50 gives a smaller output than input 49.5.
-- is when input 50 gives a smaller output than input 49.5.
            -- Experiment shows this helps, but it does not eliminate all
-- Experiment shows this helps, but it does not eliminate all
            -- surprises because it is not clear whether "50" should be
-- surprises because it is not clear whether "50" should be
            -- interpreted as "from 45 to 55" or "from 49.5 to 50.5".
-- interpreted as "from 45 to 55" or "from 49.5 to 50.5".
            adjust = -log10(in_current.scale)
adjust = -log10(in_current.scale)
        elseif subunit_more_precision then
elseif subunit_more_precision then
            -- Conversion like "{{convert|6|ft|1|in|cm}}" (where subunit is "in")
-- Conversion like "{{convert|6|ft|1|in|cm}}" (where subunit is "in")
            -- has a non-standard adjust value, to give more output precision.
-- has a non-standard adjust value, to give more output precision.
            adjust = log10(out_current.scale) + 2
adjust = log10(out_current.scale) + 2
        else
else
            adjust = log10(abs(invalue / outvalue))
adjust = log10(abs(invalue / outvalue))
        end
end
        adjust = adjust + log10(2)
adjust = adjust + log10(2)
        -- Ensure that the output has at least two significant figures.
-- Ensure that the output has at least two significant figures.
        minprec = 1 - floor(log10(outvalue) + fudge)
minprec = 1 - floor(log10(outvalue) + fudge)
    end
end
    if extra then
if extra then
        adjust = extra.adjust or adjust
adjust = extra.adjust or adjust
        minprec = extra.minprec or minprec
minprec = extra.minprec or minprec
    end
end
    return math.max(floor(prec + adjust), minprec)
return math.max(floor(prec + adjust), minprec)
end
end


local function convert(invalue, inclean, in_current, out_current)
local function convert(invalue, inclean, in_current, out_current)
    -- Convert given input value from one unit to another.
-- Convert given input value from one unit to another.
    -- Return output_value (a number) if a simple convert, or
-- Return output_value (a number) if a simple convert, or
    -- return f, t where
-- return f, t where
    --  f = true, t = table of information with results, or
--  f = true, t = table of information with results, or
    --  f = false, t = error message table.
--  f = false, t = error message table.
    local inscale = in_current.scale
local inscale = in_current.scale
    local outscale = out_current.scale
local outscale = out_current.scale
    if not in_current.iscomplex and not out_current.iscomplex then
if not in_current.iscomplex and not out_current.iscomplex then
        return invalue * (inscale / outscale)  -- minimize overhead for most common case
return invalue * (inscale / outscale)  -- minimize overhead for most common case
    end
end
    if in_current.invert then
if in_current.invert then
        -- Fuel efficiency (there are no built-ins for this type of unit).
-- Fuel efficiency (there are no built-ins for this type of unit).
        if in_current.invert * out_current.invert < 0 then
if in_current.invert * out_current.invert < 0 then
            return 1 / (invalue * inscale * outscale)
return 1 / (invalue * inscale * outscale)
        end
end
        return invalue * (inscale / outscale)
return invalue * (inscale / outscale)
    elseif in_current.offset then
elseif in_current.offset then
        -- Temperature (there are no built-ins for this type of unit).
-- Temperature (there are no built-ins for this type of unit).
        return (invalue - in_current.offset) * (inscale / outscale) + out_current.offset
return (invalue - in_current.offset) * (inscale / outscale) + out_current.offset
    else
else
        -- Built-in unit.
-- Built-in unit.
        local in_builtin = in_current.builtin
local in_builtin = in_current.builtin
        local out_builtin = out_current.builtin
local out_builtin = out_current.builtin
        if in_builtin and out_builtin then
if in_builtin and out_builtin then
            if in_builtin == out_builtin then
if in_builtin == out_builtin then
                return invalue
return invalue
            end
end
            -- There are no cases (yet) where need to convert from one
-- There are no cases (yet) where need to convert from one
            -- built-in unit to another, so this should never occur.
-- built-in unit to another, so this should never occur.
            return false, { 'cvt_bug_convert' }
return false, { 'cvt_bug_convert' }
        end
end
        if in_builtin == 'mach' or out_builtin == 'mach' then
if in_builtin == 'mach' or out_builtin == 'mach' then
            local adjust
local adjust
            if in_builtin == 'mach' then
if in_builtin == 'mach' then
                inscale = speed_of_sound(in_current.altitude)
inscale = speed_of_sound(in_current.altitude)
                adjust = outscale / 0.1
adjust = outscale / 0.1
            else
else
                outscale = speed_of_sound(out_current.altitude)
outscale = speed_of_sound(out_current.altitude)
                adjust = 0.1 / inscale
adjust = 0.1 / inscale
            end
end
            return true, {
return true, {
                outvalue = invalue * (inscale / outscale),
outvalue = invalue * (inscale / outscale),
                adjust = log10(adjust) + log10(2),
adjust = log10(adjust) + log10(2),
            }
}
        elseif in_builtin == 'hand' then
elseif in_builtin == 'hand' then
            -- 1 hand = 4 inches; 1.2 hands = 6 inches.
-- 1 hand = 4 inches; 1.2 hands = 6 inches.
            -- Fractions of a hand are only defined for the first digit, and
-- 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).
-- the first fractional digit should be a number of inches (1, 2 or 3).
            -- However, this code interprets the entire fraction as the number
-- However, this code interprets the entire fraction as the number
            -- of inches / 10 (so 1.75 inches would be 0.175 hands).
-- 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.
-- A value like 12.3 hands is exactly 12*4 + 3 inches; base default precision on that.
            local integer, fraction = math.modf(invalue)
local integer, fraction = math.modf(invalue)
            local outvalue = (integer + 2.5 * fraction) * (inscale / outscale)
local outvalue = (integer + 2.5 * fraction) * (inscale / outscale)
            local inch_value = 4 * integer + 10 * fraction  -- equivalent number of inches
local inch_value = 4 * integer + 10 * fraction  -- equivalent number of inches
            local fracstr = inclean:match('%.(.*)') or ''
local fracstr = inclean:match('%.(.*)') or ''
            local fmt
local fmt
            if fracstr == '' then
if fracstr == '' then
                fmt = '%.0f'
fmt = '%.0f'
            else
else
                fmt = '%.' .. format('%d', #fracstr - 1) .. 'f'
fmt = '%.' .. format('%d', #fracstr - 1) .. 'f'
            end
end
            return true, {
return true, {
                invalue = inch_value,
invalue = inch_value,
                inclean = format(fmt, inch_value),
inclean = format(fmt, inch_value),
                outvalue = outvalue,
outvalue = outvalue,
                minprec = 0,
minprec = 0,
            }
}
        end
end
    end
end
    return false, { 'cvt_bug_convert' }  -- should never occur
return false, { 'cvt_bug_convert' }  -- should never occur
end
end


local function cvtround(parms, info, in_current, out_current)
local function cvtround(parms, info, in_current, out_current)
    -- Return true, t where t is a table with the conversion results; fields:
-- 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,
--  show = rounded, formatted string with the result of converting value in info,
    --      using the rounding specified in parms.
--      using the rounding specified in parms.
    --  singular = true if result is positive, and (after rounding)
--  singular = true if result is positive, and (after rounding)
    --      is "1", or like "1.00";
--      is "1", or like "1.00";
    --  (and more fields shown below, and a calculated 'absvalue' field).
--  (and more fields shown below, and a calculated 'absvalue' field).
    -- or return true, nil if no value specified;
-- or return true, nil if no value specified;
    -- or return false, t where t is an error message table.
-- or return false, t where t is an error message table.
    -- Input info.clean uses en digits (it has been translated, if necessary).
-- 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.
-- Output show uses en or non-en digits as appropriate, or can be spelled.
    local invalue, inclean
local invalue, inclean
    if info then
if info then
        invalue, inclean = info.value, info.clean
invalue, inclean = info.value, info.clean
        if in_current.builtin == 'hand' then
if in_current.builtin == 'hand' then
            invalue = info.altvalue
invalue = info.altvalue
        end
end
    end
end
    if invalue == nil or invalue == '' then
if invalue == nil or invalue == '' then
        return true, nil
return true, nil
    end
end
    if out_current.builtin == 'hand' then
if out_current.builtin == 'hand' then
        -- Convert to hands, then convert the fractional part to inches.
-- Convert to hands, then convert the fractional part to inches.
        -- Code is not correct when output is spelled, and it ignores any requested
-- Code is not correct when output is spelled, and it ignores any requested
        -- precision if the output uses scientific notation (very large, or very
-- precision if the output uses scientific notation (very large, or very
        -- small). Not worth more complexity as these cases should be very rare.
-- small). Not worth more complexity as these cases should be very rare.
        if parms.abbr_org == nil then
if parms.abbr_org == nil then
            out_current.usename = true  -- default is to show name not symbol
out_current.usename = true  -- default is to show name not symbol
        end
end
        local dummy_unit_table = { scale = out_current.scale }
local dummy_unit_table = { scale = out_current.scale }
        local success, outinfo = cvtround(parms, info, in_current, dummy_unit_table)
local success, outinfo = cvtround(parms, info, in_current, dummy_unit_table)
        if not success then return false, outinfo end
if not success then return false, outinfo end
        local fmt
local fmt
        if outinfo.is_scientific then
if outinfo.is_scientific then
            fmt = '%.1f'
fmt = '%.1f'
        else
else
            local fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or ''  -- outinfo.show is in local language
local fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or ''  -- outinfo.show is in local language
            if fraction == '' then
if fraction == '' then
                if not outinfo.use_default_precision then
if not outinfo.use_default_precision then
                    return true, outinfo
return true, outinfo
                end
end
                fmt = '%.0f'
fmt = '%.0f'
            else
else
                fmt = '%.' .. format('%d', ulen(fraction) - 1) .. 'f'
fmt = '%.' .. format('%d', ulen(fraction) - 1) .. 'f'
            end
end
        end
end
        local hands, inches = math.modf(outinfo.raw_absvalue)
local hands, inches = math.modf(outinfo.raw_absvalue)
        inches = format(fmt, inches * 4)
inches = format(fmt, inches * 4)
        if inches:sub(1, 1) == '4' then
if inches:sub(1, 1) == '4' then
            hands = hands + 1
hands = hands + 1
            inches = '0' .. inches:sub(2)
inches = '0' .. inches:sub(2)
            if tonumber(inches) == 0 then
if tonumber(inches) == 0 then
                inches = '0'
inches = '0'
            end
end
        end
end
        if inches:sub(2, 2) == '.' then
if inches:sub(2, 2) == '.' then
            inches = inches:sub(1, 1) .. inches:sub(3)
inches = inches:sub(1, 1) .. inches:sub(3)
        end
end
        outinfo.show = outinfo.sign .. with_separator(parms, format('%d', hands)) .. numdot .. from_en(inches)
outinfo.show = outinfo.sign .. with_separator(parms, format('%d', hands)) .. numdot .. from_en(inches)
        return true, outinfo
return true, outinfo
    end
end
    local outvalue, extra = convert(invalue, inclean, in_current, out_current)
local outvalue, extra = convert(invalue, inclean, in_current, out_current)
    if extra then
if extra then
        if not outvalue then return false, extra end
if not outvalue then return false, extra end
        invalue = extra.invalue or invalue
invalue = extra.invalue or invalue
        inclean = extra.inclean or inclean
inclean = extra.inclean or inclean
        outvalue = extra.outvalue
outvalue = extra.outvalue
    end
end
    if not valid_number(outvalue) then
if not valid_number(outvalue) then
        return false, { 'cvt_invalid_num' }
return false, { 'cvt_invalid_num' }
    end
end
    local isnegative
local isnegative
    if outvalue < 0 then
if outvalue < 0 then
        isnegative = true
isnegative = true
        outvalue = -outvalue
outvalue = -outvalue
    end
end
    local success, use_default_precision, show, exponent
local success, use_default_precision, show, exponent
    local precision = parms.precision
local precision = parms.precision
    if not precision then
if not precision then
        local sigfig = parms.sigfig
local sigfig = parms.sigfig
        if sigfig then
if sigfig then
            show, exponent = make_sigfig(outvalue, sigfig)
show, exponent = make_sigfig(outvalue, sigfig)
        elseif parms.opt_round5 then
elseif parms.opt_round5 then
            show = format('%.0f', floor((outvalue / 5) + 0.5) * 5)
show = format('%.0f', floor((outvalue / 5) + 0.5) * 5)
        else
else
            use_default_precision = true
use_default_precision = true
            precision = default_precision(invalue, inclean, info.denominator, outvalue, in_current, out_current, extra)
precision = default_precision(invalue, inclean, info.denominator, outvalue, in_current, out_current, extra)
        end
end
    end
end
    if precision then
if precision then
        if precision >= 0 then
if precision >= 0 then
            if precision <= 8 then
if precision <= 8 then
                -- Add a fudge to handle common cases of bad rounding due to inability
-- Add a fudge to handle common cases of bad rounding due to inability
                -- to precisely represent some values. This makes the following work:
-- to precisely represent some values. This makes the following work:
                -- {{convert|-100.1|C|K}} and {{convert|5555000|um|m|2}}.
-- {{convert|-100.1|C|K}} and {{convert|5555000|um|m|2}}.
                -- Old template uses #expr round, which invokes PHP round().
-- Old template uses #expr round, which invokes PHP round().
                -- LATER: Investigate how PHP round() works.
-- LATER: Investigate how PHP round() works.
                outvalue = outvalue + 2e-14
outvalue = outvalue + 2e-14
            end
end
            local fmt = '%.' .. format('%d', precision) .. 'f'
local fmt = '%.' .. format('%d', precision) .. 'f'
            local success
local success
            success, show = pcall(format, fmt, outvalue)
success, show = pcall(format, fmt, outvalue)
            if not success then
if not success then
                return false, { 'cvt_big_prec', tostring(precision) }
return false, { 'cvt_big_prec', tostring(precision) }
            end
end
        else
else
            precision = -precision  -- #digits to zero (in addition to any digits after dot)
precision = -precision  -- #digits to zero (in addition to any digits after dot)
            local shift = 10 ^ precision
local shift = 10 ^ precision
            show = format('%.0f', outvalue/shift)
show = format('%.0f', outvalue/shift)
            if show ~= '0' then
if show ~= '0' then
                exponent = #show + precision
exponent = #show + precision
            end
end
        end
end
    end
end
    local t = format_number(parms, show, exponent, isnegative)
local t = format_number(parms, show, exponent, isnegative)
    -- Set singular using match because on some systems 0.99999999999999999 is 1.0.
-- 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.singular = ((show == '1' or show:match('^1%.0*$') ~= nil) and not isnegative)
    t.raw_absvalue = outvalue  -- absolute value before rounding
t.raw_absvalue = outvalue  -- absolute value before rounding
    t.use_default_precision = use_default_precision
t.use_default_precision = use_default_precision
    return true, setmetatable(t, {
return true, setmetatable(t, {
        __index = function (self, key)
__index = function (self, key)
            if key == 'absvalue' then
if key == 'absvalue' then
                -- Calculate absolute value after rounding, if needed.
-- Calculate absolute value after rounding, if needed.
                local clean, exponent = rawget(self, 'clean'), rawget(self, 'exponent')
local clean, exponent = rawget(self, 'clean'), rawget(self, 'exponent')
                local value = tonumber(clean)  -- absolute value (any negative sign has been ignored)
local value = tonumber(clean)  -- absolute value (any negative sign has been ignored)
                if exponent then
if exponent then
                    value = value * 10^exponent
value = value * 10^exponent
                end
end
                rawset(self, key, value)
rawset(self, key, value)
                return value
return value
            end
end
        end })
end })
end
end


local function evaluate_condition(value, condition)
local function evaluate_condition(value, condition)
    -- Return true or false from applying a conditional expression to value,
-- Return true or false from applying a conditional expression to value,
    -- or throw an error if invalid.
-- or throw an error if invalid.
    -- A very limited set of expressions is supported:
-- A very limited set of expressions is supported:
    --    v < 9
--    v < 9
    --    v * 9 < 9
--    v * 9 < 9
    -- where
-- where
    --    'v' is replaced with value
--    'v' is replaced with value
    --    9 is any number (as defined by Lua tonumber)
--    9 is any number (as defined by Lua tonumber)
    --      only en digits are accepted
--      only en digits are accepted
    --    '<' can also be '<=' or '>' or '>='
--    '<' can also be '<=' or '>' or '>='
    -- In addition, the following form is supported:
-- In addition, the following form is supported:
    --    LHS and RHS
--    LHS and RHS
    -- where
-- where
    --    LHS, RHS = any of above expressions.
--    LHS, RHS = any of above expressions.
    local function compare(value, text)
local function compare(value, text)
        local arithop, factor, compop, limit = text:match('^%s*v%s*([*]?)(.-)([<>]=?)(.*)$')
local arithop, factor, compop, limit = text:match('^%s*v%s*([*]?)(.-)([<>]=?)(.*)$')
        if arithop == nil then
if arithop == nil then
            error('Invalid default expression', 0)
error('Invalid default expression', 0)
        elseif arithop == '*' then
elseif arithop == '*' then
            factor = tonumber(factor)
factor = tonumber(factor)
            if factor == nil then
if factor == nil then
                error('Invalid default expression', 0)
error('Invalid default expression', 0)
            end
end
            value = value * factor
value = value * factor
        end
end
        limit = tonumber(limit)
limit = tonumber(limit)
        if limit == nil then
if limit == nil then
            error('Invalid default expression', 0)
error('Invalid default expression', 0)
        end
end
        if compop == '<' then
if compop == '<' then
            return value < limit
return value < limit
        elseif compop == '<=' then
elseif compop == '<=' then
            return value <= limit
return value <= limit
        elseif compop == '>' then
elseif compop == '>' then
            return value > limit
return value > limit
        elseif compop == '>=' then
elseif compop == '>=' then
            return value >= limit
return value >= limit
        end
end
        error('Invalid default expression', 0)  -- should not occur
error('Invalid default expression', 0)  -- should not occur
    end
end
    local lhs, rhs = condition:match('^(.-%W)and(%W.*)')
local lhs, rhs = condition:match('^(.-%W)and(%W.*)')
    if lhs == nil then
if lhs == nil then
        return compare(value, condition)
return compare(value, condition)
    end
end
    return compare(value, lhs) and compare(value, rhs)
return compare(value, lhs) and compare(value, rhs)
end
end


local function get_default(value, unit_table)
local function get_default(value, unit_table)
    -- Return true, s where s = name of unit's default output unit,
-- Return true, s where s = name of unit's default output unit,
    -- or return false, t where t is an error message table.
-- or return false, t where t is an error message table.
    -- Some units have a default that depends on the input value
-- Some units have a default that depends on the input value
    -- (the first value if a range of values is used).
-- (the first value if a range of values is used).
    -- If '!' is in the default, the first bang-delimited field is an
-- If '!' is in the default, the first bang-delimited field is an
    -- expression that uses 'v' to represent the input value.
-- expression that uses 'v' to represent the input value.
    -- Example: 'v < 120 ! small ! big ! suffix' (suffix is optional)
-- Example: 'v < 120 ! small ! big ! suffix' (suffix is optional)
    -- evaluates 'v < 120' as a boolean with result
-- evaluates 'v < 120' as a boolean with result
    -- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise.
-- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise.
    -- Input must use en digits and '.' decimal mark.
-- Input must use en digits and '.' decimal mark.
    local default = default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default
local default = default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default
    if default == nil then
if default == nil then
        return false, { 'cvt_no_default', unit_table.symbol }
return false, { 'cvt_no_default', unit_table.symbol }
    end
end
    if default:find('!', 1, true) == nil then
if default:find('!', 1, true) == nil then
        return true, default
return true, default
    end
end
    local t = split(default, '!')
local t = split(default, '!')
    if #t == 3 or #t == 4 then
if #t == 3 or #t == 4 then
        local success, result = pcall(evaluate_condition, value, t[1])
local success, result = pcall(evaluate_condition, value, t[1])
        if success then
if success then
            default = result and t[2] or t[3]
default = result and t[2] or t[3]
            if #t == 4 then
if #t == 4 then
                default = default .. t[4]
default = default .. t[4]
            end
end
            return true, default
return true, default
        end
end
    end
end
    return false, { 'cvt_bad_default', unit_table.symbol }
return false, { 'cvt_bad_default', unit_table.symbol }
end
end


Line 2,040: Line 2,040:


local function make_link(link, id, link_key)
local function make_link(link, id, link_key)
    -- Return wikilink "[[link|id]]", possibly abbreviated as in examples:
-- Return wikilink "[[link|id]]", possibly abbreviated as in examples:
    --  [[Mile|mile]]  --> [[mile]]
--  [[Mile|mile]]  --> [[mile]]
    --  [[Mile|miles]] --> [[mile]]s
--  [[Mile|miles]] --> [[mile]]s
    -- However, just id is returned if:
-- However, just id is returned if:
    -- * no link given (so caller does not need to check if a link was defined); or
-- * 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).
-- * 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
-- Linking with a unit uses the unit table as the link key, which fails to detect
    -- overlinking for conversions like (each links "mile" twice):
-- overlinking for conversions like (each links "mile" twice):
    --  {{convert|1|impgal/mi|USgal/mi|lk=on}}
--  {{convert|1|impgal/mi|USgal/mi|lk=on}}
    --  {{convert|1|l/km|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)
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
if link == nil or link == '' or linked_pages[link_key] then
        return id
return id
    end
end
    linked_pages[link_key] = true
linked_pages[link_key] = true
    -- Following only works for language en, but it should be safe on other wikis,
-- Following only works for language en, but it should be safe on other wikis,
    -- and overhead of doing it generally does not seem worthwhile.
-- and overhead of doing it generally does not seem worthwhile.
    local l = link:sub(1, 1):lower() .. link:sub(2)
local l = link:sub(1, 1):lower() .. link:sub(2)
    if link == id or l == id then
if link == id or l == id then
        return '[[' .. id .. ']]'
return '[[' .. id .. ']]'
    elseif link .. 's' == id or l .. 's' == id then
elseif link .. 's' == id or l .. 's' == id then
        return '[[' .. id:sub(1, -2) .. ']]s'
return '[[' .. id:sub(1, -2) .. ']]s'
    else
else
        return '[[' .. link .. '|' .. id .. ']]'
return '[[' .. link .. '|' .. id .. ']]'
    end
end
end
end


local function linked_id(unit_table, key_id, want_link)
local function linked_id(unit_table, key_id, want_link)
    -- Return final unit id (symbol or name), optionally with a wikilink,
-- Return final unit id (symbol or name), optionally with a wikilink,
    -- and update unit_table.sep if required.
-- and update unit_table.sep if required.
    -- key_id is one of: 'symbol', 'sym_us', 'name1', 'name1_us', 'name2', 'name2_us'.
-- 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')
local abbr_on = (key_id == 'symbol' or key_id == 'sym_us')
    if abbr_on and want_link then
if abbr_on and want_link then
        local symlink = rawget(unit_table, 'symlink')
local symlink = rawget(unit_table, 'symlink')
        if symlink then
if symlink then
            return symlink  -- for exceptions that have the linked symbol built-in
return symlink  -- for exceptions that have the linked symbol built-in
        end
end
    end
end
    local multiplier = rawget(unit_table, 'multiplier')
local multiplier = rawget(unit_table, 'multiplier')
    local per = unit_table.per
local per = unit_table.per
    if per then
if per then
        local unit1 = per[1]  -- top unit_table, or nil
local unit1 = per[1]  -- top unit_table, or nil
        local unit2 = per[2]  -- bottom unit_table
local unit2 = per[2]  -- bottom unit_table
        if abbr_on then
if abbr_on then
            if not unit1 then
if not unit1 then
                unit_table.sep = ''  -- no separator in "$2/acre"
unit_table.sep = ''  -- no separator in "$2/acre"
            end
end
            if not want_link then
if not want_link then
                local symbol = unit_table.symbol_raw
local symbol = unit_table.symbol_raw
                if symbol then
if symbol then
                    return symbol  -- for exceptions that have the symbol built-in
return symbol  -- for exceptions that have the symbol built-in
                end
end
            end
end
        end
end
        local key_id2  -- unit2 is always singular
local key_id2  -- unit2 is always singular
        if key_id == 'name2' then
if key_id == 'name2' then
            key_id2 = 'name1'
key_id2 = 'name1'
        elseif key_id == 'name2_us' then
elseif key_id == 'name2_us' then
            key_id2 = 'name1_us'
key_id2 = 'name1_us'
        else
else
            key_id2 = key_id
key_id2 = key_id
        end
end
        local result
local result
        if abbr_on then
if abbr_on then
            result = '/'
result = '/'
        elseif unit1 then
elseif unit1 then
            result = ' ' .. per_word .. ' '
result = ' ' .. per_word .. ' '
        else
else
            result = per_word .. ' '
result = per_word .. ' '
        end
end
        if want_link and unit_table.link then
if want_link and unit_table.link then
            result = (unit1 and unit1[key_id] or '') .. result .. unit2[key_id2]
result = (unit1 and unit1[key_id] or '') .. result .. unit2[key_id2]
            return make_link(unit_table.link, result, unit_table)
return make_link(unit_table.link, result, unit_table)
        end
end
        if unit1 then
if unit1 then
            result = linked_id(unit1, key_id, want_link) .. result
result = linked_id(unit1, key_id, want_link) .. result
        end
end
        return result .. linked_id(unit2, key_id2, want_link)
return result .. linked_id(unit2, key_id2, want_link)
    end
end
    if multiplier then
if multiplier then
        -- A multiplier (like "100" in "100km") forces the unit to be plural.
-- A multiplier (like "100" in "100km") forces the unit to be plural.
        multiplier = from_en(multiplier)
multiplier = from_en(multiplier)
        if abbr_on then
if abbr_on then
            multiplier = multiplier .. '&nbsp;'
multiplier = multiplier .. '&nbsp;'
        else
else
            multiplier = multiplier .. ' '
multiplier = multiplier .. ' '
            if key_id == 'name1' then
if key_id == 'name1' then
                key_id = 'name2'
key_id = 'name2'
            elseif key_id == 'name1_us' then
elseif key_id == 'name1_us' then
                key_id = 'name2_us'
key_id = 'name2_us'
            end
end
        end
end
    else
else
        multiplier = ''
multiplier = ''
    end
end
    local id = unit_table.fixed_name or unit_table[key_id]
local id = unit_table.fixed_name or unit_table[key_id]
    if want_link then
if want_link then
        local link = link_exceptions[unit_table.symbol] or unit_table.link
local link = link_exceptions[unit_table.symbol] or unit_table.link
        if link then
if link then
            local before = ''
local before = ''
            local i = unit_table.customary
local i = unit_table.customary
            if i == 1 and unit_table.sp_us then
if i == 1 and unit_table.sp_us then
                i = 2  -- show "U.S." not "US"
i = 2  -- show "U.S." not "US"
            end
end
            if i == 3 and abbr_on then
if i == 3 and abbr_on then
                i = 4  -- abbreviate "imperial" to "imp"
i = 4  -- abbreviate "imperial" to "imp"
            end
end
            local customary = customary_units[i]
local customary = customary_units[i]
            if customary then
if customary then
                -- LATER: This works for language en only, but it's esoteric so ignore for now.
-- LATER: This works for language en only, but it's esoteric so ignore for now.
                local pertext
local pertext
                if id:sub(1, 1) == '/' then
if id:sub(1, 1) == '/' then
                    -- Want unit "/USgal" to display as "/U.S. gal", not "U.S. /gal".
-- Want unit "/USgal" to display as "/U.S. gal", not "U.S. /gal".
                    pertext = '/'
pertext = '/'
                    id = id:sub(2)
id = id:sub(2)
                elseif id:sub(1, 4) == 'per ' then
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).
-- Similarly want "per U.S. gallon", not "U.S. per gallon" (but in practice this is unlikely to be used).
                    pertext = 'per '
pertext = 'per '
                    id = id:sub(5)
id = id:sub(5)
                else
else
                    pertext = ''
pertext = ''
                end
end
                -- Omit any "US"/"U.S."/"imp"/"imperial" from start of id since that will be inserted.
-- Omit any "US"/"U.S."/"imp"/"imperial" from start of id since that will be inserted.
                local removes = (i < 3) and { 'US&nbsp;', 'US ', 'U.S.&nbsp;', 'U.S. ' } or { 'imp&nbsp;', 'imp ', 'imperial ' }
local removes = (i < 3) and { 'US&nbsp;', 'US ', 'U.S.&nbsp;', 'U.S. ' } or { 'imp&nbsp;', 'imp ', 'imperial ' }
                for _, prefix in ipairs(removes) do
for _, prefix in ipairs(removes) do
                    local plen = #prefix
local plen = #prefix
                    if id:sub(1, plen) == prefix then
if id:sub(1, plen) == prefix then
                        id = id:sub(plen + 1)
id = id:sub(plen + 1)
                        break
break
                    end
end
                end
end
                before = pertext .. make_link(customary.link, customary[1]) .. ' '
before = pertext .. make_link(customary.link, customary[1]) .. ' '
            end
end
            id = before .. make_link(link, id, unit_table)
id = before .. make_link(link, id, unit_table)
        end
end
    end
end
    return multiplier .. id
return multiplier .. id
end
end


local function make_id(parms, which, unit_table)
local function make_id(parms, which, unit_table)
    -- Return id, f where
-- Return id, f where
    --  id = unit name or symbol, possibly modified
--  id = unit name or symbol, possibly modified
    --  f = true if id is a name, or false if id is a symbol
--  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).
-- using 1st or 2nd values (which), and for 'in' or 'out' (unit_table.inout).
    -- Result is '' if no symbol/name is to be used.
-- Result is '' if no symbol/name is to be used.
    -- In addition, set unit_table.sep = ' ' or '&nbsp;' or ''
-- In addition, set unit_table.sep = ' ' or '&nbsp;' or ''
    -- (the separator that caller will normally insert before the id).
-- (the separator that caller will normally insert before the id).
    if parms.opt_values then
if parms.opt_values then
        unit_table.sep = ''
unit_table.sep = ''
        return ''
return ''
    end
end
    local inout = unit_table.inout
local inout = unit_table.inout
    local valinfo = unit_table.valinfo
local valinfo = unit_table.valinfo
    local abbr_org = parms.abbr_org
local abbr_org = parms.abbr_org
    local adjectival = parms.opt_adjectival
local adjectival = parms.opt_adjectival
    local disp = parms.disp
local disp = parms.disp
    local lk = parms.lk
local lk = parms.lk
    local usename = unit_table.usename
local usename = unit_table.usename
    local singular = valinfo[which].singular
local singular = valinfo[which].singular
    if usename then
if usename then
        -- Old template does something like this.
-- Old template does something like this.
        if lk == 'on' or lk == inout then
if lk == 'on' or lk == inout then
            -- A linked unit uses the standard singular.
-- A linked unit uses the standard singular.
        else
else
            -- Set non-standard singular.
-- Set non-standard singular.
            local flipped = parms.opt_flip
local flipped = parms.opt_flip
            if inout == 'in' then
if inout == 'in' then
                if not adjectival and (abbr_org == 'out' or flipped) then
if not adjectival and (abbr_org == 'out' or flipped) then
                    local value = valinfo[which].value
local value = valinfo[which].value
                    singular = (0 < value and value < 1.0001)
singular = (0 < value and value < 1.0001)
                end
end
            else
else
                if (abbr_org == 'on') or
if (abbr_org == 'on') or
                (not flipped and (abbr_org == nil or abbr_org == 'out')) or
(not flipped and (abbr_org == nil or abbr_org == 'out')) or
                (flipped and abbr_org == 'in') then
(flipped and abbr_org == 'in') then
                    singular = (valinfo[which].absvalue < 1.0001 and
singular = (valinfo[which].absvalue < 1.0001 and
                                not valinfo[which].is_scientific)
not valinfo[which].is_scientific)
                end
end
            end
end
        end
end
    end
end
    local want_name
local want_name
    if usename then
if usename then
        want_name = true
want_name = true
    else
else
        if abbr_org == nil then
if abbr_org == nil then
            if disp == 'br' or disp == 'or' or disp == 'slash' then
if disp == 'br' or disp == 'or' or disp == 'slash' then
                want_name = true
want_name = true
            end
end
            if unit_table.usesymbol then
if unit_table.usesymbol then
                want_name = false
want_name = false
            end
end
        end
end
        if want_name == nil then
if want_name == nil then
            local abbr = parms.abbr
local abbr = parms.abbr
            if abbr == 'on' or abbr == inout or (abbr == 'mos' and inout == 'out') then
if abbr == 'on' or abbr == inout or (abbr == 'mos' and inout == 'out') then
                want_name = false
want_name = false
            else
else
                want_name = true
want_name = true
            end
end
        end
end
    end
end
    local key
local key
    if want_name then
if want_name then
        if parms.opt_use_nbsp then
if parms.opt_use_nbsp then
            unit_table.sep = '&nbsp;'
unit_table.sep = '&nbsp;'
        else
else
            unit_table.sep = ' '
unit_table.sep = ' '
        end
end
        if parms.opt_singular then
if parms.opt_singular then
            local value
local value
            if inout == 'in' then
if inout == 'in' then
                value = valinfo[which].value
value = valinfo[which].value
            else
else
                value = valinfo[which].absvalue
value = valinfo[which].absvalue
            end
end
            if value then  -- some unusual units do not always set value field
if value then  -- some unusual units do not always set value field
                value = abs(value)
value = abs(value)
                singular = (0 < value and value < 1.0001)
singular = (0 < value and value < 1.0001)
            end
end
        end
end
        if unit_table.engscale or parms.is_range_x then
if unit_table.engscale or parms.is_range_x then
            -- engscale: so "|1|e3kg" gives "1 thousand kilograms" (plural)
-- 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)
-- is_range_x: so "|0.5|x|0.9|mi" gives "0.5 by 0.9 miles" (plural)
            singular = false
singular = false
        end
end
        key = (adjectival or singular) and 'name1' or 'name2'
key = (adjectival or singular) and 'name1' or 'name2'
        if unit_table.sp_us then
if unit_table.sp_us then
            key = key .. '_us'
key = key .. '_us'
        end
end
    else
else
        unit_table.sep = '&nbsp;'
unit_table.sep = '&nbsp;'
        key = unit_table.sp_us and 'sym_us' or 'symbol'
key = unit_table.sp_us and 'sym_us' or 'symbol'
    end
end
    return linked_id(unit_table, key, lk == 'on' or lk == inout), want_name
return linked_id(unit_table, key, lk == 'on' or lk == inout), want_name
end
end


local function decorate_value(parms, unit_table, which)
local function decorate_value(parms, unit_table, which)
    -- If needed, update unit_table so values will be shown with extra information.
-- If needed, update unit_table so values will be shown with extra information.
    -- For consistency with the old template (but different from fmtpower),
-- For consistency with the old template (but different from fmtpower),
    -- the style to display powers of 10 includes "display:none" to allow some
-- 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".
-- browsers to copy, for example, "10³" as "10^3", rather than as "103".
    local engscale = unit_table.engscale
local engscale = unit_table.engscale
    if engscale then
if engscale then
        local inout = unit_table.inout
local inout = unit_table.inout
        local info = unit_table.valinfo[which]
local info = unit_table.valinfo[which]
        local abbr = parms.abbr
local abbr = parms.abbr
        if abbr == 'on' or abbr == inout then
if abbr == 'on' or abbr == inout then
            info.show = info.show ..
info.show = info.show ..
                '<span style="margin-left:0.2em">×<span style="margin-left:0.1em">' ..
'<span style="margin-left:0.2em">×<span style="margin-left:0.1em">' ..
                from_en('10') ..
from_en('10') ..
                '</span></span><s style="display:none">^</s><sup>' ..
'</span></span><s style="display:none">^</s><sup>' ..
                from_en(tostring(engscale.exponent)) .. '</sup>'
from_en(tostring(engscale.exponent)) .. '</sup>'
        else
else
            local number_id
local number_id
            local lk = parms.lk
local lk = parms.lk
            if lk == 'on' or lk == inout then
if lk == 'on' or lk == inout then
                number_id = make_link(engscale.link, engscale[1])
number_id = make_link(engscale.link, engscale[1])
            else
else
                number_id = engscale[1]
number_id = engscale[1]
            end
end
            -- WP:NUMERAL recommends "&nbsp;" in values like "12 million".
-- WP:NUMERAL recommends "&nbsp;" in values like "12 million".
            info.show = info.show .. (parms.opt_adjectival and '-' or '&nbsp;') .. number_id
info.show = info.show .. (parms.opt_adjectival and '-' or '&nbsp;') .. number_id
        end
end
    end
end
    local prefix = unit_table.vprefix
local prefix = unit_table.vprefix
    if prefix then
if prefix then
        local info = unit_table.valinfo[which]
local info = unit_table.valinfo[which]
        info.show = prefix .. info.show
info.show = prefix .. info.show
    end
end
end
end


local function process_input(parms, in_current)
local function process_input(parms, in_current)
    -- Processing required once per conversion.
-- Processing required once per conversion.
    -- Return block of text to represent input (value/unit).
-- 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
if parms.opt_output_only or parms.opt_output_number_only or parms.opt_output_unit_only then
        parms.joins = { '', '' }
parms.joins = { '', '' }
        return ''
return ''
    end
end
    local first_unit
local first_unit
    local composite = in_current.composite  -- nil or table of units
local composite = in_current.composite  -- nil or table of units
    if composite then
if composite then
        first_unit = composite[1]
first_unit = composite[1]
    else
else
        first_unit = in_current
first_unit = in_current
    end
end
    local id1, want_name = make_id(parms, 1, first_unit)
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 sep = first_unit.sep  -- separator between value and unit, set by make_id
    local preunit = parms.preunit1
local preunit = parms.preunit1
    if preunit then
if preunit then
        sep = ''  -- any separator is included in preunit
sep = ''  -- any separator is included in preunit
    else
else
        preunit = ''
preunit = ''
    end
end
    if parms.opt_input_unit_only then
if parms.opt_input_unit_only then
        parms.joins = { '', '' }
parms.joins = { '', '' }
        if composite then
if composite then
            local parts = { id1 }
local parts = { id1 }
            for i, unit in ipairs(composite) do
for i, unit in ipairs(composite) do
                if i > 1 then
if i > 1 then
                    table.insert(parts, (make_id(parms, 1, unit)))
table.insert(parts, (make_id(parms, 1, unit)))
                end
end
            end
end
            id1 = table.concat(parts, ' ')
id1 = table.concat(parts, ' ')
        end
end
        if want_name and parms.opt_adjectival then
if want_name and parms.opt_adjectival then
            return preunit .. hyphenated(id1)
return preunit .. hyphenated(id1)
        end
end
        return  preunit .. id1
return  preunit .. id1
    end
end
    local abbr = parms.abbr
local abbr = parms.abbr
    local disp = parms.disp
local disp = parms.disp
    if disp == nil then  -- special case for the most common setting
if disp == nil then  -- special case for the most common setting
        parms.joins = disp_joins['b']
parms.joins = disp_joins['b']
    elseif disp ~= 'x' then
elseif disp ~= 'x' then
        -- Old template does this.
-- Old template does this.
        if disp == 'slash' then
if disp == 'slash' then
            if parms.abbr_org == nil then
if parms.abbr_org == nil then
                disp = 'slash-nbsp'
disp = 'slash-nbsp'
            elseif abbr == 'in' or abbr == 'out' then
elseif abbr == 'in' or abbr == 'out' then
                disp = 'slash-sp'
disp = 'slash-sp'
            else
else
                disp = 'slash-nosp'
disp = 'slash-nosp'
            end
end
        elseif disp == 'sqbr' then
elseif disp == 'sqbr' then
            if abbr == 'on' then
if abbr == 'on' then
                disp = 'sqbr-nbsp'
disp = 'sqbr-nbsp'
            else
else
                disp = 'sqbr-sp'
disp = 'sqbr-sp'
            end
end
        end
end
        parms.joins = disp_joins[disp] or disp_joins['b']
parms.joins = disp_joins[disp] or disp_joins['b']
    end
end
    if parms.opt_also_symbol and not composite then
if parms.opt_also_symbol and not composite then
        local join1 = parms.joins[1]
local join1 = parms.joins[1]
        if join1 == ' (' or join1 == ' [' then
if join1 == ' (' or join1 == ' [' then
            parms.joins = { join1 .. first_unit[first_unit.sp_us and 'sym_us' or 'symbol'] .. ', ', parms.joins[2] }
parms.joins = { join1 .. first_unit[first_unit.sp_us and 'sym_us' or 'symbol'] .. ', ', parms.joins[2] }
        end
end
    end
end
    if in_current.builtin == 'mach' then
if in_current.builtin == 'mach' then
        local prefix = id1 .. '&nbsp;'
local prefix = id1 .. '&nbsp;'
        local range = parms.range
local range = parms.range
        local valinfo = first_unit.valinfo
local valinfo = first_unit.valinfo
        local result = prefix .. valinfo[1].show
local result = prefix .. valinfo[1].show
        if range then
if range then
            -- For simplicity and because more not needed, handle one range item only.
-- For simplicity and because more not needed, handle one range item only.
            local prefix2 = make_id(parms, 2, first_unit) .. '&nbsp;'
local prefix2 = make_id(parms, 2, first_unit) .. '&nbsp;'
            result = range_text(range[1], want_name, parms, result, prefix2 .. valinfo[2].show)
result = range_text(range[1], want_name, parms, result, prefix2 .. valinfo[2].show)
        end
end
        return preunit .. result
return preunit .. result
    end
end
    if composite then
if composite then
        -- Simplify: assume there is no range, and no decoration.
-- Simplify: assume there is no range, and no decoration.
        local mid = ''
local mid = ''
        local sep1 = '&nbsp;'
local sep1 = '&nbsp;'
        local sep2 = ' '
local sep2 = ' '
        if parms.opt_adjectival then
if parms.opt_adjectival then
            if not parms.opt_flip then
if not parms.opt_flip then
                mid = parms.mid or ''
mid = parms.mid or ''
            end
end
            if want_name then
if want_name then
                sep1 = '-'
sep1 = '-'
                sep2 = '-'
sep2 = '-'
            end
end
        end
end
        local parts = { first_unit.valinfo[1].show .. sep1 .. id1 }
local parts = { first_unit.valinfo[1].show .. sep1 .. id1 }
        for i, unit in ipairs(composite) do
for i, unit in ipairs(composite) do
            if i > 1 then
if i > 1 then
                table.insert(parts, unit.valinfo[1].show .. sep1 .. (make_id(parms, 1, unit)))
table.insert(parts, unit.valinfo[1].show .. sep1 .. (make_id(parms, 1, unit)))
            end
end
        end
end
        return table.concat(parts, sep2) .. mid
return table.concat(parts, sep2) .. mid
    end
end
    local result, mos
local result, mos
    local range = parms.range
local range = parms.range
    if range then
if range then
        mos = (abbr == 'mos')
mos = (abbr == 'mos')
        if not (mos or (parms.is_range_x and not want_name)) then
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
linked_pages[first_unit] = nil  -- so the second and only id will be linked, if wanted
        end
end
    end
end
    local id = (range == nil) and id1 or make_id(parms, 2, first_unit)
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')
local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, 'in')
    if mos and was_hyphenated then
if mos and was_hyphenated then
        mos = false  -- suppress repeat of unit in a range
mos = false  -- suppress repeat of unit in a range
        if linked_pages[first_unit] then
if linked_pages[first_unit] then
            linked_pages[first_unit] = nil
linked_pages[first_unit] = nil
            id = make_id(parms, 2, first_unit)
id = make_id(parms, 2, first_unit)
            extra = hyphenated_maybe(parms, want_name, sep, id, 'in')
extra = hyphenated_maybe(parms, want_name, sep, id, 'in')
        end
end
    end
end
    local valinfo = first_unit.valinfo
local valinfo = first_unit.valinfo
    if range then
if range then
        if range.n == 1 then
if range.n == 1 then
            -- Like {{convert|1|x|2|ft}} (one range item; two values).
-- Like {{convert|1|x|2|ft}} (one range item; two values).
            -- Do what old template did.
-- Do what old template did.
            local sep1 = first_unit.sep
local sep1 = first_unit.sep
            if mos then
if mos then
                decorate_value(parms, in_current, 1)
decorate_value(parms, in_current, 1)
                decorate_value(parms, in_current, 2)
decorate_value(parms, in_current, 2)
                result = valinfo[1].show .. sep1 .. id1
result = valinfo[1].show .. sep1 .. id1
            elseif parms.is_range_x and not want_name then
elseif parms.is_range_x and not want_name then
                if abbr == 'in' or abbr == 'on' then
if abbr == 'in' or abbr == 'on' then
                    decorate_value(parms, in_current, 1)
decorate_value(parms, in_current, 1)
                end
end
                decorate_value(parms, in_current, 2)
decorate_value(parms, in_current, 2)
                result = valinfo[1].show .. sep1 .. id1
result = valinfo[1].show .. sep1 .. id1
            else
else
                if abbr == 'in' or abbr == 'on' then
if abbr == 'in' or abbr == 'on' then
                    decorate_value(parms, in_current, 1)
decorate_value(parms, in_current, 1)
                end
end
                decorate_value(parms, in_current, 2)
decorate_value(parms, in_current, 2)
                result = valinfo[1].show
result = valinfo[1].show
            end
end
            result = range_text(range[1], want_name, parms, result, valinfo[2].show)
result = range_text(range[1], want_name, parms, result, valinfo[2].show)
        else
else
            -- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify.
-- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify.
            decorate_value(parms, in_current, 1)
decorate_value(parms, in_current, 1)
            result = valinfo[1].show
result = valinfo[1].show
            for i = 1, range.n do
for i = 1, range.n do
                decorate_value(parms, in_current, i+1)
decorate_value(parms, in_current, i+1)
                result = range_text(range[i], want_name, parms, result, valinfo[i+1].show)
result = range_text(range[i], want_name, parms, result, valinfo[i+1].show)
            end
end
        end
end
    else
else
        decorate_value(parms, first_unit, 1)
decorate_value(parms, first_unit, 1)
        result = valinfo[1].show
result = valinfo[1].show
    end
end
    return result .. preunit .. extra
return result .. preunit .. extra
end
end


local function process_one_output(parms, out_current)
local function process_one_output(parms, out_current)
    -- Processing required for each output unit.
-- Processing required for each output unit.
    -- Return block of text to represent output (value/unit).
-- Return block of text to represent output (value/unit).
    local id1, want_name = make_id(parms, 1, out_current)
local id1, want_name = make_id(parms, 1, out_current)
    local sep = out_current.sep  -- set by make_id
local sep = out_current.sep  -- set by make_id
    local preunit = parms.preunit2
local preunit = parms.preunit2
    if preunit then
if preunit then
        sep = ''  -- any separator is included in preunit
sep = ''  -- any separator is included in preunit
    else
else
        preunit = ''
preunit = ''
    end
end
    if parms.opt_output_unit_only then
if parms.opt_output_unit_only then
        if want_name and parms.opt_adjectival then
if want_name and parms.opt_adjectival then
            return preunit .. hyphenated(id1)
return preunit .. hyphenated(id1)
        end
end
        return preunit .. id1
return preunit .. id1
    end
end
    if out_current.builtin == 'mach' then
if out_current.builtin == 'mach' then
        local prefix = id1 .. '&nbsp;'
local prefix = id1 .. '&nbsp;'
        local range = parms.range
local range = parms.range
        local valinfo = out_current.valinfo
local valinfo = out_current.valinfo
        local result = prefix .. valinfo[1].show
local result = prefix .. valinfo[1].show
        if range then
if range then
            -- For simplicity and because more not needed, handle one range item only.
-- 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)
result = range_text(range[1], want_name, parms, result, prefix .. valinfo[2].show)
        end
end
        return preunit .. result
return preunit .. result
    end
end
    local result
local result
    local range = parms.range
local range = parms.range
    if range then
if range then
        if not (parms.is_range_x and not want_name) 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
linked_pages[out_current] = nil  -- so the second and only id will be linked, if wanted
        end
end
    end
end
    local id = (range == nil) and id1 or make_id(parms, 2, out_current)
local id = (range == nil) and id1 or make_id(parms, 2, out_current)
    local extra = hyphenated_maybe(parms, want_name, sep, id, 'out')
local extra = hyphenated_maybe(parms, want_name, sep, id, 'out')
    local valinfo = out_current.valinfo
local valinfo = out_current.valinfo
    if range then
if range then
        if range.n == 1 then
if range.n == 1 then
            local sep1 = out_current.sep
local sep1 = out_current.sep
            local abbr = parms.abbr
local abbr = parms.abbr
            if parms.is_range_x and not want_name then
if parms.is_range_x and not want_name then
                if abbr == 'out' or abbr == 'on' then
if abbr == 'out' or abbr == 'on' then
                    decorate_value(parms, out_current, 1)
decorate_value(parms, out_current, 1)
                end
end
                decorate_value(parms, out_current, 2)
decorate_value(parms, out_current, 2)
                result = valinfo[1].show .. sep1 .. id1
result = valinfo[1].show .. sep1 .. id1
            else
else
                if abbr == 'out' or abbr == 'on' then
if abbr == 'out' or abbr == 'on' then
                    decorate_value(parms, out_current, 1)
decorate_value(parms, out_current, 1)
                end
end
                decorate_value(parms, out_current, 2)
decorate_value(parms, out_current, 2)
                result = valinfo[1].show
result = valinfo[1].show
            end
end
            result = range_text(range[1], want_name, parms, result, valinfo[2].show)
result = range_text(range[1], want_name, parms, result, valinfo[2].show)
        else
else
            -- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify.
-- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify.
            decorate_value(parms, out_current, 1)
decorate_value(parms, out_current, 1)
            result = valinfo[1].show
result = valinfo[1].show
            for i = 1, range.n do
for i = 1, range.n do
                decorate_value(parms, out_current, i+1)
decorate_value(parms, out_current, i+1)
                result = range_text(range[i], want_name, parms, result, valinfo[i+1].show)
result = range_text(range[i], want_name, parms, result, valinfo[i+1].show)
            end
end
        end
end
    else
else
        decorate_value(parms, out_current, 1)
decorate_value(parms, out_current, 1)
        result = valinfo[1].show
result = valinfo[1].show
    end
end
    if parms.opt_output_number_only then
if parms.opt_output_number_only then
        return result
return result
    end
end
    return result .. preunit .. extra
return result .. preunit .. extra
end
end


local function make_output_single(parms, in_unit_table, out_unit_table)
local function make_output_single(parms, in_unit_table, out_unit_table)
    -- Return true, item where item = wikitext of the conversion result
-- Return true, item where item = wikitext of the conversion result
    -- for a single output (which is not a combination or a multiple);
-- for a single output (which is not a combination or a multiple);
    -- or return false, t where t is an error message table.
-- or return false, t where t is an error message table.
    out_unit_table.valinfo = collection()
out_unit_table.valinfo = collection()
    local range = parms.range
local range = parms.range
    for i = 1, (range and (range.n + 1) or 1) do
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)
local success, info = cvtround(parms, in_unit_table.valinfo[i], in_unit_table, out_unit_table)
        if not success then return false, info end
if not success then return false, info end
        out_unit_table.valinfo:add(info)
out_unit_table.valinfo:add(info)
    end
end
    return true, process_one_output(parms, out_unit_table)
return true, process_one_output(parms, out_unit_table)
end
end


local function make_output_multiple(parms, in_unit_table, out_unit_table)
local function make_output_multiple(parms, in_unit_table, out_unit_table)
    -- Return true, item where item = wikitext of the conversion result
-- Return true, item where item = wikitext of the conversion result
    -- for an output which is a multiple (like 'ftin');
-- for an output which is a multiple (like 'ftin');
    -- or return false, t where t is an error message table.
-- 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 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 combos = out_unit_table.combination  -- table of unit tables (will not be nil)
    local abbr = parms.abbr
local abbr = parms.abbr
    local abbr_org = parms.abbr_org
local abbr_org = parms.abbr_org
    local disp = parms.disp
local disp = parms.disp
    local want_name = (abbr_org == nil and (disp == 'or' or disp == 'slash')) or
local want_name = (abbr_org == nil and (disp == 'or' or disp == 'slash')) or
                      not (abbr == 'on' or abbr == 'out' or abbr == 'mos')
not (abbr == 'on' or abbr == 'out' or abbr == 'mos')
    local want_link = (parms.lk == 'on' or parms.lk == 'out')
local want_link = (parms.lk == 'on' or parms.lk == 'out')
    local mid = ''
local mid = ''
    local sep1 = '&nbsp;'
local sep1 = '&nbsp;'
    local sep2 = ' '
local sep2 = ' '
    if parms.opt_adjectival then
if parms.opt_adjectival then
        if parms.opt_flip then
if parms.opt_flip then
            mid = parms.mid or ''
mid = parms.mid or ''
        end
end
        if want_name then
if want_name then
            sep1 = '-'
sep1 = '-'
            sep2 = '-'
sep2 = '-'
        end
end
    end
end
    local function make_result(info)
local function make_result(info)
        local fmt, outvalue, sign
local fmt, outvalue, sign
        local results = {}
local results = {}
        for i = 1, #combos do
for i = 1, #combos do
            local thisvalue, strforce
local thisvalue, strforce
            local out_current = combos[i]
local out_current = combos[i]
            out_current.inout = 'out'
out_current.inout = 'out'
            local scale = multiple[i]
local scale = multiple[i]
            if i == 1 then  -- least significant unit ('in' from 'ftin')
if i == 1 then  -- least significant unit ('in' from 'ftin')
                local fraction
local fraction
                local success, outinfo = cvtround(parms, info, in_unit_table, out_current)
local success, outinfo = cvtround(parms, info, in_unit_table, out_current)
                if not success then return false, outinfo end
if not success then return false, outinfo end
                sign = outinfo.sign
sign = outinfo.sign
                if outinfo.is_scientific then
if outinfo.is_scientific then
                    strforce = outinfo.show
strforce = outinfo.show
                    fraction = ''
fraction = ''
                else
else
                    fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or ''  -- outinfo.show is in local language
fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or ''  -- outinfo.show is in local language
                end
end
                fmt = '%.' .. ulen(fraction) .. 'f'  -- to reproduce precision
fmt = '%.' .. ulen(fraction) .. 'f'  -- to reproduce precision
                if fraction == '' then
if fraction == '' then
                    outvalue = floor(outinfo.raw_absvalue + 0.5)  -- keep all integer digits of least significant unit
outvalue = floor(outinfo.raw_absvalue + 0.5)  -- keep all integer digits of least significant unit
                else
else
                    outvalue = outinfo.absvalue
outvalue = outinfo.absvalue
                end
end
            end
end
            if scale then
if scale then
                outvalue, thisvalue = floor(outvalue / scale), outvalue % scale
outvalue, thisvalue = floor(outvalue / scale), outvalue % scale
            else
else
                thisvalue = outvalue
thisvalue = outvalue
            end
end
            local id
local id
            if want_name then
if want_name then
                id = out_current[(thisvalue == 1) and 'name1' or 'name2']
id = out_current[(thisvalue == 1) and 'name1' or 'name2']
            else
else
                id = out_current['symbol']
id = out_current['symbol']
            end
end
            if want_link then
if want_link then
                local link = out_current.link
local link = out_current.link
                if link then
if link then
                    id = make_link(link, id, out_current)
id = make_link(link, id, out_current)
                end
end
            end
end
            local strval
local strval
            if strforce and outvalue == 0 then
if strforce and outvalue == 0 then
                sign = ''  -- any sign is in strforce
sign = ''  -- any sign is in strforce
                strval = strforce  -- show small values in scientific notation; will only use least significant unit
strval = strforce  -- show small values in scientific notation; will only use least significant unit
            else
else
                strval = (thisvalue == 0) and from_en('0') or with_separator(parms, format(fmt, thisvalue))
strval = (thisvalue == 0) and from_en('0') or with_separator(parms, format(fmt, thisvalue))
            end
end
            table.insert(results, strval .. sep1 .. id)
table.insert(results, strval .. sep1 .. id)
            if outvalue == 0 then
if outvalue == 0 then
                break
break
            end
end
            fmt = '%.0f'  -- only least significant unit can have a fraction
fmt = '%.0f'  -- only least significant unit can have a fraction
        end
end
        local reversed, count = {}, #results
local reversed, count = {}, #results
        for i = 1, count do
for i = 1, count do
            reversed[i] = results[count + 1 - i]
reversed[i] = results[count + 1 - i]
        end
end
        return true, sign .. table.concat(reversed, sep2)
return true, sign .. table.concat(reversed, sep2)
    end
end
    local valinfo = in_unit_table.valinfo
local valinfo = in_unit_table.valinfo
    local success, result = make_result(valinfo[1])
local success, result = make_result(valinfo[1])
    if not success then return false, result end
if not success then return false, result end
    local range = parms.range
local range = parms.range
    if range then
if range then
        for i = 1, range.n do
for i = 1, range.n do
            local success, result2 = make_result(valinfo[i+1])
local success, result2 = make_result(valinfo[i+1])
            if not success then return false, result2 end
if not success then return false, result2 end
            result = range_text(range[i], want_name, parms, result, result2)
result = range_text(range[i], want_name, parms, result, result2)
        end
end
    end
end
    return true, result .. mid
return true, result .. mid
end
end


local function process(parms, in_unit_table)
local function process(parms, in_unit_table)
    -- Return true, s where s = final wikitext result,
-- Return true, s where s = final wikitext result,
    -- or return false, t where t is an error message table.
-- or return false, t where t is an error message table.
    linked_pages = {}
linked_pages = {}
    local success, bad_output, out_unit_table
local success, bad_output, out_unit_table
    local bad_input_mcode = in_unit_table.bad_mcode  -- nil if input unit is valid
local bad_input_mcode = in_unit_table.bad_mcode  -- nil if input unit is valid
    local invalue1 = in_unit_table.valinfo[1].value
local invalue1 = in_unit_table.valinfo[1].value
    local out_unit = parms.out_unit
local out_unit = parms.out_unit
    if out_unit == nil or out_unit == '' then
if out_unit == nil or out_unit == '' then
        if bad_input_mcode then
if bad_input_mcode then
            bad_output = ''
bad_output = ''
        else
else
            success, out_unit = get_default(invalue1, in_unit_table)
success, out_unit = get_default(invalue1, in_unit_table)
            if not success then
if not success then
                bad_output = out_unit
bad_output = out_unit
            end
end
        end
end
    end
end
    if not bad_output then
if not bad_output then
        success, out_unit_table = lookup(out_unit, parms.opt_sp_us, 'any_combination')
success, out_unit_table = lookup(out_unit, parms.opt_sp_us, 'any_combination')
        if success then
if success then
            local mismatch = check_mismatch(in_unit_table, out_unit_table)
local mismatch = check_mismatch(in_unit_table, out_unit_table)
            if mismatch then
if mismatch then
                bad_output = mismatch
bad_output = mismatch
            end
end
        else
else
            bad_output = out_unit_table
bad_output = out_unit_table
        end
end
    end
end
    local flipped = parms.opt_flip and not bad_input_mcode
local flipped = parms.opt_flip and not bad_input_mcode
    local parts = {}
local parts = {}
    for part = 1, 2 do
for part = 1, 2 do
        -- The LHS (parts[1]) is normally the input, but is the output if flipped.
-- The LHS (parts[1]) is normally the input, but is the output if flipped.
        -- Process LHS first so it will be linked, if wanted.
-- Process LHS first so it will be linked, if wanted.
        -- Linking to the same item is suppressed in the RHS to avoid overlinking.
-- 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
if (part == 1 and not flipped) or (part == 2 and flipped) then
            parts[part] = process_input(parms, in_unit_table)
parts[part] = process_input(parms, in_unit_table)
        elseif bad_output then
elseif bad_output then
            if bad_output ~= '' then
if bad_output ~= '' then
                parts[part] = message(bad_output)
parts[part] = message(bad_output)
            end
end
        else
else
            local outputs = {}
local outputs = {}
            local combos  -- nil (for 'ft' or 'ftin'), or table of unit tables (for 'm ft')
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')
if out_unit_table.multiple == nil then  -- nil ('ft' or 'm ft'), or table of factors ('ftin')
                combos = out_unit_table.combination
combos = out_unit_table.combination
            end
end
            local imax = combos and #combos or 1  -- 1 (single unit) or number of unit tables
local imax = combos and #combos or 1  -- 1 (single unit) or number of unit tables
            for i = 1, imax do
for i = 1, imax do
                local success, item
local success, item
                local out_current = combos and combos[i] or out_unit_table
local out_current = combos and combos[i] or out_unit_table
                out_current.inout = 'out'
out_current.inout = 'out'
                if out_current.multiple == nil then
if out_current.multiple == nil then
                    success, item = make_output_single(parms, in_unit_table, out_current)
success, item = make_output_single(parms, in_unit_table, out_current)
                else
else
                    success, item = make_output_multiple(parms, in_unit_table, out_current)
success, item = make_output_multiple(parms, in_unit_table, out_current)
                end
end
                if not success then return false, item end
if not success then return false, item end
                table.insert(outputs, item)
table.insert(outputs, item)
            end
end
            parts[part] = parms.opt_input_unit_only and '' or table.concat(outputs, '; ')
parts[part] = parms.opt_input_unit_only and '' or table.concat(outputs, '; ')
        end
end
    end
end
    if parms.opt_sortable then
if parms.opt_sortable then
        parts[1] = ntsh(invalue1, parms.opt_sortable_debug) .. parts[1]
parts[1] = ntsh(invalue1, parms.opt_sortable_debug) .. parts[1]
    end
end
    local wikitext
local wikitext
    if bad_input_mcode then
if bad_input_mcode then
        wikitext = parts[1] .. message(bad_input_mcode)
wikitext = parts[1] .. message(bad_input_mcode)
    elseif parms.table_joins then
elseif parms.table_joins then
        wikitext = parms.table_joins[1] .. parts[1] .. parms.table_joins[2] .. parts[2]
wikitext = parms.table_joins[1] .. parts[1] .. parms.table_joins[2] .. parts[2]
    else
else
        wikitext = parts[1] .. parms.joins[1] .. parts[2] .. parms.joins[2]
wikitext = parts[1] .. parms.joins[1] .. parts[2] .. parms.joins[2]
    end
end
    if parms.warnings and not bad_input_mcode then
if parms.warnings and not bad_input_mcode then
        wikitext = wikitext .. parms.warnings
wikitext = wikitext .. parms.warnings
    end
end
    return true, wikitext
return true, wikitext
end
end


local function main_convert(frame)
local function main_convert(frame)
    set_config(frame)
set_config(frame)
    local result
local result
    local success, parms, in_unit_table = get_parms(frame:getParent())
local success, parms, in_unit_table = get_parms(frame:getParent())
    if success then
if success then
        success, result = process(parms, in_unit_table)
success, result = process(parms, in_unit_table)
    else
else
        result = parms
result = parms
    end
end
    if success then
if success then
        return result
return result
    end
end
    return message(result)
return message(result)
end
end


return { convert = main_convert }
return { convert = main_convert }

Revision as of 03:38, 6 November 2013

Documentation for this module may be created at Module:Convert/doc

-- 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('"', '&quot;')
		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 "&nbsp;" in combinations, so
	-- replace underscore, "&nbsp;", 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('&nbsp;', ' '):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 combo like "MT+LT" (convert output to MT and LT).
	local combo = collection()
	for item in string.gmatch(unitcode .. '+', '%s*(.-)%s*%+') do
		if item ~= '' then
			combo:add(item)
		end
	end
	if combo.n > 1 then
		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
	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.
		local failed = fails[unitcode]
		if not failed 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&nbsp;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;">&nbsp;</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, '&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 '&nbsp;'
		-- 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 == '&nbsp;' 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 == '&#32;' 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('&nbsp;', '-')
		end
	end
	if rtext == '–' and after:sub(1, #MINUS) == MINUS then
		rtext = '&nbsp;– '
	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 '&#32;' rather than ' ' to avoid the default
			first = ' [&nbsp;' .. first
			second = '&nbsp;]' .. 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 .. '&nbsp;'
		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&nbsp;', 'US ', 'U.S.&nbsp;', 'U.S. ' } or { 'imp&nbsp;', '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 '&nbsp;' 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 = '&nbsp;'
		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 = '&nbsp;'
		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 "&nbsp;" in values like "12 million".
			info.show = info.show .. (parms.opt_adjectival and '-' or '&nbsp;') .. 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 .. '&nbsp;'
		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) .. '&nbsp;'
			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 = '&nbsp;'
		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 .. '&nbsp;'
		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 = '&nbsp;'
	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 }