Module:Convert: Difference between revisions

From TEPwiki, Urth's Encyclopedia
Jump to navigation Jump to search
Content added Content deleted
(don't change read-only eng_scales)
(sortable=on, sp=us, customary_units moved to Module:Convert/text; don't link a unit twice; tweaks: will have to fix LTf, STf another time)
Line 14: Line 14:
-- can be set to control which is used.
-- can be set to control which is used.
local SIprefixes, default_exceptions, link_exceptions, units
local SIprefixes, default_exceptions, link_exceptions, units
local all_categories, all_messages, disp_joins, en_option_value
local all_categories, all_messages, customary_units, disp_joins, en_option_value
local eng_scales, local_option_name, range_aliases, range_types
local eng_scales, local_option_name, range_aliases, range_types


Line 46: Line 46:
all_categories = converttext.all_categories
all_categories = converttext.all_categories
all_messages = converttext.all_messages
all_messages = converttext.all_messages
customary_units = converttext.customary_units
disp_joins = converttext.disp_joins
disp_joins = converttext.disp_joins
en_option_value = converttext.en_option_value
en_option_value = converttext.en_option_value
Line 256: Line 257:
}
}


local function lookup(unitcode, sp, what)
local function lookup(unitcode, opt_sp_us, what)
-- 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 'sp' is nil, or is 'us' for US spelling of SI prefixes and
-- Parameter opt_sp_us is true for US spelling of SI prefixes and
-- the symbol and names of the unit. If 'us', 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:
Line 282: Line 283:
return false, { 'cvt_should_be', t.shouldbe }
return false, { 'cvt_should_be', t.shouldbe }
end
end
local force_sp_us = (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
sp = 'us'
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, sp, what)
local success, result = lookup(target, opt_sp_us, what)
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' })
Line 310: Line 311:
prefix = v
prefix = v
else
else
local success, t = lookup(v, sp, 'no_combination')
local success, t = lookup(v, opt_sp_us, 'no_combination')
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 id
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
Line 337: Line 338:
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, sp, multiple and 'no_combination' or 'only_multiple')
local success, t = lookup(v, opt_sp_us, multiple and 'no_combination' or 'only_multiple')
if not success then return false, t end
if not success then return false, t end
cvt[i] = t
cvt[i] = t
Line 362: Line 363:
if t and t.prefixes then
if t and t.prefixes then
local result = shallow_copy(t)
local result = shallow_copy(t)
if sp == 'us' then
if opt_sp_us then
result.sp_us = true
result.sp_us = true
end
end
Line 384: Line 385:
local engscale = eng_scales[exponent]
local engscale = eng_scales[exponent]
if engscale then
if engscale then
local success, result = lookup(baseunit, sp, 'no_combination')
local success, result = lookup(baseunit, opt_sp_us, 'no_combination')
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
Line 795: Line 796:
singular = false -- any fraction (even with value 1) is regarded as plural
singular = false -- any fraction (even with value 1) is regarded as plural
end
end
if not valid_number(value) then -- for example, "1e310"
if not valid_number(value) then -- for example, "1e310" overflows
return false, { 'cvt_invalid_num' }
return false, { 'cvt_invalid_num' }
end
end
Line 873: Line 874:
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 ''
Line 930: Line 931:
local function translate_parms(parms)
local function translate_parms(parms)
-- Update fields in parms by translating parameters to those used at enwiki.
-- Update fields in parms by translating parameters to those used at enwiki.
for en_name, loc_name in pairs(local_option_name) do
-- TODO Needs checking.
if en_name ~= 'sing' then -- 'sing' is an old equivalent of 'adj'; cannot have both
for _, en_name in ipairs({ 'abbr', 'adj', 'disp', 'lk' }) do
local loc_name = local_option_name[en_name]
local loc_value = parms[loc_name]
local loc_value = parms[loc_name]
if loc_value == nil and en_name == 'adj' then
if loc_value == nil and en_name == 'adj' then
loc_name = local_option_name['sing']
loc_name = local_option_name['sing']
loc_value = parms[loc_name]
loc_value = parms[loc_name]
end
end
if loc_value then
local en_value = en_option_value[en_name][loc_value]
if loc_value then
local 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
add_warning(parms, 'cvt_unknown_option', loc_name .. '=' .. loc_value)
else
add_warning(parms, 'cvt_unknown_option', loc_name .. '=' .. loc_value)
end
elseif en_value == '' then
en_value = nil
elseif en_value:sub(1, 4) == 'opt_' then
parms[en_value] = true
en_value = nil
end
end
elseif en_value == '' then
parms[en_name] = en_value
en_value = nil
elseif en_value:sub(1, 4) == 'opt_' then
parms[en_value] = true
en_value = nil
end
end
parms[en_name] = en_value
end
end
end
end
Line 972: Line 973:
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 (use symbol not name) output only
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
Line 1,037: Line 1,038:
i = 5
i = 5
end
end
local success, in_unit_table = lookup(in_unit, parms.sp, 'no_combination')
local success, in_unit_table = lookup(in_unit, parms.opt_sp_us, 'no_combination')
if not success then return false, in_unit_table end
if not success then return false, in_unit_table end
if parms.test == 'msg' then
if parms.test == 'msg' then
Line 1,062: Line 1,063:
-- but it can be replaced by the optional subdiv[3].
-- but it can be replaced by the optional subdiv[3].
local success, subunit, subinfo
local success, subunit, subinfo
success, subunit = lookup(subdiv[3] or subcode, parms.sp, 'no_combination')
success, subunit = lookup(subdiv[3] or 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[i], 1)
success, subinfo = extract_number(parms, parms[i], 1)
Line 1,195: Line 1,196:
return 0
return 0
end
end
if out_current.symbol == 'ft' and floor(invalue) == invalue then
if out_current.exception == 'moreprecision' and floor(invalue) == invalue then
-- More precision when output ft with input value equal to an integer.
-- 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)
adjust = -log10(in_current.scale)
else
else
Line 1,520: Line 1,527:
-- 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.
-- If linked, set unit_table.linked = true so will not link same unit again
-- (like in {{convert|3|x|4|in|mm|lk=on}}).
-- 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'.
if want_link then
if unit_table.linked then
want_link = false
else
unit_table.linked = true
end
end
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
Line 1,584: Line 1,600:
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 customary_units = {
'[[United States customary units|US]] ',
'[[United States customary units|U.S.]] ',
'[[Imperial unit|imperial]] ',
'[[Imperial unit|imp]] ',
}
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
Line 1,838: Line 1,848:
second_unit.valinfo[1].show .. sep1 .. (make_id(parms, 1, second_unit)) .. mid
second_unit.valinfo[1].show .. sep1 .. (make_id(parms, 1, second_unit)) .. mid
end
end
local result
local result, mos
local mos = (abbr == 'mos')
local range = parms.range
local range = parms.range
if range then
mos = (abbr == 'mos')
if not (mos or (parms.is_range_x and not want_name)) then
first_unit.linked = false -- 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 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 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 first_unit.linked then
first_unit.linked = false
id = make_id(parms, 2, first_unit)
extra = hyphenated_maybe(parms, want_name, sep, id, 'in')
end
end
end
local valinfo = first_unit.valinfo
local valinfo = first_unit.valinfo
if range == nil then
if range then
decorate_value(parms, first_unit, 1)
result = valinfo[1].show
else
local sep1 = first_unit.sep
local sep1 = first_unit.sep
if mos then
if mos then
Line 1,861: Line 1,878:
end
end
decorate_value(parms, in_current, 2)
decorate_value(parms, in_current, 2)
result = valinfo[1].show .. sep1 .. id
result = valinfo[1].show .. sep1 .. id1
else
else
if abbr == 'in' or abbr == 'on' then
if abbr == 'in' or abbr == 'on' then
Line 1,870: Line 1,887:
end
end
result = range_text(range, want_name, parms, result, valinfo[2].show)
result = range_text(range, want_name, parms, result, valinfo[2].show)
else
decorate_value(parms, first_unit, 1)
result = valinfo[1].show
end
end
return result .. preunit .. extra
return result .. preunit .. extra
Line 1,903: Line 1,923:
local result
local result
local range = parms.range
local range = parms.range
if range then
if not (parms.is_range_x and not want_name) then
out_current.linked = false -- 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 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 == nil then
if range then
decorate_value(parms, out_current, 1)
result = valinfo[1].show
else
local sep1 = out_current.sep
local sep1 = out_current.sep
local abbr = parms.abbr
local abbr = parms.abbr
Line 1,917: Line 1,939:
end
end
decorate_value(parms, out_current, 2)
decorate_value(parms, out_current, 2)
result = valinfo[1].show .. sep1 .. id
result = valinfo[1].show .. sep1 .. id1
else
else
if abbr == 'out' or abbr == 'on' then
if abbr == 'out' or abbr == 'on' then
Line 1,926: Line 1,948:
end
end
result = range_text(range, want_name, parms, result, valinfo[2].show)
result = range_text(range, want_name, parms, result, valinfo[2].show)
else
decorate_value(parms, out_current, 1)
result = valinfo[1].show
end
end
if parms.opt_output_number_only then
if parms.opt_output_number_only then
Line 2,043: Line 2,068:
if not success then return false, out_unit end
if not success then return false, out_unit end
end
end
success, out_unit_table = lookup(out_unit, parms.sp, 'any_combination')
success, out_unit_table = lookup(out_unit, parms.opt_sp_us, 'any_combination')
if not success then return false, out_unit_table end
if not success then return false, out_unit_table end
if in_unit_table.utype ~= out_unit_table.utype then
if in_unit_table.utype ~= out_unit_table.utype then
Line 2,071: Line 2,096:
in_block, out_block = out_block, in_block
in_block, out_block = out_block, in_block
end
end
if parms.sortable == 'on' then
if parms.opt_sortable then
in_block = ntsh(invalue1, parms.debug) .. in_block
in_block = ntsh(invalue1, parms.debug) .. in_block
end
end

Revision as of 05:50, 30 May 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

-- Configuration options to keep magic values in one location.
local numdot, numsep, maxsigfig
-- The conversion data and message text are defined in separate modules.
-- To allow easy comparison between "require" and "loadData", a config option
-- can be set to control which is used.
local SIprefixes, default_exceptions, link_exceptions, units
local all_categories, all_messages, customary_units, disp_joins, en_option_value
local eng_scales, local_option_name, range_aliases, range_types

local function set_config(frame)
    -- Set configuration options from template #invoke or defaults.
    local args = frame.args
    numdot = args.numdot or '.'       -- decimal mark before fractional digits
    numsep = args.numsep or ','       -- thousands separator for numbers (',', '.', '')
    maxsigfig = args.maxsigfig or 14  -- maximum number of significant figures
    -- Scribunto sets the global variable 'mw'.
    -- A testing program can set the global variable 'is_test_run'.
    local convertdata, converttext, data_module, text_module
    if is_test_run then
        data_module = "convertdata"
        text_module = "converttext"
    else
        data_module = "Module:Convert/data"
        text_module = "Module:Convert/text"
    end
    if args.use_require then
        convertdata = require(data_module)
        converttext = require(text_module)
    else
        convertdata = mw.loadData(data_module)
        converttext = mw.loadData(text_module)
    end
    SIprefixes = convertdata.SIprefixes
    default_exceptions = convertdata.default_exceptions
    link_exceptions = convertdata.link_exceptions
    units = convertdata.units
    all_categories = converttext.all_categories
    all_messages = converttext.all_messages
    customary_units = converttext.customary_units
    disp_joins = converttext.disp_joins
    en_option_value = converttext.en_option_value
    eng_scales = converttext.eng_scales
    local_option_name = converttext.local_option_name
    range_aliases = converttext.range_aliases
    range_types = converttext.range_types
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 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 text = format(msg[1] or 'Missing message',
            mcode[2] or '?',
            mcode[3] or '?',
            mcode[4] or '?')
        local cat = all_categories[msg[2]] or ''
        local prefix = all_messages[msg.warning and 'cvt_prefix_warning' or 'cvt_prefix_error'] or ''
        local suffix = (prefix == '') and '' or '</span>'
        local regex, replace = msg.regex, msg.replace
        if regex and replace then
            text = text:gsub(regex, replace)
        end
        return prefix .. ' ' .. text .. cat .. suffix
    end
    return 'Convert internal error: unknown message'
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 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 a for a unit that accepts prefixes but where no prefix was used.
    -- In the latter case, and before use, fields symbol, name1, name1_us
    -- must be set from _symbol, _name1, _name1_us respectively.
    __index = function (self, key)
        local value
        if key == 'name1' or key == 'sym_us' then
            value = self.symbol
        elseif key == 'name2' then
            value = self.name1 .. 's'
        elseif key == 'name1_us' then
            value = self.name1
            if not rawget(self, 'name2_us') then
                -- If name1_us is 'foot', do not make name2_us by appending 's'.
                self.name2_us = self.name2
            end
        elseif key == 'name2_us' then
            local raw1_us = rawget(self, 'name1_us')
            if raw1_us then
                value = raw1_us .. 's'
            else
                value = self.name2
            end
        elseif key == 'link' then
            value = self.name1
        elseif key == 'engscale' or key == 'per' 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
            local pos = rawget(self, 'prefix_position') or 1
            value = self._name1
            value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
        elseif key == 'name2' then
            value = self.name1 .. 's'
        elseif key == 'name1_us' then
            value = rawget(self, '_name1_us')
            if value then
                local pos = rawget(self, 'prefix_position') or 1
                value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
            else
                value = self.name1
            end
        elseif key == 'name2_us' then
            if rawget(self, '_name1_us') then
                value = self.name1_us .. 's'
            else
                value = self.name2
            end
        elseif key == 'link' then
            value = self.name1
        elseif key == 'engscale' or key == 'per' 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) / unit2.scale
        elseif key == 'engscale' then
            value = false
        else
            return nil
        end
        rawset(self, key, value)
        return value
    end
}

local function lookup(unitcode, opt_sp_us, what)
    -- Return true, t where t is a copy of the unit's converter table,
    -- or return false, t where t is an error message table.
    -- Parameter 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, underscores in unitcode are replaced
    -- with spaces so {{convert|350|board_feet}} --> 350 board feet (0.83 m³).
    if unitcode == nil or unitcode == '' then
        return false, { 'cvt_no_unit' }
    end
    unitcode = unitcode:gsub('_', ' ')
    local t = units[unitcode]
    if t then
        if t.shouldbe then
            return false, { 'cvt_should_be', t.shouldbe }
        end
        local force_sp_us = 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)
            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 = {} }
            override_from(result, t, { 'default', 'invert', 'iscomplex', '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')
                    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')
                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 = 2, 1, -1 do
        -- Look for an SI prefix; should never occur with an alias.
        -- Check for longer prefix first ('dam' is decametre).
        -- Micro (µ) is two bytes in utf-8, so is found with plen = 2.
        local prefix = unitcode:sub(1, plen)
        local si = SIprefixes[prefix]
        if si then
            local t = units[unitcode:sub(plen+1)]
            if t and t.prefixes then
                local result = shallow_copy(t)
                if 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.
    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')
            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
    return false, { 'cvt_unknown', unitcode }
end

local function valid_number(num)
    -- Return true if num is a valid number.
    -- Expressed as a string, overflow or other problems are indicated with
    -- text like "1.#INF" or ".#IND" which are regarded as invalid here.
    if type(num) == 'number' and tostring(num):find('#', 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 == 'yes', 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 == 'yes' then
        style = 'border:1px solid'
    else
        style = 'display:none'
    end
    return '<span style="' .. style .. '">' .. result .. '</span>'
end

local function hyphenated(name)
    -- Return a hyphenated form of given name (for adjectival usage).
    -- This uses a simple and efficient procedure that works for most cases.
    -- Some units (if used) would require more, and can later think about
    -- adding a method to handle exceptions.
    -- The procedure is to replace each space with a hyphen, but
    -- not a space after ')' [for "(pre-1954&nbsp;US) nautical mile"], and
    -- not spaces immediately before '(' or in '(...)' [for cases like
    -- "British thermal unit (ISO)" and "Calorie (International Steam Table)"].
    local pos
    if name:sub(1, 1) == '(' then
        pos = name:find(')', 1, true)
        if pos then
            return name:sub(1, pos+1) .. name:sub(pos+2):gsub(' ', '-')
        end
    elseif name:sub(-1, -1) == ')' then
        pos = name:find('(', 1, true)
        if pos then
            return name:sub(1, pos-2):gsub(' ', '-') .. name:sub(pos-1)
        end
    end
    return name:gsub(' ', '-')
end

local function 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 with_separator(parms, text)
    -- Return text with thousand separators inserted, if wanted.
    -- The given text is like '123' or '12345.6789' or '1.23e45'
    -- (e notation can only occur when processing an input value).
    -- The text has no sign (caller inserts that later, if necessary).
    -- Separator is inserted only in the integer part of the significand
    -- (not after numdot, and not after 'e' or 'E').
    -- Four-digit integer parts have a separator (like '1,234').
    if parms.opt_nocomma or numsep == '' then
        return text
    end
    local last = text:match('()[' .. numdot .. 'eE]')  -- () returns position
    if last == nil then
        last = #text
    else
        last = last - 1  -- index of last character before dot/e/E
    end
    if last >= 4 then
        local groups = {}
        local first = last % 3
        if first > 0 then
            table.insert(groups, text:sub(1, first))
        end
        first = first + 1
        while first < last do
            table.insert(groups, text:sub(first, first+2))
            first = first + 3
        end
        return table.concat(groups, numsep) .. text:sub(last+1)
    end
    return text
end

-- Input values can use values like 1.23e12, but are never displayed
-- using exponent notation like 1.23×10¹².
-- Very small or very large output values use exponent notation.
-- Use format(fmtpower, significand, exponent) where each arg is a string.
local fmtpower = '%s<span style="margin:0 .15em 0 .25em">×</span>10<sup>%s</sup>'

local function with_exponent(show, exponent)
    -- Return wikitext to display the implied value in exponent notation.
    if #show > 1 then
        show = show:sub(1, 1) .. numdot .. show:sub(2)
    end
    return format(fmtpower, show, use_minus(tostring(exponent)))
end

local function make_sigfig(value, sigfig)
    -- Return show, exponent that are equivalent to the result of
    -- converting the number 'value' (where value >= 0) to a string,
    -- rounded to 'sigfig' significant figures.
    -- The returned items are:
    --   show: a string of digits; no sign and no dot;
    --         there is an implied dot before show.
    --   exponent: a number (an integer) to shift the implied dot.
    -- Resulting value = tonumber('.' .. show) * 10^exponent.
    -- Examples:
    --   make_sigfig(23.456, 3) returns '235', 2 (.235 * 10^2).
    --   make_sigfig(0.0023456, 3) returns '235', -2 (.235 * 10^-2).
    --   make_sigfig(0, 3) returns '000', 1 (.000 * 10^1).
    if sigfig <= 0 then
        sigfig = 1
    elseif sigfig > maxsigfig then
        sigfig = maxsigfig
    end
    if value == 0 then
        return string.rep('0', sigfig), 1
    end
    local exp, frac = math.modf(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)
    -- Return t where t is a table with the results; fields:
    --   show = wikitext formatted to display implied value
    --   is_scientific = true if show uses scientific notation
    --   clean = unformatted show (possibly adjusted and with inserted numdot)
    --   sign = '' or MINUS
    --   exponent = exponent (possibly adjusted)
    -- The clean and exponent fields can be used to calculate the
    -- rounded absolute value, if needed.
    --
    -- The value implied by the arguments is found from:
    --   exponent is nil; and
    --   show is a string of digits (no sign), with an optional dot;
    --   show = '123.4' is value 123.4, '1234' is value 1234.0;
    -- or:
    --   exponent is an integer indicating where dot should be;
    --   show is a string of digits (no sign and no dot);
    --   there is an implied dot before show;
    --   show does not start with '0';
    --   show = '1234', exponent = 3 is value 0.1234*10^3 = 123.4.
    --
    -- The formatted result:
    -- * Includes a Unicode minus if isnegative.
    -- * Has numsep inserted where necessary, if wanted.
    -- * Uses scientific notation for very small or large values.
    -- * Has no more than maxsigfig significant digits
    --   (same as old template and {{#expr}}).
    local sign = isnegative and MINUS or ''
    local maxlen = maxsigfig
    if exponent == nil then
        local integer, dot, fraction = show:match('^(%d*)([' .. numdot .. ']?)(.*)')
        if #integer >= 10 then
            show = integer .. fraction
            exponent = #integer
        elseif integer == '0' or integer == '' then
            local zeros, figs = fraction:match('^(0*)([^0]?.*)')
            if #figs == 0 then
                if #zeros > maxlen then
                    show = '0' .. numdot .. zeros:sub(1, maxlen)
                end
            elseif #zeros >= 4 then
                show = figs
                exponent = -#zeros
            elseif #figs > maxlen then
                show = '0' .. numdot .. zeros .. figs:sub(1, maxlen)
            end
        else
            maxlen = maxlen + #dot
            if #show > maxlen then
                show = show:sub(1, maxlen)
            end
        end
    end
    if exponent then
        if #show > maxlen then
            show = show:sub(1, maxlen)
        end
        if exponent > 10 or exponent <= -4 or (exponent == 10 and show ~= '1000000000') then
            -- Rounded value satisfies: value >= 1e9 or value < 1e-4 (1e9 = 0.1e10).
            return {
                clean = '.' .. show,
                exponent = exponent,
                sign = sign,
                show = sign .. with_exponent(show, exponent-1),
                is_scientific = true }
        end
        if exponent >= #show then
            show = show .. string.rep('0', exponent - #show)  -- result has no dot
        elseif exponent <= 0 then
            show = '0' .. numdot .. string.rep('0', -exponent) .. show
        else
            show = show:sub(1, exponent) .. numdot .. show:sub(exponent+1)
        end
    end
    if isnegative and show:match('^0.?0*$') then
        sign = ''  -- don't show minus if result is negative but rounds to zero
    end
    return {
        clean = show,
        sign = sign,
        show = sign .. with_separator(parms, show) }
end

-- Fraction output format (like old template).
-- frac1: sign, numerator, denominator
-- frac2: wholenumber, sign, numerator, denominator
local frac1 = '<span style="white-space:nowrap">%s<sup>%s</sup>&frasl;<sub>%s</sub></span>'
local frac2 = '<span class="frac nowrap">%s<s style="display:none">%s</s><sup>%s</sup>&frasl;<sub>%s</sub></span>'

local function extract_fraction(text, negative)
    -- If text represents a fraction, return value, show where
    -- value is a number and show is a string.
    -- Otherwise, return nil.
    --
    -- In the following, '(3/8)' represents the wikitext required to
    -- display a fraction with numerator 3 and denominator 8.
    -- In the wikitext, Unicode minus is used for a negative value.
    --   text          value, show            value, show
    --                 if not negative       if negative
    --   3 / 8         0.375, '(3/8)'        -0.375, '−(3/8)'
    --   2 + 3 / 8     2.375, '2(3/8)'       -1.625, '−2(−3/8)'
    --   2 - 3 / 8     1.625, '2(−3/8)'      -2.375, '−2(3/8)'
    --   1 + 20/8      3.5  , '1/(20/8)'     1.5   , '−1/(−20/8)'
    --   1 - 20/8      -1.5., '1(−20/8)'     -3.5  , '−1(20/8)'
    -- Wherever an integer appears above, numbers like 1.25 or 12.5e-3
    -- (which may be negative) are also accepted (like old template).
    -- Template interprets '1.23e+2+12/24' as '123(12/24)' = 123.5!
    local lhs, negfrac, rhs, numstr, numerator, denstr, denominator, wholestr, whole, value
    lhs, denstr = text:match('^%s*([^/]-)%s*/%s*(.-)%s*$')
    denominator = tonumber(denstr)
    if denominator == nil then return nil end
    wholestr, negfrac, rhs = lhs:match('^%s*(.-[^eE])%s*([+-])%s*(.-)%s*$')
    if wholestr == nil or wholestr == '' then
        wholestr = nil
        whole = 0
        numstr = lhs
    else
        whole = tonumber(wholestr)
        if whole == nil then return nil end
        numstr = rhs
    end
    negfrac = (negfrac == '-')
    numerator = tonumber(numstr)
    if numerator == nil then return nil end
    if negative == negfrac or wholestr == nil then
        value = whole + numerator / denominator
    else
        value = whole - numerator / denominator
        numstr = change_sign(numstr)
    end
    if not valid_number(value) then
        return nil  -- overflow or similar
    end
    numstr = use_minus(numstr)
    denstr = use_minus(denstr)
    local wikitext
    if wholestr then
        local sign = negative and MINUS or '+'
        if negative then
            wholestr = change_sign(wholestr)
        end
        wikitext = format(frac2, use_minus(wholestr), sign, numstr, denstr)
    else
        local sign = negative and MINUS or ''
        wikitext = format(frac1, sign, numstr, denstr)
    end
    return value, wikitext
end

local missing = { 'cvt_no_num', 'cvt_no_num2' }
local invalid = { 'cvt_bad_num', 'cvt_bad_num2' }

local function extract_number(parms, text, which, 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.
    -- Parameter 'which' (1 or 2) selects which input value is being
    -- processed (to select the appropriate error message, if needed).
    -- Before processing, the input text is cleaned:
    -- * Any thousand separators (valid or not) are removed.
    -- * Any sign (and optional following whitespace) is replaced with
    --   '-' (if negative) or '' (otherwise).
    --   That replaces Unicode minus with '-'.
    -- If successful, the returned info table contains named fields:
    --   value    = a valid number
    --   singular = true if value is 1 (to use singular form of units)
    --            = false if value is -1 (like old template)
    --   clean    = cleaned text with any separators and sign removed
    --   show     = text formatted for output
    -- For show:
    -- * Value is rounded, if wanted.
    -- * Thousand separators are inserted, 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).
    -- TODO Think about fact that the input value might be like 1.23e+123.
    -- Will the exponent break anything?
    text = strip(text)
    if text == nil or text == '' then return false, { missing[which] } end
    local clean, sign
    if numsep == '' then
        clean = text
    else
        clean = text:gsub('[' .. numsep .. ']', '')  -- use '[.]' if numsep is '.'
    end
    -- Remove any sign character (assuming a number starts with '.' or a digit).
    sign, clean = clean:match('^%s*([^ .%d]*)%s*(.*)')
    if sign == nil or clean == nil then
        return false, { missing[which] }  -- should never occur
    end
    local propersign, isnegative
    if sign == MINUS or sign == '-' or sign == '&minus;' then
        propersign = MINUS
        isnegative = true
    elseif sign == '+' then
        propersign = '+'
        isnegative = false
    elseif sign == '' then
        propersign = ''
        isnegative = false
    else
        return false, { invalid[which], text }
    end
    local show, singular
    local value = tonumber(clean)
    if value == nil then
        if not no_fraction then
            value, show = extract_fraction(clean, isnegative)
        end
        if value == nil then
            return false, { invalid[which], text }
        end
        singular = false  -- any fraction (even with value 1) is regarded as plural
    end
    if not valid_number(value) then  -- for example, "1e310" overflows
        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)
    end
    if isnegative and (value ~= 0) then
        value = -value
    end
    return true, {
        value = value,
        singular = singular,
        clean = clean,
        show = show
    }
end

local function require_integer(text, invalid)
    -- Return true, n where n = integer equivalent to given text,
    -- or return false, t where t is an error message table.
    -- Input should be the text for a simple integer (no separators, no Unicode minus,
    -- limited size). Using regex avoids irritations with input like '-0.000001'.
    if text == nil then return false, { 'cvt_no_num' } end
    if #text > 9 or text:match('^-?%d+$') == nil then
        return false, { invalid, text }
    end
    return true, tonumber(text)
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"
    --     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 add_warning(parms, mcode, text)
    -- If enabled, add a warning that will be displayed after the convert result.
    -- Currently, only the first warning is displayed.
    if parms.test == 'warnings' then  -- LATER replace this by enabling via a config option
        if parms.warnings == nil then
            parms.warnings = message({ mcode, text })
        end
    end
end

local function translate_parms(parms)
    -- Update fields in parms by translating parameters to those used at enwiki.
    for en_name, loc_name in pairs(local_option_name) do
        if en_name ~= 'sing' then  -- 'sing' is an old equivalent of 'adj'; cannot have both
            local loc_value = parms[loc_name]
            if loc_value == nil and en_name == 'adj' then
                loc_name = local_option_name['sing']
                loc_value = parms[loc_name]
            end
            if loc_value then
                local 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
                        add_warning(parms, 'cvt_unknown_option', loc_name .. '=' .. loc_value)
                    end
                elseif en_value == '' then
                    en_value = nil
                elseif en_value:sub(1, 4) == 'opt_' then
                    parms[en_value] = true
                    en_value = nil
                end
                parms[en_name] = en_value
            end
        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.
            parms.input_precision = tonumber(parms.adj:sub(-1))
            parms.adj = nil
        end
    end
    if parms.disp then
        if parms.disp == 'special_flip5' then
            parms.opt_round5 = true
            parms.opt_flip = true
            parms.disp = 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
        if parms.abbr == 'in' then
            parms.abbr = 'out'
        elseif parms.abbr == 'out' then
            parms.abbr = 'in'
        end
        if parms.lk == 'in' then
            parms.lk = 'out'
        elseif parms.lk == 'out' then
            parms.lk = 'in'
        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
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.
    -- 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 success, info1, info2
    local parms = {}  -- arguments passed to template
    for k, v in pairs(pframe.args) do
        parms[k] = v
    end
    translate_parms(parms)
    local range
    local next = strip(parms[2])
    local i = 3
    if next then
        range = range_types[next] or range_types[range_aliases[next]]
        if range == nil and next:sub(-7, -1) == 'nocomma' then
            local base = strip(next:sub(1, -8))
            range = range_types[base] or range_types[range_aliases[base]]
            if range then
                parms.opt_nocomma = true
            end
        end
    end
    success, info1 = extract_number(parms, parms[1], 1)  -- need to set parms.opt_nocomma before calling this
    if not success then return false, info1 end
    local in_unit
    if range == nil then
        in_unit = next
    else
        parms.range = range
        parms.is_range_x = range.is_range_x
        success, info2 = extract_number(parms, parms[3], 2)
        if not success then return false, info2 end
        in_unit = strip(parms[4])
        i = 5
    end
    local success, in_unit_table = lookup(in_unit, parms.opt_sp_us, 'no_combination')
    if not success then return false, in_unit_table 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 = { info1, info2 }  -- info2 is nil if no range
    in_unit_table.inout = 'in'  -- this is an input unit
    if not range then
        local subdivs = in_unit_table.subdivs  -- nil or a table of allowed subdivisions
        if subdivs then
            -- Look for a composite input unit like "|2|ft|6|in".
            local subcode = strip(parms[i+1])
            local subdiv = subdivs[subcode]
            if subdiv then
                -- subcode = unit code of a valid subdivision of main unit,
                -- but it can be replaced by the optional subdiv[3].
                local success, subunit, subinfo
                success, subunit = lookup(subdiv[3] or subcode, parms.opt_sp_us, 'no_combination')
                if not success then return false, subunit end  -- should never occur
                success, subinfo = extract_number(parms, parms[i], 1)
                if not success then return false, subinfo end
                i = i + 2
                subunit.inout = 'in'
                subunit.valinfo = { subinfo }
                -- Calculate total value as a number of subdivisions.
                -- subdiv[1] = number of subdivisions per main unit (integer > 1).
                local total = info1.value * subdiv[1] + subinfo.value
                in_unit_table = {
                        utype = in_unit_table.utype,
                        scale = subunit.scale,
                        default = subdiv[2] or in_unit_table.default,
                        valinfo = { { value = total, clean = subinfo.clean } },
                        composite = { in_unit_table, subunit },
                    }
            end
        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], 1, true)
        if success then
            i = i + 1
            in_unit_table.altitude = info.value
        end
    end
    next = strip(parms[i])
    i = i + 1
    local precision
    if tonumber(next) then
        precision = next
    else
        parms.out_unit = next
        next = strip(parms[i])
        if tonumber(next) then
            i = i + 1
            precision = next
        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 tonumber(parms[i]) then
            precision = strip(parms[i])
            i = i + 1
        end
    end
    parms.precision = parms.precision or precision  -- allow named parameter
    return true, parms, in_unit_table
end

local function default_precision(invalue, inclean, outvalue, in_current, out_current, extra)
    -- Return a default value for precision (an integer like 2, 0, -2).
    -- 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
    -- Count digits after decimal mark, handling cases like '12.345e6'.
    local exponent
    local integer, dot, fraction, expstr = inclean:match('^(%d*)([' .. numdot .. ']?)(%d*)(.*)')
    local e = expstr:sub(1, 1)
    if e == 'e' or e == 'E' then
        exponent = tonumber(expstr:sub(2))
    end
    if dot == '' then
        prec = -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
    local exception = (utype == 'temperature' and not
            (in_current.exception == 'temperature' or out_current.exception == 'temperature'))
    if exception then
        -- 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 == 'moreprecision' 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)
        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 fraction = inclean:match('[' .. numdot .. '](.*)') or ''
            local fmt
            if fraction == '' then
                fmt = '%.0f'
            else
                fmt = '%.' .. format('%d', #fraction - 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 from converting value in info,
    --      using the rounding specified in parms.
    --   singular = true if result is positive, and (after rounding)
    --      is "1", or like "1.00";
    --   (and more fields shown below, and a calculated 'absvalue' field).
    -- or return true, nil if no value specified;
    -- or return false, t where t is an error message table.
    -- This code combines convert/round because some rounding requires
    -- knowledge of what we are converting.
    local invalue, inclean, show, exponent, singular
    if info then
        invalue, inclean = info.value, info.clean
    end
    if invalue == nil or invalue == '' then
        return true, nil
    end
    if out_current.builtin == 'hand' then
        -- Convert to hands, then convert the fractional part to inches.
        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
        local fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or ''
        if fraction == '' then
            if not outinfo.use_default_precision then
                return true, outinfo
            end
            fmt = '%.0f'
        else
            fmt = '%.' .. format('%d', #fraction - 1) .. 'f'
        end
        local hands, inches = math.modf(tonumber(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) == numdot then
            inches = inches:sub(1, 1) .. inches:sub(3)
        end
        return true, {
            sign = outinfo.sign,
            singular = outinfo.singular,
            show = outinfo.sign .. with_separator(parms, format('%d', hands)) .. '.' .. inches
        }
    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
    local precision = parms.precision
    local sigfig = parms.sigfig
    if precision then
        success, precision = require_integer(precision, 'cvt_bad_prec')
        if not success then return false, precision end
    elseif sigfig then
        success, sigfig = require_integer(sigfig, 'cvt_bad_sigfig')
        if not success then return false, sigfig end
        if sigfig <= 0 then
            return false, { 'cvt_sigfig_pos', parms.sigfig }
        end
        show, exponent = make_sigfig(outvalue, sigfig)
    elseif parms.opt_round5 then
        show = format('%.0f', floor((outvalue / 5) + 0.5) * 5)
    else
        use_default_precision = true
        precision = default_precision(invalue, inclean, outvalue, in_current, out_current, extra)
    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 digits after dot)
            local shift = 10 ^ precision
            show = format('%.0f', outvalue/shift)
            if show ~= '0' then
                exponent = #show + precision
            end
        end
    end
    -- TODO Does following work when exponent ~= nil?
    --      What if show = '1000' and exponent = 1 (value = .1000*10^1 = 1)?
    --      What if show = '1000' and exponent = 2 (value = .1000*10^2 = 10)?
    if (show == '1' or show:match('^1%.0*$') ~= nil) and not isnegative then
        -- Use match because on some systems 0.99999999999999999 is 1.0.
        singular = true
    end
    local t = format_number(parms, show, exponent, isnegative)
    t.singular = singular
    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)
    --    '<' 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.
    local default = default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default
    if default == nil then
        return false, { 'cvt_no_default', unit_table.symbol }
    end
    if default:find('!', 1, true) == nil then
        return true, default
    end
    local t = {}
    default = default .. '!'  -- to get last item
    for item in default:gmatch('%s*(.-)%s*!') do
        table.insert(t, item)  -- split on '!', removing leading/trailing whitespace
    end
    if #t == 3 or #t == 4 then
        local success, result = pcall(evaluate_condition, value, t[1])
        if success then
            default = result and t[2] or t[3]
            if #t == 4 then
                default = default .. t[4]
            end
            return true, default
        end
    end
    return false, { 'cvt_bad_default', unit_table.symbol }
end

local function make_link(link, id)
    -- Return wikilink "[[link|id]]", possibly abbreviated as in examples:
    --   [[Mile|mile]]  --> [[mile]]
    --   [[Mile|miles]] --> [[mile]]s
    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.
    -- If linked, set unit_table.linked = true so will not link same unit again
    -- (like in {{convert|3|x|4|in|mm|lk=on}}).
    -- key_id is one of: 'symbol', 'sym_us', 'name1', 'name1_us', 'name2', 'name2_us'.
    if want_link then
        if unit_table.linked then
            want_link = false
        else
            unit_table.linked = true
        end
    end
    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 '
        else
            result = 'per '
        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.
        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[key_id]
    if want_link then
        local link = link_exceptions[unit_table.symbol] or unit_table.link
        if link then
            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
                -- 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
            else
                customary = ''
            end
            id = customary .. make_link(link, id)
        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.utype == 'temperature' or unit_table.utype == 'temperature change' then
                if not (unit_table.exception == 'temperature') then
                    want_name = false
                end
            end
        end
        if want_name == nil then
            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">10</span></span><s style="display:none">^</s><sup>'
                .. engscale.exponent .. '</sup>'
        else
            local number_id
            local lk = parms.lk
            if lk == 'on' or lk == inout then
                number_id = engscale[2] or engscale[1]
            else
                number_id = engscale[1]
            end
            info.show = info.show .. (parms.opt_adjectival and '-' or ' ') .. number_id
        end
    end
    local prefix = unit_table.vprefix
    if prefix then
        local info = unit_table.valinfo[which]
        info.show = prefix .. info.show
    end
end

local function process_input(parms, in_current)
    -- Processing required once per conversion.
    -- Return block of text to represent input (value/unit).
    if parms.opt_output_only or parms.opt_output_number_only or parms.opt_output_unit_only then
        parms.joins = { '', '' }
        return ''
    end
    local first_unit, second_unit
    local composite = in_current.composite  -- nil or table of units
    if composite then
        first_unit = composite[1]
        second_unit = composite[2]
    else
        first_unit = in_current
    end
    local id1, want_name = make_id(parms, 1, first_unit)
    local sep = first_unit.sep  -- 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
            id1 = id1 .. ' ' .. make_id(parms, 1, second_unit)
        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_extra 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
            result = range_text(range, want_name, parms, result, prefix .. 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
        return first_unit.valinfo[1].show .. sep1 .. id1 .. sep2 ..
               second_unit.valinfo[1].show .. sep1 .. (make_id(parms, 1, second_unit)) .. 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
            first_unit.linked = false  -- 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 first_unit.linked then
            first_unit.linked = false
            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
        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, want_name, parms, result, valinfo[2].show)
    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
            result = range_text(range, 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
            out_current.linked = false  -- 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
        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, want_name, parms, result, valinfo[2].show)
    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.
    local success, info1, info2
    local valinfo = in_unit_table.valinfo
    success, info1 = cvtround(parms, valinfo[1], in_unit_table, out_unit_table)
    if not success then return false, info1 end
    success, info2 = cvtround(parms, valinfo[2], in_unit_table, out_unit_table)
    if not success then return false, info2 end
    out_unit_table.valinfo = { info1, info2 }
    return true, process_one_output(parms, out_unit_table)
end

local function make_output_multiple(parms, 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 outvalue, sign, fmt
        local results = {}
        for i = 1, #combos do
            local thisvalue
            local out_current = combos[i]
            out_current.inout = 'out'
            local scale = multiple[i]
            if i == 1 then  -- least significant unit ('in' from 'ftin')
                local success, outinfo = cvtround(parms, info, in_unit_table, out_current)
                if not success then return false, outinfo end
                sign = outinfo.sign
                local fraction = (outinfo.show):match('[' .. numdot .. '](.*)') or ''
                fmt = '%.' .. #fraction .. 'f'  -- to reproduce precision
                if fraction == '' then
                    outvalue = floor(outinfo.raw_absvalue + 0.5)  -- keep all integer digits of least significant unit
                else
                    outvalue = outinfo.absvalue
                end
            end
            if scale then
                outvalue, thisvalue = floor(outvalue / scale), outvalue % scale
            else
                thisvalue = outvalue
            end
            local id
            if want_name then
                id = out_current[(thisvalue == 1) and 'name1' or 'name2']
            else
                id = out_current['symbol']
            end
            if want_link then
                local link = out_current.link
                if link then
                    id = make_link(link, id)
                end
            end
            local strval = (thisvalue == 0) and '0' or with_separator(parms, format(fmt, thisvalue))
            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
        local success, result2 = make_result(valinfo[2])
        if not success then return false, result2 end
        result = range_text(range, want_name, parms, result, result2)
    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.
    local success, out_unit_table
    local invalue1 = in_unit_table.valinfo[1].value
    local out_unit = parms.out_unit
    if out_unit == nil or out_unit == '' then
        success, out_unit = get_default(invalue1, in_unit_table)
        if not success then return false, out_unit end
    end
    success, out_unit_table = lookup(out_unit, parms.opt_sp_us, 'any_combination')
    if not success then return false, out_unit_table end
    if in_unit_table.utype ~= out_unit_table.utype then
        return false, { 'cvt_mismatch', in_unit_table.utype, out_unit_table.utype }
    end
    local outputs = {}
    local combos  -- nil (for 'ft' or 'ftin'), or table of unit tables (for 'm ft')
    if out_unit_table.multiple == nil then  -- nil ('ft' or 'm ft'), or table of factors ('ftin')
        combos = out_unit_table.combination
    end
    local imax = combos and #combos or 1  -- 1 (single unit) or number of unit tables
    for i = 1, imax do
        local success, item
        local out_current = combos and combos[i] or out_unit_table
        out_current.inout = 'out'
        if out_current.multiple == nil then
            success, item = make_output_single(parms, 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
    local in_block = process_input(parms, in_unit_table)
    local out_block = parms.opt_input_unit_only and '' or table.concat(outputs, '; ')
    if parms.opt_flip then
        in_block, out_block = out_block, in_block
    end
    if parms.opt_sortable then
        in_block = ntsh(invalue1, parms.debug) .. in_block
    end
    local wikitext
    if parms.table_joins then
        wikitext = parms.table_joins[1] .. in_block .. parms.table_joins[2] .. out_block
    else
        wikitext = in_block .. parms.joins[1] .. out_block .. parms.joins[2]
    end
    if parms.warnings 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 }