Module:Convert: Difference between revisions

(update from sandbox per Template talk:Convert#Module version 11)
(update from sandbox per Template talk:Convert#Module version 12)
Line 35:
local extra_module -- name of module with extra units
local extra_units -- nil or table of extra units from extra_module
local ignore_extra_units -- if true, do not require the extra module
 
-- Some options in the invoking template can set variables used later in the module.
Line 577 ⟶ 576:
}
 
local function make_per(unit_table, force_sp_us, ulookup)
-- Return true, t where t is a per unit with unit codes expanded to unit tables,
-- or return false, t where t is an error message table.
Line 593 ⟶ 592:
if not success then return false, t end
result.per[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
Line 617 ⟶ 613:
result.scalemultiplier = multiplier or 1
result.vprefix = prefix or false -- set to non-nil to avoid calling __index
result.sp_us = force_sp_us
return true, setmetatable(result, unit_per_mt)
end
 
local function lookup(unitcodeparms, opt_sp_usunitcode, what, utable, fails, depth)
-- Return true, t where t is a copy of the unit's converter table,
-- or return false, t where t is an error message table.
-- Parameter opt_sp_us is true for US spelling of SI prefixes and
-- the symbol and name of the unit. If true, the result includes field
-- sp_us = true (that field may also have been in the unit definition).
-- Parameter 'what' determines whether combination units are accepted:
-- 'no_combination' : single unit only
Line 658 ⟶ 650:
return false, { 'cvt_should_be', t.shouldbe }
end
local force_sp_us = opt_sp_us
if t.sp_us then
force_sp_usparms.opt_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(targetparms, opt_sp_ustarget, what, utable, fails, depth)
if not success then return false, result end
override_from(result, t, { 'customary', 'default', 'link', 'symbol', 'symlink' })
Line 676 ⟶ 666:
end
if t.per then
return make_per(t, force_sp_us, function (ucode) return lookup(ucodeparms, opt_sp_usucode, 'no_combination', utable, fails, depth) end)
end
local combo = t.combination -- nil or a table of unitcodes
Line 689 ⟶ 679:
local cvt = result.combination
for i, v in ipairs(combo) do
local success, t = lookup(vparms, opt_sp_usv, multiple and 'no_combination' or 'only_multiple', utable, fails, depth)
if not success then return false, t end
cvt[i] = t
Line 696 ⟶ 686:
end
local result = shallow_copy(t)
result.sp_us = force_sp_us
if result.prefixes then
result.si_name = ''
Line 715 ⟶ 704:
if t and t.prefixes then
local result = shallow_copy(t)
ifresult.si_name = parms.opt_sp_us thenand si.name_us or si.name
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)
Line 740 ⟶ 722:
local engscale = text_code.eng_scales[exponent]
if engscale then
local success, result = lookup(baseunitparms, opt_sp_usbaseunit, 'no_combination', utable, fails, depth)
if success and not (result.offset or result.builtin or result.engscale) then
result.defkey = unitcode -- key to lookup default exception
Line 776 ⟶ 758:
local cvt = result.combination
for i, v in ipairs(combo) do
local success, t = lookup(vparms, opt_sp_usv, 'only_multiple', utable, fails, depth)
if not success then return false, t end
if i == 1 then
Line 802 ⟶ 784:
-- Engineering notation (apart from at start and which has been stripped before here),
-- is not supported so do not make a per unit if find text like 'e3' in unitcode.
local success, result = make_per({ per = {top, bottom} }, opt_sp_us, function (ucode) return lookup(ucodeparms, opt_sp_usucode, 'no_combination', utable, fails, depth) end)
if success then
return true, result
end
end
if not ignore_extra_unitsparms.opt_ignore_error and not get_range(unitcode) then
-- Want the "what links here" list for the extra_module to show only cases
-- where an extra unit is used, so do not require it if invoked from {{val}}
Line 823 ⟶ 805:
fails[unitcode] = true
local other = (utable == all_units) and extra_units or all_units
local success, result = lookup(unitcodeparms, opt_sp_usunitcode, what, other, fails, depth)
if success then
return true, result
Line 834 ⟶ 816:
local en_code = ustring.gsub(unitcode, '%d', to_en_table)
if en_code ~= unitcode then
return lookup(en_codeparms, opt_sp_usen_code, what, utable, fails, depth)
end
end
Line 1,442 ⟶ 1,424:
if show == nil then
-- clean is a non-empty string with no spaces, and does not represent a fraction,
-- and value = tonumber(clean) is a number >= 0.
-- If the input uses e-notation, show will be displayed using a power of ten, but
-- we use the number as given so it might not be normalized scientific notation.
Line 1,448 ⟶ 1,430:
-- that allows input like 2e6 to be spelled as "two million" which works
-- because the spell module converts '2e6' to '2000000' before spelling.
local function rounded(value, default, exponent)
local precision = parms.opt_ri
if precision then
local fmt = '%.' .. format('%d', precision) .. 'f'
returnlocal result = fmt:format(tonumber(value) + 2e-14) -- fudge for some common cases of bad rounding
if not exponent then
singular = (tonumber(result) == 1)
end
return result
end
return default
Line 1,460 ⟶ 1,446:
local significand, exponent = clean:match('^([%d.]+)[Ee]([+%-]?%d+)')
if significand then
show = with_exponent(parms, rounded(significand, significand, exponent), exponent)
scientific = true
else
Line 1,467 ⟶ 1,453:
show = propersign .. show
if parms.opt_spell_in then
show = spell_number(parms, 'in', propersign .. rounded(cleanvalue, clean)) or show
scientific = false
end
Line 1,674 ⟶ 1,660:
end
local success
success, subunit = lookup(subcodeparms, parms.opt_sp_ussubcode, 'no_combination')
if not success then return false, subunit end -- should never occur
success, subinfo = extract_number(parms, parms[iparm])
Line 1,702 ⟶ 1,688:
composite_units[i].fixed_name = name
else
local success, alternate = lookup(unitparms, parms.opt_sp_usunit, 'no_combination')
if not success then return false, alternate end -- should never occur
alternate.inout = 'in'
Line 1,957 ⟶ 1,943:
range:add(range_item)
if type(range_item) == 'table' then
-- For range "x", if append unit to some values, append it to all.
parms.is_range_x = range_item.is_range_x
parms.in_range_x = parms.in_range_x or range_item.in_range_x
parms.out_range_x = parms.out_range_x or range_item.out_range_x
parms.abbr_range_x = parms.abbr_range_x or range_item.abbr_range_x
is_change = range_item.is_range_change
end
Line 1,994 ⟶ 1,983:
}
local in_unit = strip(parms[2])
local success, in_unit_table = lookup(in_unitparms, parms.opt_sp_usin_unit, 'no_combination')
if not success then return end
in_unit_table.valinfo = { info }
Line 2,023 ⟶ 2,012:
local success, msg = translate_parms(parms, kv_pairs)
if not success then return false, msg end
ignore_extra_units = parms.opt_ignore_error
local success, i, in_unit, in_unit_table = simple_get_values(parms)
if not success then
Line 2,031 ⟶ 2,019:
in_unit = strip(parms[i])
i = i + 1
success, in_unit_table = lookup(in_unitparms, parms.opt_sp_usin_unit, 'no_combination')
if not success then
if in_unit == nil then
Line 2,211 ⟶ 2,199:
end
if in_current.istemperature and out_current.istemperature then
-- Converting between common temperatures (°C, °F, °R, K); not keVT, MK.
-- Kelvin value can be almost zero, or small but negative due to precision problems.
-- Also, an input value like -300 C (below absolute zero) gives negative kelvins.
Line 2,364 ⟶ 2,352:
end
 
local function make_table_or_sort(parms, invalue, info, in_current, scaled_top)
-- Set options to handle output for a table or a sort key, or both.
-- The text sort key is based on the value resulting from converting
-- the input to a fake base unit with scale = 1, and other properties
-- required for a conversion derived from the input unit.
-- For other modules, return the sort key in a hidden span element, and
local sortkey
-- the scaled value used to generate the sort key.
-- If scaled_top is set, it is the scaled value of the numerator of a per unit
-- to be combined with this unit (the denominator) to make the sort key.
-- Scaling only works with units that convert with a factor (not temperature).
local sortkey, scaled_value
if parms.opt_sortable_on then
local base = { -- a fake unit with enough fields for a valid convert
Line 2,381 ⟶ 2,374:
outvalue = extra.outvalue
end
if in_current.istemperature then
-- Have converted to kelvin; assume numbers close to zero have a
-- rounding error and should be zero.
if abs(outvalue) < 1e-12 then
outvalue = 0
end
end
if scaled_top and outvalue ~= 0 then
outvalue = scaled_top / outvalue
end
scaled_value = outvalue
if not valid_number(outvalue) then
if outvalue < 0 then
Line 2,401 ⟶ 2,405:
end
end
local sortspan
if sortkey and (parms.opt_sortable_debug or not parms.table_align) then
parms.join_beforesortspan = parms.opt_sortable_debug and
'<span style="border:1px solid;display:inline;" class="sortkey">' .. sortkey .. '</span>' or
'<span style="display:none" class="sortkey">' .. sortkey .. '</span>'
parms.join_before = sortspan
end
if parms.table_align then
Line 2,415 ⟶ 2,421:
parms.table_joins = joins
end
return sortspan, scaled_value
end
 
Line 2,472 ⟶ 2,479:
elseif parms.opt_round then
local n = parms.opt_round
show = format('%.0f', floor((outvalue /if n) +== 0.5) * n)then
local integer, fracpart = math.modf(floor(2 * outvalue + 0.5) / 2)
if fracpart == 0 then
show = format('%.0f', integer)
else
show = format('%.1f', integer + fracpart)
end
else
show = format('%.0f', floor((outvalue / n) + 0.5) * n)
end
else
local inclean = info.clean
Line 2,775 ⟶ 2,791:
end
 
local function linked_id(parms, unit_table, key_id, want_link, clean)
-- Return final unit id (symbol or name), optionally with a wikilink,
-- and update unit_table.sep if required.
Line 2,822 ⟶ 2,838:
if want_link and unit_table.link then
if abbr_on or not varname then
result = (unit1 and linked_id(parms, unit1, key_id, false, clean) or '') .. result .. linked_id(parms, unit2, key_id2, false, '1')
else
result = (unit1 and variable_name(clean, unit1) or '') .. result .. variable_name('1', unit2)
Line 2,832 ⟶ 2,848:
end
if unit1 then
result = linked_id(parms, unit1, key_id, want_link, clean) .. result
if unit1.sep then
unit_table.sep = unit1.sep
Line 2,839 ⟶ 2,855:
unit_table.sep = ''
end
return result .. linked_id(parms, unit2, key_id2, want_link, '1')
end
if multiplier then
Line 2,866 ⟶ 2,882:
local before = ''
local i = unit_table.customary
if i == 1 and unit_tableparms.sp_usopt_sp_us then
i = 2 -- show "U.S." not "US"
end
Line 2,908 ⟶ 2,924:
-- id = unit name or symbol, possibly modified
-- f = true if id is a name, or false if id is a symbol
-- using 1stthe orvalue 2ndfor valuesindex ('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 ''
Line 2,968 ⟶ 2,984:
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_tableparms.sp_usopt_sp_us then
key = key .. '_us'
end
Line 2,984 ⟶ 2,999:
end
unit_table.sep = '&nbsp;'
key = unit_tableparms.sp_usopt_sp_us and 'sym_us' or 'symbol'
end
return linked_id(parms, unit_table, key, want_link, info.clean), want_name
end
 
local function decorate_value(parms, unit_table, which, number_word)
-- If needed, update unit_table so values will be shown with extra information.
-- For consistency with the old template (but different from fmtpower),
Line 3,012 ⟶ 3,027:
'</span></span><s style="display:none">^</s><sup>' ..
from_en(tostring(engscale.exponent)) .. '</sup>'
elseif number_word then
else
local number_id
local lk = parms.lk
Line 3,071 ⟶ 3,086:
local join1 = parms.joins[1]
if join1 == ' (' or join1 == ' [' then
parms.joins = { ' [' .. first_unit[first_unitparms.sp_usopt_sp_us and 'sym_us' or 'symbol'] .. ']' .. join1 , parms.joins[2] }
end
end
Line 3,108 ⟶ 3,123:
return table.concat(parts, sep2) .. mid
end
local result,add_unit = (parms.abbr == 'mos') or
parms[parms.opt_flip and 'out_range_x' or 'in_range_x'] or
local abbr = parms.abbr
(not want_name and parms.abbr_range_x)
local range = parms.range
if range and not add_unit then
linked_pages[first_unit] = nil -- so the final and only id will be linked, if wanted
mos = (abbr == 'mos')
if not (mos or (parms.is_range_x and not want_name)) then
linked_pages[first_unit] = nil -- so the second and only id will be linked, if wanted
end
end
local id = (range == nil) and id1 or make_id(parms, 2range.n + 1, first_unit) or id1
local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, 'in')
if mos and was_hyphenated then
add_unit = false
mos = false -- suppress repeat of unit in a range
if linked_pages[first_unit] then
linked_pages[first_unit] = nil
id = make_id(parms, 2, first_unit)
extra = hyphenated_maybe(parms, want_name, sep, id, 'in')
end
end
local result
local valinfo = first_unit.valinfo
if range then
iffor range.ni == 10, range.n thendo
local number_word
-- Like {{convert|1|x|2|ft}} (one range item; two values).
if i == range.n then
-- Do what old template did.
add_unit = false
local sep1 = first_unit.sep
if number_word mos= thentrue
end
decorate_value(parms, in_current, 1)
decorate_value(parms, in_currentfirst_unit, 2i+1, number_word)
resultlocal show = valinfo[i+1].show .. sep1 .. id1
if add_unit then
elseif parms.is_range_x and not want_name then
show = show .. first_unit.sep .. (i == 0 and id1 or make_id(parms, i+1, first_unit))
if abbr == 'in' or abbr == 'on' then
end
decorate_value(parms, in_current, 1)
if i == 0 then
end
result = show
decorate_value(parms, in_current, 2)
result = valinfo[1].show .. sep1 .. id1
else
result = range_text(range[i], want_name, parms, result, show, 'in')
if abbr == 'in' or abbr == 'on' then
decorate_value(parms, in_current, 1)
end
decorate_value(parms, in_current, 2)
result = valinfo[1].show
end
result = range_text(range[1], want_name, parms, result, valinfo[2].show, 'in')
else
-- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify.
decorate_value(parms, in_current, 1)
result = valinfo[1].show
for i = 1, range.n do
decorate_value(parms, in_current, i+1)
result = range_text(range[i], want_name, parms, result, valinfo[i+1].show, 'in')
end
end
else
decorate_value(parms, first_unit, 1, true)
result = valinfo[1].show
end
Line 3,195 ⟶ 3,190:
return preunit .. result
end
local add_unit = (parms[parms.opt_flip and 'in_range_x' or 'out_range_x'] or
local result
(not want_name and parms.abbr_range_x)) and
not parms.opt_output_number_only
local range = parms.range
if range and not add_unit then
linked_pages[out_current] = nil -- so the final and only id will be linked, if wanted
if not (parms.is_range_x and not want_name) then
linked_pages[out_current] = nil -- so the second and only id will be linked, if wanted
end
end
local id = (range == nil) and id1 or make_id(parms, 2range.n + 1, out_current) or id1
local extra, was_hyphenated = hyphenated_maybe(parms, want_name, sep, id, 'out')
if was_hyphenated then
add_unit = false
end
local result
local valinfo = out_current.valinfo
if range then
iffor range.ni == 10, range.n thendo
local sep1 = out_current.sepnumber_word
localif abbri == parmsrange.abbrn then
add_unit = false
if parms.is_range_x and not want_name then
number_word = true
if abbr == 'out' or abbr == 'on' then
end
decorate_value(parms, out_current, 1)
decorate_value(parms, out_current, i+1, number_word)
end
local show = valinfo[i+1].show
decorate_value(parms, out_current, 2)
if add_unit then
result = valinfo[1].show .. sep1 .. id1
show = show .. out_current.sep .. (i == 0 and id1 or make_id(parms, i+1, out_current))
end
if i == 0 then
result = show
else
result = range_text(range[i], want_name, parms, result, show, 'out')
if abbr == 'out' or abbr == 'on' then
decorate_value(parms, out_current, 1)
end
decorate_value(parms, out_current, 2)
result = valinfo[1].show
end
result = range_text(range[1], want_name, parms, result, valinfo[2].show, 'out')
else
-- Like {{convert|1|x|2|x|3|ft}} (two or more range items): simplify.
decorate_value(parms, out_current, 1)
result = valinfo[1].show
for i = 1, range.n do
decorate_value(parms, out_current, i+1)
result = range_text(range[i], want_name, parms, result, valinfo[i+1].show, 'out')
end
end
else
decorate_value(parms, out_current, 1, true)
result = valinfo[1].show
end
Line 3,424 ⟶ 3,414:
end
if not bad_output and not out_unit_table then
success, out_unit_table = lookup(out_unitparms, parms.opt_sp_usout_unit, 'any_combination')
if success then
local mismatch = check_mismatch(in_unit_table, out_unit_table)
Line 3,541 ⟶ 3,531:
end
 
local function _unit(unitcode, options)
return { convert = main_convert }
-- Helper function for Module:Val to look up a unit.
-- Parameter unitcode must be a string to identify the wanted unit.
-- Parameter options must be nil or a table with optional fields:
-- value = number (for sort key; default value is 1)
-- scaled_top = nil for a normal unit, or a number for a unit which is
-- the denominator of a per unit (for sort key)
-- si = { 'symbol', 'link' }
-- (a table with two strings) to make an SI unit
-- that will be used for the look up
-- link = true if result should be [[linked]]
-- sort = 'on' or 'debug' if result should include a sort key in a
-- span element ('debug' makes the key visible)
-- name = true for the name of the unit instead of the symbol
-- us = true for the US spelling of the unit, if any
-- Return nil if unitcode is not a non-empty string.
-- Otherwise return a table with fields:
-- text = requested symbol or name of unit, optionally linked
-- scaled_value = input value adjusted by unit scale; used for sort key
-- sortspan = span element with sort key like that provided by {{ntsh}},
-- calculated from the result of converting value
-- to a base unit with scale 1.
-- unknown = true if the unitcode was not known
unitcode = strip(unitcode)
if unitcode == nil or unitcode == '' then
return nil
end
set_config({})
linked_pages = {}
options = options or {}
local parms = {
abbr = options.name and 'off' or 'on',
lk = options.link and 'on' or nil,
opt_sp_us = options.us and true or nil,
opt_ignore_error = true, -- do not add pages using this function to 'what links here' for Module:Convert/extra
opt_sortable_on = options.sort == 'on' or options.sort == 'debug',
opt_sortable_debug = options.sort == 'debug',
}
local utable
if options.si then
-- Make a dummy table of units (just one unit) for lookup to use.
-- This makes lookup recognize any SI prefix in the unitcode.
local symbol = options.si[1] or '?'
utable = { [symbol] = {
_name1 = symbol,
_name2 = symbol,
_symbol = symbol,
utype = symbol,
scale = symbol == 'g' and 0.001 or 1,
prefixes = 1,
default = symbol,
link = options.si[2],
}}
end
local success, unit_table = lookup(parms, unitcode, 'no_combination', utable)
if not success then
unit_table = setmetatable({
symbol = unitcode, name2 = unitcode,
default = "m", defkey = "m", linkey = "m",
utype = "length", scale = 1 }, unit_mt)
end
local value = tonumber(options.value) or 1
local clean = tostring(abs(value))
local info = {
value = value,
altvalue = value,
singular = (clean == '1'),
clean = clean,
show = clean,
}
unit_table.inout = 'in'
unit_table.valinfo = { info }
local sortspan, scaled_value
if options.sort then
sortspan, scaled_value = make_table_or_sort(parms, value, info, unit_table, options.scaled_top)
end
return {
text = make_id(parms, 1, unit_table),
sortspan = sortspan,
scaled_value = scaled_value,
unknown = not success and true or nil,
}
end
 
return { convert = main_convert, _unit = _unit }
Anonymous user