Module:Convert: Difference between revisions

Content deleted Content added
fixes for default rounding with composite input units
process input/output in order left-to-right so left is linked; avoid overlinking, including of spelled numbers; boost default precision if input uses a fraction
Line 7:
local format = string.format
local log10 = math.log10
 
local function boolean(text)
-- Return true if text represents a "true" option value.
if text then
text = text:lower()
if text == '1' or text == 'y' or text == 'yes' or text == 'true' then
return true
end
end
end
 
-- Configuration options to keep magic values in one ___location.
local numdot, numsep, maxsigfig, warnings
-- The conversion data and message text are defined in separate modules.
-- To allow easy comparison between "require" and "loadData", a config option
Line 23 ⟶ 33:
numsep = args.numsep or ',' -- thousands separator for numbers (',', '.', '')
maxsigfig = args.maxsigfig or 14 -- maximum number of significant figures
warnings = boolean(args.warnings) -- true if want warnings for invalid options
-- Scribunto sets the global variable 'mw'.
-- A testing program can set the global variable 'is_test_run'.
Line 175 ⟶ 186:
elseif key == 'link' then
value = self.name1
elseif key == 'engscale' or key == 'perbuiltin' then
value = false
else
Line 217 ⟶ 228:
elseif key == 'link' then
value = self.name1
elseif key == 'engscale' or key == 'perbuiltin' then
value = false
else
Line 247 ⟶ 258:
local unit1, unit2 = per[1], per[2]
value = (unit1 and unit1.scale or 1) / unit2.scale
elseif key == 'engscalebuiltin' then
value = false
else
Line 584 ⟶ 595:
 
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
Line 757 ⟶ 768:
-- * 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
Line 765 ⟶ 774:
clean = text
else
clean = text:gsub('[' .. numsep .. ']', '') -- use '[.x]' ifin numsepcase x is '.'
end
-- Remove any sign character (assuming a number starts with '.' or a digit).
Line 874 ⟶ 883:
return nil
end
return withspace(withspace(preunit1, 1), -1)
end
preunit2 = preunit2 or ''
local trim2 = strip(preunit2)
if trim1 == '' and trim2 == '' then
return nil, nil
end
if trim1 ~= '+' then
preunit1 = withspace(preunit1, 1)
end
if trim2 == ' ' then -- trick to make preunit2 empty
preunit2 = nil
elseif trim2 == '' then
preunit2 = preunit1
elseif trim2 ~= '+' then
preunit2 = withspace(preunit2, 1)
end
return preunit1, preunit2
Line 922 ⟶ 931:
-- 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 })
Line 1,174 ⟶ 1,183:
local integer, dot, fraction, expstr = inclean:match('^(%d*)([' .. numdot .. ']?)(%d*)(.*)')
local e = expstr:sub(1, 1)
local boost = 0 -- can increase default precision
if e == 'e' or e == 'E' then
exponent = tonumber(expstr:sub(2))
elseif expstr:find('/', 1, true) then
boost = 1 -- any input fraction is regarded as one extra digit of precision
end
if dot == '' then
Line 1,229 ⟶ 1,241:
minprec = extra.minprec or minprec
end
return math.max(floor(prec + adjust + boost), minprec)
end
 
Line 1,407 ⟶ 1,419:
end
else
precision = -precision -- #digits to zero (in addition to any digits after dot)
local shift = 10 ^ precision
show = format('%.0f', outvalue/shift)
Line 1,415 ⟶ 1,427:
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.
Line 1,523 ⟶ 1,532:
end
 
local linked_pages -- to record linked pages so will not link to the same page more than once
local function make_link(link, id)
 
local function make_link(link, id, link_key)
-- Return wikilink "[[link|id]]", possibly abbreviated as in examples:
-- [[Mile|mile]] --> [[mile]]
-- [[Mile|miles]] --> [[mile]]s
-- However, just id is returned if:
-- * no link given (so caller does not need to check if a link was defined); or
-- * link has previously been used during the current convert (to avoid overlinking).
-- Linking with a unit uses the unit table as the link key, which fails to detect
-- overlinking for conversions like (each links "mile" twice):
-- {{convert|1|impgal/mi|USgal/mi|lk=on}}
-- {{convert|1|l/km|impgal/mi USgal/mi|lk=on}}
link_key = link_key or link -- use key if given (the key, but not the link, may be known when need to cancel a link record)
if link == nil or link == '' or linked_pages[link_key] then
return id
end
linked_pages[link_key] = true
local l = link:sub(1, 1):lower() .. link:sub(2)
if link == id or l == id then
Line 1,540 ⟶ 1,563:
-- 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
Line 1,613 ⟶ 1,627:
local link = link_exceptions[unit_table.symbol] or unit_table.link
if link then
local before = ''
local i = unit_table.customary
if i == 1 and unit_table.sp_us then
Line 1,622 ⟶ 1,637:
local customary = customary_units[i]
if customary then
local pertext
if id:sub(1, 1) == '/' then
-- Want unit "/USgal" to display as "/U.S. gal", not "U.S. /gal".
pertext = '/'
id = id:sub(2)
elseif id:sub(1, 4) == 'per ' then
-- Similarly want "per U.S. gallon", not "U.S. per gallon" (but in practice this is unlikely to be used).
pertext = 'per '
id = id:sub(5)
else
pertext = ''
end
-- Omit any "US"/"U.S."/"imp"/"imperial" from start of id since that will be inserted.
local removes = (i < 3) and { 'US&nbsp;', 'US ', 'U.S.&nbsp;', 'U.S. ' } or { 'imp&nbsp;', 'imp ', 'imperial ' }
Line 1,631 ⟶ 1,658:
end
end
before = pertext .. make_link(customary.link, customary[1]) .. ' '
else
customary = ''
end
id = customarybefore .. make_link(link, id, unit_table)
end
end
Line 1,758 ⟶ 1,784:
local lk = parms.lk
if lk == 'on' or lk == inout then
number_id = make_link(engscale[2] or.link, engscale[1])
else
number_id = engscale[1]
end
-- WP:NUMERAL recommends "&nbsp;" in values like "12 million".
info.show = info.show .. (parms.opt_adjectival and '-' or ' ') .. number_id
info.show = info.show .. (parms.opt_adjectival and '-' or '&nbsp;') .. number_id
end
end
Line 1,788 ⟶ 1,815:
end
local id1, want_name = make_id(parms, 1, first_unit)
local sep = first_unit.sep -- separator between value and unit, set by make_id
local preunit = parms.preunit1
if preunit then
Line 1,840 ⟶ 1,867:
local result = prefix .. valinfo[1].show
if range then
resultlocal prefix2 = range_textmake_id(range, want_name, parms, result2, prefixfirst_unit) .. valinfo[2].show)'&nbsp;'
result = range_text(range, want_name, parms, result, prefix2 .. valinfo[2].show)
end
return preunit .. result
Line 1,866 ⟶ 1,894:
mos = (abbr == 'mos')
if not (mos or (parms.is_range_x and not want_name)) then
linked_pages[first_unit.linked] = falsenil -- so the second and only id will be linked, if wanted
end
end
Line 1,873 ⟶ 1,901:
if mos and was_hyphenated then
mos = false -- suppress repeat of unit in a range
if linked_pages[first_unit.linked] then
linked_pages[first_unit.linked] = falsenil
id = make_id(parms, 2, first_unit)
extra = hyphenated_maybe(parms, want_name, sep, id, 'in')
Line 1,938 ⟶ 1,966:
if range then
if not (parms.is_range_x and not want_name) then
linked_pages[out_current.linked] = falsenil -- so the second and only id will be linked, if wanted
end
end
Line 2,043 ⟶ 2,071:
local link = out_current.link
if link then
id = make_link(link, id, out_current)
end
end
Line 2,074 ⟶ 2,102:
-- Return true, s where s = final wikitext result,
-- or return false, t where t is an error message table.
linked_pages = {}
local success, out_unit_table
local invalue1 = in_unit_table.valinfo[1].value
Line 2,086 ⟶ 2,115:
return false, { 'cvt_mismatch', in_unit_table.utype, out_unit_table.utype }
end
local outputsflipped = {}parms.opt_flip
local parts = {}
local combos -- nil (for 'ft' or 'ftin'), or table of unit tables (for 'm ft')
for part = 1, 2 do
if out_unit_table.multiple == nil then -- nil ('ft' or 'm ft'), or table of factors ('ftin')
-- The LHS (parts[1]) is normally the input, but is the output if flipped.
combos = out_unit_table.combination
-- Process LHS first so it will be linked, if wanted.
end
-- Linking to the same item is suppressed in the RHS to avoid overlinking.
local imax = combos and #combos or 1 -- 1 (single unit) or number of unit tables
if (part == 1 and not flipped) or (part == 2 and flipped) then
for i = 1, imax do
parts[part] = process_input(parms, in_unit_table)
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
local outputs = {}
success, item = make_output_multiple(parms, in_unit_table, out_current)
local combos -- nil (for 'ft' or 'ftin'), or table of unit tables (for 'm ft')
if out_unit_table.multiple == nil then -- nil ('ft' or 'm ft'), or table of factors ('ftin')
combos = out_unit_table.combination
end
local imax = combos and #combos or 1 -- 1 (single unit) or number of unit tables
for i = 1, imax do
local success, item
local out_current = combos and combos[i] or out_unit_table
out_current.inout = 'out'
if out_current.multiple == nil then
success, item = make_output_single(parms, in_unit_table, out_current)
else
success, item = make_output_multiple(parms, in_unit_table, out_current)
end
if not success then return false, item end
table.insert(outputs, item)
end
parts[part] = parms.opt_input_unit_only and '' or table.concat(outputs, '; ')
end
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_blockparts[1] = ntsh(invalue1, parms.debug) .. in_blockparts[1]
end
local wikitext
if parms.table_joins then
wikitext = parms.table_joins[1] .. in_blockparts[1] .. parms.table_joins[2] .. out_blockparts[2]
else
wikitext = in_blockparts[1] .. parms.joins[1] .. out_blockparts[2] .. parms.joins[2]
end
if parms.warnings then