Module:Convert: Difference between revisions
Jump to navigation
Jump to search
Content added Content deleted
(update from sandbox per Template talk:Convert#Module version 7) |
(update from sandbox per Template talk:Convert#Module version 8) |
||
Line 17: | Line 17: | ||
local numdot -- must be '.' or ',' or a character which works in a regex |
local numdot -- must be '.' or ',' or a character which works in a regex |
||
local numsep, numsep_remove, numsep_remove2 |
local numsep, numsep_remove, numsep_remove2 |
||
local |
local data_code, all_units |
||
local text_code |
local text_code |
||
local varname -- can be a code to use variable names that depend on value |
local varname -- can be a code to use variable names that depend on value |
||
Line 143: | Line 143: | ||
config = frame.args |
config = frame.args |
||
maxsigfig = config.maxsigfig or 14 -- maximum number of significant figures |
maxsigfig = config.maxsigfig or 14 -- maximum number of significant figures |
||
local data_module, text_module |
|||
-- Scribunto sets the global variable 'mw'. |
|||
local sandbox = config.sandbox and ('/' .. config.sandbox) or '' |
|||
-- A testing program can set the global variable 'is_test_run'. |
|||
data_module = "Module:Convert/data" .. sandbox |
|||
text_module = "Module:Convert/text" .. sandbox |
|||
if is_test_run then |
|||
extra_module = "Module:Convert/extra" .. sandbox |
|||
local langcode = mw.language.getContentLanguage().code |
|||
spell_module = "Module:ConvertNumeric" |
|||
data_module = "convertdata-" .. langcode |
|||
text_module = "converttext-" .. langcode |
|||
extra_module = "convertextra-" .. langcode |
|||
spell_module = "ConvertNumeric" |
|||
else |
|||
local sandbox = config.sandbox and ('/' .. config.sandbox) or '' |
|||
data_module = "Module:Convert/data" .. sandbox |
|||
text_module = "Module:Convert/text" .. sandbox |
|||
extra_module = "Module:Convert/extra" .. sandbox |
|||
spell_module = "Module:ConvertNumeric" |
|||
end |
|||
data_code = mw.loadData(data_module) |
data_code = mw.loadData(data_module) |
||
text_code = mw.loadData(text_module) |
text_code = mw.loadData(text_module) |
||
default_exceptions = data_code.default_exceptions |
|||
link_exceptions = data_code.link_exceptions |
|||
all_units = data_code.all_units |
all_units = data_code.all_units |
||
local translation = text_code.translation_table |
local translation = text_code.translation_table |
||
Line 554: | Line 542: | ||
-- This is never called to determine a unit name or link because "per" units |
-- This is never called to determine a unit name or link because "per" units |
||
-- are handled as a special case. |
-- are handled as a special case. |
||
-- Similarly, the default output is handled elsewhere. |
|||
__index = function (self, key) |
__index = function (self, key) |
||
local value |
local value |
||
Line 577: | Line 566: | ||
end |
end |
||
} |
} |
||
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. |
|||
local result = { utype = unit_table.utype, per = {} } |
|||
override_from(result, unit_table, { 'invert', 'iscomplex', 'default', 'link', 'symbol', 'symlink' }) |
|||
result.symbol_raw = (result.symbol or false) -- to distinguish between a defined exception and a metatable calculation |
|||
local prefix |
|||
for i, v in ipairs(unit_table.per) do |
|||
if i == 1 and v == '' then |
|||
-- First unit symbol can be empty; that gives a nil first unit table. |
|||
elseif i == 1 and text_code.currency[v] then |
|||
prefix = currency_text or v |
|||
else |
|||
local success, t = ulookup(v) |
|||
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 |
|||
local multiplier = unit_table.multiplier |
|||
if not result.utype then |
|||
-- Creating an automatic "per" unit. |
|||
local unit1 = result.per[1] |
|||
local utype = (unit1 and unit1.utype or prefix or '') .. '/' .. result.per[2].utype |
|||
local t = data_code.per_unit_fixups[utype] |
|||
if t then |
|||
if type(t) == 'table' then |
|||
utype = t.utype or utype |
|||
result.link = result.link or t.link |
|||
multiplier = multiplier or t.multiplier |
|||
else |
|||
utype = t |
|||
end |
|||
end |
|||
result.utype = utype |
|||
end |
|||
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(unitcode, opt_sp_us, what, utable, fails, depth) |
local function lookup(unitcode, opt_sp_us, what, utable, fails, depth) |
||
Line 632: | Line 665: | ||
return true, result |
return true, result |
||
end |
end |
||
if t.per then |
|||
local per = t.per -- nil/false, or a numbered table for "x/y" units |
|||
return make_per(t, force_sp_us, function (ucode) return lookup(ucode, opt_sp_us, 'no_combination', utable, fails, depth) end) |
|||
if per then |
|||
local result = { utype = t.utype, per = {} } |
|||
result.scalemultiplier = t.multiplier or 1 |
|||
override_from(result, t, { 'invert', 'iscomplex', 'default', 'link', 'symbol', 'symlink' }) |
|||
result.symbol_raw = (result.symbol or false) -- to distinguish between a defined exception and a metatable calculation |
|||
local cvt = result.per |
|||
local prefix |
|||
for i, v in ipairs(per) do |
|||
if i == 1 and text_code.currency[v] then |
|||
prefix = currency_text or v |
|||
else |
|||
local success, t = lookup(v, opt_sp_us, 'no_combination', utable, fails, depth) |
|||
if not success then return false, t end |
|||
cvt[i] = t |
|||
if t.sp_us then -- if the top or bottom unit forces sp=us, set the per unit to use the correct name/symbol |
|||
force_sp_us = true |
|||
end |
|||
end |
|||
end |
|||
if prefix then |
|||
result.vprefix = prefix |
|||
else |
|||
result.vprefix = false -- to avoid calling __index |
|||
end |
|||
result.sp_us = force_sp_us |
|||
return true, setmetatable(result, unit_per_mt) |
|||
end |
end |
||
local combo = t.combination -- nil or a table of unitcodes |
local combo = t.combination -- nil or a table of unitcodes |
||
Line 776: | Line 784: | ||
if success or err_is_fatal then |
if success or err_is_fatal then |
||
return success, result |
return success, result |
||
end |
|||
end |
|||
-- Look for x/y; split on right-most slash to get scale correct (x/y/z is x/y per z). |
|||
local top, bottom = unitcode:match('^(.-)/([^/]+)$') |
|||
if top and not unitcode:find('e%d') then |
|||
-- If valid, create an automatic "per" unit for an "x/y" unit code. |
|||
-- The unitcode must not include extraneous spaces. |
|||
-- 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(ucode, opt_sp_us, 'no_combination', utable, fails, depth) end) |
|||
if success then |
|||
return true, result |
|||
end |
end |
||
end |
end |
||
Line 1,003: | Line 1,023: | ||
-- digits in local language |
-- digits in local language |
||
-- The given text is like '123' or '12345.6789' or '1.23e45' |
-- The given text is like '123' or '12345.6789' or '1.23e45' |
||
-- ( |
-- (at one time e-notation could occur when processing an input value, |
||
-- but is now handled elsewhere for scientific notation). |
|||
-- The text has no sign (caller inserts that later, if necessary). |
-- The text has no sign (caller inserts that later, if necessary). |
||
-- Separator is inserted only in the integer part of the significand |
-- Separator is inserted only in the integer part of the significand |
||
Line 1,029: | Line 1,050: | ||
end |
end |
||
-- |
-- An input value like 1.23e12 is displayed using scientific notation (1.23×10¹²). |
||
-- |
-- That also makes the output use scientific notation, except for small values. |
||
-- |
-- In addition, very small or very large output values use scientific notation. |
||
-- Use format(fmtpower, significand, '10', exponent) where each |
-- Use format(fmtpower, significand, '10', exponent) where each argument is a string. |
||
local fmtpower = '%s<span style="margin:0 .15em 0 .25em">×</span>%s<sup>%s</sup>' |
local fmtpower = '%s<span style="margin:0 .15em 0 .25em">×</span>%s<sup>%s</sup>' |
||
Line 1,038: | Line 1,059: | ||
-- Return wikitext to display the implied value in scientific notation. |
-- Return wikitext to display the implied value in scientific notation. |
||
-- Input uses en digits; output uses digits in local language. |
-- Input uses en digits; output uses digits in local language. |
||
if #show > 1 then |
|||
show = show:sub(1, 1) .. '.' .. show:sub(2) |
|||
end |
|||
return format(fmtpower, from_en(show), from_en('10'), use_minus(from_en(tostring(exponent)))) |
return format(fmtpower, from_en(show), from_en('10'), use_minus(from_en(tostring(exponent)))) |
||
end |
end |
||
Line 1,162: | Line 1,180: | ||
-- * Uses a custom decimal mark, if wanted. |
-- * Uses a custom decimal mark, if wanted. |
||
-- * Has digits grouped where necessary, if wanted. |
-- * Has digits grouped where necessary, if wanted. |
||
-- * Uses scientific notation for very small or large values |
-- * Uses scientific notation if requested, or for very small or large values |
||
-- (which forces |
-- (which forces result to not be spelled). |
||
-- * Has no more than maxsigfig significant digits |
-- * Has no more than maxsigfig significant digits |
||
-- (same as old template and {{#expr}}). |
-- (same as old template and {{#expr}}). |
||
local xhi, xlo -- these control when scientific notation (exponent) is used |
|||
if parms.opt_scientific then |
|||
xhi, xlo = 4, 2 -- default for output if input uses e-notation |
|||
elseif parms.opt_scientific_always then |
|||
xhi, xlo = 0, 0 -- always use scientific notation (experimental) |
|||
else |
|||
xhi, xlo = 10, 4 -- default |
|||
end |
|||
local sign = isnegative and MINUS or '' |
local sign = isnegative and MINUS or '' |
||
local maxlen = maxsigfig |
local maxlen = maxsigfig |
||
Line 1,176: | Line 1,202: | ||
if not tfrac and not exponent then |
if not tfrac and not exponent then |
||
local integer, dot, decimals = show:match('^(%d*)(%.?)(.*)') |
local integer, dot, decimals = show:match('^(%d*)(%.?)(.*)') |
||
if |
if integer == '0' or integer == '' then |
||
show = integer .. decimals |
|||
exponent = #integer |
|||
elseif integer == '0' or integer == '' then |
|||
local zeros, figs = decimals:match('^(0*)([^0]?.*)') |
local zeros, figs = decimals:match('^(0*)([^0]?.*)') |
||
if #figs == 0 then |
if #figs == 0 then |
||
Line 1,185: | Line 1,208: | ||
show = '0.' .. zeros:sub(1, maxlen) |
show = '0.' .. zeros:sub(1, maxlen) |
||
end |
end |
||
elseif #zeros >= |
elseif #zeros >= xlo then |
||
show = figs |
show = figs |
||
exponent = -#zeros |
exponent = -#zeros |
||
Line 1,191: | Line 1,214: | ||
show = '0.' .. zeros .. figs:sub(1, maxlen) |
show = '0.' .. zeros .. figs:sub(1, maxlen) |
||
end |
end |
||
elseif #integer >= xhi then |
|||
show = integer .. decimals |
|||
exponent = #integer |
|||
else |
else |
||
maxlen = maxlen + #dot |
maxlen = maxlen + #dot |
||
Line 1,199: | Line 1,225: | ||
end |
end |
||
if exponent then |
if exponent then |
||
local function zeros(n) |
|||
return string.rep('0', n) |
|||
end |
|||
if #show > maxlen then |
if #show > maxlen then |
||
show = show:sub(1, maxlen) |
show = show:sub(1, maxlen) |
||
end |
end |
||
if exponent > |
if exponent > xhi or exponent <= -xlo or (exponent == xhi and show ~= '1' .. zeros(xhi - 1)) then |
||
-- When xhi, xlo = 10, 4 (the default), scientific notation is used if the |
|||
-- Rounded value satisfies: value >= 1e9 or value < 1e-4 (1e9 = 0.1e10). |
|||
-- rounded value satisfies: value >= 1e9 or value < 1e-4 (1e9 = 0.1e10), |
|||
-- except if show is '1000000000' (1e9), for example: |
|||
-- {{convert|1000000000|m|m|sigfig=10}} → 1,000,000,000 metres (1,000,000,000 m) |
|||
local significand |
|||
if #show > 1 then |
|||
significand = show:sub(1, 1) .. '.' .. show:sub(2) |
|||
else |
|||
significand = show |
|||
end |
|||
return { |
return { |
||
clean = '.' .. show, |
clean = '.' .. show, |
||
exponent = exponent, |
exponent = exponent, |
||
sign = sign, |
sign = sign, |
||
show = sign .. with_exponent( |
show = sign .. with_exponent(significand, exponent-1), |
||
is_scientific = true, |
is_scientific = true, |
||
} |
} |
||
end |
end |
||
if exponent >= #show then |
if exponent >= #show then |
||
show = show .. |
show = show .. zeros(exponent - #show) -- result has no dot |
||
elseif exponent <= 0 then |
elseif exponent <= 0 then |
||
show = '0.' .. |
show = '0.' .. zeros(-exponent) .. show |
||
else |
else |
||
show = show:sub(1, exponent) .. '.' .. show:sub(exponent+1) |
show = show:sub(1, exponent) .. '.' .. show:sub(exponent+1) |
||
Line 1,261: | Line 1,299: | ||
-- x (if present) is an integer or has a single digit after decimal mark |
-- x (if present) is an integer or has a single digit after decimal mark |
||
-- y and z are unsigned integers |
-- y and z are unsigned integers |
||
-- e |
-- e-notation is not accepted |
||
-- The overall number can start with '+' or '-' (so '12+3/4' and '+12+3/4' |
-- The overall number can start with '+' or '-' (so '12+3/4' and '+12+3/4' |
||
-- and '-12-3/4' are valid). |
-- and '-12-3/4' are valid). |
||
Line 1,295: | Line 1,333: | ||
-- '12.3+' and '12.3-' are also accepted (single digit after decimal point) |
-- '12.3+' and '12.3-' are also accepted (single digit after decimal point) |
||
-- because '12.3+1/2 hands' is valid (12 hands 3½ inches). |
-- because '12.3+1/2 hands' is valid (12 hands 3½ inches). |
||
local num1, num2, frac_sign = prefix:match('^(%d+)(%.?%d?)%s*([+-])$') |
local num1, num2, frac_sign = prefix:match('^(%d+)(%.?%d?)%s*([+%-])$') |
||
if num1 == nil then return nil end |
if num1 == nil then return nil end |
||
if num2 == '' then -- num2 must be '' or like '.1' but not '.' or '.12' |
if num2 == '' then -- num2 must be '' or like '.1' but not '.' or '.12' |
||
Line 1,400: | Line 1,438: | ||
end |
end |
||
if show == nil then |
if show == nil then |
||
-- clean is a non-empty string with no spaces, and does not represent a fraction, |
|||
-- and tonumber(clean) is a number. |
|||
-- 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. |
|||
-- The input value is spelled if specified so any e-notation is ignored; |
|||
-- 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) |
|||
local precision = parms.input_precision |
|||
if precision and 0 <= precision and precision <= 8 then |
|||
local fmt = '%.' .. format('%d', precision) .. 'f' |
|||
return fmt:format(value + 2e-14) -- fudge for some common cases of bad rounding |
|||
end |
|||
end |
|||
singular = (value == 1) |
singular = (value == 1) |
||
local scientific |
|||
local precision = parms.input_precision |
|||
local significand, exponent = clean:match('^([%d.]+)[Ee]([+%-]?%d+)') |
|||
if precision and 0 <= precision and precision <= 8 then |
|||
if significand then |
|||
local fmt = '%.' .. format('%d', precision) .. 'f' |
|||
show = with_exponent(rounded(tonumber(significand)) or significand, exponent) |
|||
show = fmt:format(value + 2e-14) -- fudge for some common cases of bad rounding |
|||
scientific = true |
|||
else |
else |
||
show = clean |
show = with_separator(parms, rounded(value) or clean) |
||
end |
end |
||
show = propersign .. |
show = propersign .. show |
||
if parms.opt_spell_in then |
if parms.opt_spell_in then |
||
show = spell_number(parms, 'in', propersign .. clean) or show |
show = spell_number(parms, 'in', propersign .. clean) or show |
||
scientific = false |
|||
end |
|||
if scientific then |
|||
parms.opt_scientific = true |
|||
end |
end |
||
end |
end |
||
Line 1,811: | Line 1,868: | ||
parms.joins = disp_joins[disp] or default_joins |
parms.joins = disp_joins[disp] or default_joins |
||
parms.join_between = parms.joins[3] or parms.join_between |
parms.join_between = parms.joins[3] or parms.join_between |
||
parms.wantname = parms.joins.wantname |
|||
end |
end |
||
if (en_default and not parms.opt_lang_local and (parms[1] or ''):find('%d')) or parms.opt_lang_en then |
if (en_default and not parms.opt_lang_local and (parms[1] or ''):find('%d')) or parms.opt_lang_en then |
||
Line 2,496: | Line 2,554: | ||
-- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise. |
-- 'smallsuffix' if (value < 120), or 'bigsuffix' otherwise. |
||
-- Input must use en digits and '.' decimal mark. |
-- Input must use en digits and '.' decimal mark. |
||
local default = default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default |
local default = data_code.default_exceptions[unit_table.defkey or unit_table.symbol] or unit_table.default |
||
if not default then |
if not default then |
||
local per = unit_table.per |
|||
if per then |
|||
local function a_default(v, u) |
|||
local success, ucode = get_default(v, u) |
|||
if not success then |
|||
return '?' -- an unlikely error has occurred; will cause lookup of default to fail |
|||
end |
|||
-- Attempt to use only the first unit if a combination or output multiple. |
|||
-- This is not bulletproof but should work for most cases. |
|||
-- Where it does not work, the convert will need to specify the wanted output unit. |
|||
local t = all_units[ucode] |
|||
if t then |
|||
local combo = t.combination |
|||
if combo then |
|||
-- For a multiple like ftin, the "first" unit (ft) is last in the combination. |
|||
local i = t.multiple and #t.combination or 1 |
|||
ucode = combo[i] |
|||
end |
|||
end |
|||
return ucode |
|||
end |
|||
local unit1, unit2 = per[1], per[2] |
|||
local def1 = (unit1 and a_default(value, unit1) or unit_table.vprefix or '') |
|||
local def2 = a_default(1, unit2) -- 1 because per unit of denominator |
|||
return true, def1 .. '/' .. def2 |
|||
end |
|||
return false, { 'cvt_no_default', unit_table.symbol } |
return false, { 'cvt_no_default', unit_table.symbol } |
||
end |
end |
||
Line 2,637: | Line 2,721: | ||
if want_link and unit_table.link then |
if want_link and unit_table.link then |
||
if abbr_on or not varname then |
if abbr_on or not varname then |
||
result = (unit1 and unit1 |
result = (unit1 and linked_id(unit1, key_id, false, clean) or '') .. result .. linked_id(unit2, key_id2, false, '1') |
||
else |
else |
||
result = (unit1 and variable_name(clean, unit1) or '') .. result .. variable_name('1', unit2) |
result = (unit1 and variable_name(clean, unit1) or '') .. result .. variable_name('1', unit2) |
||
Line 2,677: | Line 2,761: | ||
end |
end |
||
if want_link then |
if want_link then |
||
local link = link_exceptions[unit_table.linkey or unit_table.symbol] or unit_table.link |
local link = data_code.link_exceptions[unit_table.linkey or unit_table.symbol] or unit_table.link |
||
if link then |
if link then |
||
local before = '' |
local before = '' |
||
Line 2,745: | Line 2,829: | ||
else |
else |
||
if abbr_org == nil then |
if abbr_org == nil then |
||
if parms.wantname then |
|||
if disp == 'br' or disp == 'or' or disp == 'slash' then |
|||
want_name = true |
want_name = true |
||
end |
end |