Module:Convert: Difference between revisions

Jump to navigation Jump to search
Content added Content deleted
(update from sandbox per Template talk:Convert#Module v2 soon)
(update from sandbox per Template talk:Convert:Module version 3)
Line 1: Line 1:
-- Convert a value from one unit of measurement to another.
-- Convert a value from one unit of measurement to another.
-- Example: {{convert|123|lb|kg}} --> 123 pounds (56 kg)
-- Example: {{convert|123|lb|kg}} --> 123 pounds (56 kg)
-- See [[:en:Template:Convert/Transwiki guide]] if copying to another wiki.


local MINUS = '−' -- Unicode U+2212 MINUS SIGN (UTF-8: e2 88 92)
local MINUS = '−' -- Unicode U+2212 MINUS SIGN (UTF-8: e2 88 92)
Line 14: Line 15:
-- Conversion data and message text are defined in separate modules.
-- Conversion data and message text are defined in separate modules.
local config, maxsigfig
local config, maxsigfig
local numdot, numsep -- each must be a single byte for simple regex search/replace
local numdot -- must be '.' or ',' or a character which works in a regex as used here
local numsep, numsep_remove
local default_exceptions, link_exceptions, all_units
local default_exceptions, link_exceptions, all_units
local text_code
local text_code
Line 22: Line 24:
-- Use translation_table in convert/text to change the following.
-- Use translation_table in convert/text to change the following.
local group_method = 3 -- code for how many digits are in a group
local group_method = 3 -- code for how many digits are in a group
local per_word = 'per' -- for units like "miles per gallon"
local per_word = 'per' -- for units like "liters per kilometer"
local plural_suffix = 's' -- only other useful value is probably '' to disable plural unit names
local plural_suffix = 's' -- only other useful value is probably '' to disable plural unit names


Line 31: Line 33:
local extra_module -- name of module with extra units
local extra_module -- name of module with extra units
local extra_units -- nil or table of extra units from extra_module
local extra_units -- nil or table of extra units from extra_module

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


local function from_en(text)
local function from_en(text)
Line 62: Line 53:
-- and no separators (they have to be removed here to handle cases like
-- and no separators (they have to be removed here to handle cases like
-- numsep = '.' and numdot = ',' with input "1.234.567,8").
-- numsep = '.' and numdot = ',' with input "1.234.567,8").
if numsep ~= '' then
if numsep_remove ~= '' then
text = text:gsub('[' .. numsep .. ']', '') -- use '[x]' in case x is '.'
text = text:gsub(numsep_remove, '')
end
end
if numdot ~= '.' then
if numdot ~= '.' then
text = text:gsub('[' .. numdot .. ']', '.')
text = text:gsub(numdot, '.')
end
end
if to_en_table then
if to_en_table then
Line 80: Line 71:
-- Set configuration options from template #invoke or defaults.
-- Set configuration options from template #invoke or defaults.
config = frame.args
config = frame.args
numdot = config.numdot or '.' -- decimal mark before fractional digits
numsep = config.numsep or ',' -- group separator for numbers (',', '.', '')
maxsigfig = config.maxsigfig or 14 -- maximum number of significant figures
maxsigfig = config.maxsigfig or 14 -- maximum number of significant figures
-- Scribunto sets the global variable 'mw'.
-- Scribunto sets the global variable 'mw'.
Line 93: Line 82:
spell_module = "ConvertNumeric"
spell_module = "ConvertNumeric"
else
else
local sandbox = boolean(config.sandbox) and '/sandbox' or ''
local sandbox = config.sandbox and ('/' .. config.sandbox) or ''
data_module = "Module:Convert/data" .. sandbox
data_module = "Module:Convert/data" .. sandbox
text_module = "Module:Convert/text" .. sandbox
text_module = "Module:Convert/text" .. sandbox
Line 106: Line 95:
local translation = text_code.translation_table
local translation = text_code.translation_table
if translation then
if translation then
numdot = translation.numdot
numsep = translation.numsep
if translation.group then
if translation.group then
group_method = translation.group
group_method = translation.group
Line 133: Line 124:
end
end
end
end
numdot = config.numdot or numdot or '.' -- decimal mark before fractional digits
numsep = config.numsep or numsep or ',' -- group separator for numbers
-- numsep should be ',' or '.' or '' or ' ' or a Unicode character.
-- numsep_remove must work in a regex to identify separators to be removed.
numsep_remove = (numsep == '.') and '%.' or numsep
end
end


Line 331: Line 327:
-- END: Code required only for built-in units.
-- END: Code required only for built-in units.
------------------------------------------------------------------------
------------------------------------------------------------------------

local function get_range(word)
-- Return a range (string or table) corresponding to word (like "to"),
-- or return nil if not a range word.
local ranges = text_code.ranges
return ranges.types[word] or ranges.types[ranges.aliases[word]]
end


local function check_mismatch(unit1, unit2)
local function check_mismatch(unit1, unit2)
Line 369: Line 372:


local unit_mt = {
local unit_mt = {
-- Metatable to get missing values for a unit that does not accept SI prefixes,
-- Metatable to get missing values for a unit that does not accept SI prefixes.
-- or for a unit that accepts prefixes but where no prefix was used.
-- In the latter case, and before use, fields symbol, name1, name1_us
-- must be set from _symbol, _name1, _name1_us respectively.
-- Warning: The boolean value 'false' is returned for any missing field
-- Warning: The boolean value 'false' is returned for any missing field
-- so __index is not called twice for the same field in a given unit.
-- so __index is not called twice for the same field in a given unit.
Line 403: Line 403:
end
end
}
}

local function prefixed_name(unit, name, index)
-- Return unit name with SI prefix inserted at correct position.
-- index = 1 (name1), 2 (name2), 3 (name1_us), 4 (name2_us).
-- The position is a byte (not character) index, so use Lua's sub().
local pos = rawget(unit, 'prefix_position')
if type(pos) == 'string' then
pos = tonumber(split(pos, ',')[index])
end
if pos then
return name:sub(1, pos - 1) .. unit.si_name .. name:sub(pos)
end
return unit.si_name .. name
end


local unit_prefixed_mt = {
local unit_prefixed_mt = {
-- Metatable to get missing values for a unit that accepts SI prefixes,
-- Metatable to get missing values for a unit that accepts SI prefixes.
-- and where a prefix has been used.
-- Before use, fields si_name, si_prefix must be defined.
-- Before use, fields si_name, si_prefix must be defined.
-- The unit must define _symbol, _name1 and
-- may define _sym_us, _name1_us, _name2_us
-- (_sym_us, _name2_us may be defined for a language using sp=us
-- to refer to a variant unrelated to U.S. units).
__index = function (self, key)
__index = function (self, key)
local value
local value
Line 413: Line 430:
value = self.si_prefix .. self._symbol
value = self.si_prefix .. self._symbol
elseif key == 'sym_us' then
elseif key == 'sym_us' then
value = self.symbol -- always the same as sym_us for prefixed units
value = rawget(self, '_sym_us')
if value then
value = self.si_prefix .. value
else
value = self.symbol
end
elseif key == 'name1' then
elseif key == 'name1' then
value = prefixed_name(self, self._name1, 1)
-- prefix_position is a byte (not character) position, so use Lua's sub().
local pos = rawget(self, 'prefix_position') or 1
value = self._name1
value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
elseif key == 'name2' then
elseif key == 'name2' then
value = self.name1 .. plural_suffix
value = rawget(self, '_name2')
if value then
value = prefixed_name(self, value, 2)
else
value = self.name1 .. plural_suffix
end
elseif key == 'name1_us' then
elseif key == 'name1_us' then
value = rawget(self, '_name1_us')
value = rawget(self, '_name1_us')
if value then
if value then
local pos = rawget(self, 'prefix_position') or 1
value = prefixed_name(self, value, 3)
value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
else
else
value = self.name1
value = self.name1
end
end
elseif key == 'name2_us' then
elseif key == 'name2_us' then
if rawget(self, '_name1_us') then
value = rawget(self, '_name2_us')
if value then
value = prefixed_name(self, value, 4)
elseif rawget(self, '_name1_us') then
value = self.name1_us .. plural_suffix
value = self.name1_us .. plural_suffix
else
else
Line 575: Line 601:
result.sp_us = force_sp_us
result.sp_us = force_sp_us
if result.prefixes then
if result.prefixes then
result.symbol = result._symbol
result.si_name = ''
result.name1 = result._name1
result.si_prefix = ''
return true, setmetatable(result, unit_prefixed_mt)
result.name1_us = result._name1_us
end
end
return true, setmetatable(result, unit_mt)
return true, setmetatable(result, unit_mt)
Line 673: Line 699:
end
end
end
end
if not get_range(unitcode) then -- do not require extra if looking up a range word which cannot be a unit
if not extra_units then
if not extra_units then
local success, extra = pcall(function () return require(extra_module).extra_units end)
local success, extra = pcall(function () return require(extra_module).extra_units end)
if success and type(extra) == 'table' then
if success and type(extra) == 'table' then
extra_units = extra
extra_units = extra
end
end
end
if extra_units then
end
-- A unit in one data table might refer to a unit in the other table, so
if extra_units then
-- switch between them, relying on fails or depth to terminate loops.
-- A unit in one data table might refer to a unit in the other table, so
if not fails[unitcode] then
-- switch between them, relying on fails or depth to terminate loops.
if not fails[unitcode] then
fails[unitcode] = true
local other = (utable == all_units) and extra_units or all_units
fails[unitcode] = true
local success, result = lookup(unitcode, opt_sp_us, what, other, fails, depth)
local other = (utable == all_units) and extra_units or all_units
if success then
local success, result = lookup(unitcode, opt_sp_us, what, other, fails, depth)
return true, result
if success then
end
return true, result
end
end
end
end
if to_en_table then
-- At fawiki it is common to translate all digits so a unit like "km2" becomes "km۲".
local en_code = ustring.gsub(unitcode, '%d', to_en_table)
if en_code ~= unitcode then
return lookup(en_code, opt_sp_us, what, utable, fails, depth)
end
end
end
end
Line 806: Line 841:
return ''
return ''
end
end
local mid = (inout == (parms.opt_flip and 'out' or 'in')) and parms.mid or ''
local mid
if parms.opt_adjectival then
if want_name then
if inout == (parms.opt_flip and 'out' or 'in') then
if parms.opt_adjectival then
return '-' .. hyphenated(id) .. mid, true
mid = parms.mid
end
end
if want_name then
if parms.opt_add_s and id:sub(-1) ~= 's' then
return '-' .. hyphenated(id) .. (mid or ''), true
id = id .. 's' -- for nowiki
end
end
end
end
return sep .. id .. (mid or '')
return sep .. id .. mid
end
end


Line 1,538: Line 1,573:
-- Return true if successful or return false, t where t is an error message table.
-- Return true if successful or return false, t where t is an error message table.
if kv_pairs.adj and kv_pairs.sing then
if kv_pairs.adj and kv_pairs.sing then
-- For en.wiki (before translation), warn if attempt to use adj and sing
-- For enwiki (before translation), warn if attempt to use adj and sing
-- as the latter is a deprecated alias for the former.
-- as the latter is a deprecated alias for the former.
if kv_pairs.adj ~= kv_pairs.sing and kv_pairs.sing ~= '' then
if kv_pairs.adj ~= kv_pairs.sing and kv_pairs.sing ~= '' then
Line 1,605: Line 1,640:
local cfg_abbr = config.abbr
local cfg_abbr = config.abbr
if cfg_abbr then
if cfg_abbr then
-- Don't warn if invalid because every convert would show that warning.
if cfg_abbr == 'on always' then
if cfg_abbr == 'on always' then
parms.abbr = 'on'
parms.abbr = 'on'
elseif cfg_abbr == 'on default' then
elseif cfg_abbr == 'off always' then
if parms.abbr == nil then
parms.abbr = 'off'
elseif parms.abbr == nil then
if cfg_abbr == 'on default' then
parms.abbr = 'on'
parms.abbr = 'on'
elseif cfg_abbr == 'off default' then
parms.abbr = 'off'
end
end
end
end
Line 1,660: Line 1,700:
-- i = index to next entry in parms after those processed here
-- i = index to next entry in parms after those processed here
-- or return false, t where t is an error message table.
-- or return false, t where t is an error message table.
local ranges = text_code.ranges
local valinfo = collection() -- numbered table of input values
local valinfo = collection() -- numbered table of input values
local range = collection() -- numbered table of range items (having, for example, 2 range items requires 3 input values)
local range = collection() -- numbered table of range items (having, for example, 2 range items requires 3 input values)
Line 1,677: Line 1,716:
local success, result = extract_number(parms, valstr, i > 1)
local success, result = extract_number(parms, valstr, i > 1)
if not success and valstr and i < 20 then -- check i to limit abuse
if not success and valstr and i < 20 then -- check i to limit abuse
for _, sep in ipairs(ranges.words) do
for _, sep in ipairs(text_code.ranges.words) do
local start, stop = valstr:find(sep, 2, true) -- start at 2 to skip any negative sign for range '-'
local start, stop = valstr:find(sep, 2, true) -- start at 2 to skip any negative sign for range '-'
if start then
if start then
Line 1,701: Line 1,740:
valinfo:add(info)
valinfo:add(info)
local next = strip(parms[i])
local next = strip(parms[i])
local range_item = ranges.types[next] or ranges.types[ranges.aliases[next]]
local range_item = get_range(next)
if not range_item then
if not range_item then
break
break
Line 1,786: Line 1,825:
end
end
if parms.opt_ignore_error then -- display given unit code with no error (for use with {{val}})
if parms.opt_ignore_error then -- display given unit code with no error (for use with {{val}})
in_unit_table = nil
in_unit_table = '' -- suppress error message and prevent processing of output unit
end
end
in_unit_table = setmetatable({ symbol = in_unit, name2 = in_unit, utype = "length", scale = 1, bad_mcode = in_unit_table, default = "m" }, unit_mt)
in_unit_table = setmetatable({ symbol = in_unit, name2 = in_unit,
default = "m", defkey = "m", linkey = "m",
utype = "length", scale = 1, bad_mcode = in_unit_table }, unit_mt)
end
end
end
end
Line 1,845: Line 1,886:
end
end
if parms.opt_adj_mid then
if parms.opt_adj_mid then
parms.opt_adjectival = true
next = parms[i]
next = parms[i]
i = i + 1
i = i + 1
Line 2,369: Line 2,409:


local function variable_name(clean, unit_table)
local function variable_name(clean, unit_table)
-- For sl.wiki (Slovenian Wikipedia), a unit name depends on the value.
-- For slwiki (Slovenian Wikipedia), a unit name depends on the value.
-- Parameter clean is the unsigned rounded value in en digits, as a string.
-- Parameter clean is the unsigned rounded value in en digits, as a string.
-- Value Source Example for "m"
-- Value Source Example for "m"
Line 2,757: Line 2,797:
if composite then
if composite then
-- Simplify: assume there is no range, and no decoration.
-- Simplify: assume there is no range, and no decoration.
local mid = ''
local mid = (not parms.opt_flip) and parms.mid or ''
local sep1 = '&nbsp;'
local sep1 = '&nbsp;'
local sep2 = ' '
local sep2 = ' '
if parms.opt_adjectival then
if parms.opt_adjectival and want_name then
sep1 = '-'
if not parms.opt_flip then
mid = parms.mid or ''
sep2 = '-'
end
if want_name then
sep1 = '-'
sep2 = '-'
end
end
end
local parts = { first_unit.valinfo[1].show .. sep1 .. id1 }
local parts = { first_unit.valinfo[1].show .. sep1 .. id1 }
Line 2,936: Line 2,971:
not (abbr == 'on' or abbr == 'out' or abbr == 'mos')
not (abbr == 'on' or abbr == 'out' or abbr == 'mos')
local want_link = (parms.lk == 'on' or parms.lk == 'out')
local want_link = (parms.lk == 'on' or parms.lk == 'out')
local mid = ''
local mid = parms.opt_flip and parms.mid or ''
local sep1 = '&nbsp;'
local sep1 = '&nbsp;'
local sep2 = ' '
local sep2 = ' '
if parms.opt_adjectival then
if parms.opt_adjectival and want_name then
sep1 = '-'
if parms.opt_flip then
mid = parms.mid or ''
sep2 = '-'
end
if want_name then
sep1 = '-'
sep2 = '-'
end
end
end
local do_spell = parms.opt_spell_out
local do_spell = parms.opt_spell_out
Line 2,974: Line 3,004:
decimals = ''
decimals = ''
else
else
decimals = (outinfo.show):match('[' .. numdot .. '](.*)') or '' -- outinfo.show is in local language
local show = outinfo.show -- number as a string in local language
local p1, p2 = show:find(numdot, 1, true)
decimals = p1 and show:sub(p2 + 1) or '' -- text after numdot, if any
end
end
fmt = '%.' .. ulen(decimals) .. 'f' -- to reproduce precision
fmt = '%.' .. ulen(decimals) .. 'f' -- to reproduce precision
Line 3,003: Line 3,035:
id = variable_name(clean, out_current)
id = variable_name(clean, out_current)
else
else
id = out_current[(thisvalue == 1) and 'name1' or 'name2']
local key = 'name2'
if parms.opt_adjectival then
key = 'name1'
elseif tfrac then
if thisvalue == 0 then
key = 'name1'
end
elseif parms.opt_singular then
if 0 < thisvalue and thisvalue < 1.0001 then
key = 'name1'
end
else
if thisvalue == 1 then
key = 'name1'
end
end
id = out_current[key]
end
end
else
else
Line 3,162: Line 3,210:
local wikitext
local wikitext
if bad_input_mcode then
if bad_input_mcode then
if bad_input_mcode == '' then
wikitext = parts[1] .. message(bad_input_mcode)
wikitext = parts[1]
else
wikitext = parts[1] .. message(bad_input_mcode)
end
elseif parms.table_joins then
elseif parms.table_joins then
wikitext = parms.table_joins[1] .. parts[1] .. parms.table_joins[2] .. parts[2]
wikitext = parms.table_joins[1] .. parts[1] .. parms.table_joins[2] .. parts[2]