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 |
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 " |
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 |
if numsep_remove ~= '' then |
||
text = text:gsub( |
text = text:gsub(numsep_remove, '') |
||
end |
end |
||
if numdot ~= '.' then |
if numdot ~= '.' then |
||
text = text:gsub( |
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 = |
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 |
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 |
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 |
||
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 |
||
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. |
result.si_name = '' |
||
result. |
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. |
|||
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 |
if want_name then |
||
if |
if parms.opt_adjectival then |
||
return '-' .. hyphenated(id) .. mid, true |
|||
mid = parms.mid |
|||
end |
end |
||
if |
if parms.opt_add_s and id:sub(-1) ~= 's' then |
||
id = id .. 's' -- for nowiki |
|||
end |
end |
||
end |
end |
||
return sep .. id .. |
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 |
-- 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 == ' |
elseif cfg_abbr == 'off always' 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 = |
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 = |
in_unit_table = '' -- suppress error message and prevent processing of output unit |
||
end |
end |
||
in_unit_table = setmetatable({ symbol = in_unit, name2 = in_unit, |
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 |
-- 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 = ' ' |
local sep1 = ' ' |
||
local sep2 = ' ' |
local sep2 = ' ' |
||
if parms.opt_adjectival then |
if parms.opt_adjectival and want_name then |
||
sep1 = '-' |
|||
if not parms.opt_flip then |
|||
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 = ' ' |
local sep1 = ' ' |
||
local sep2 = ' ' |
local sep2 = ' ' |
||
if parms.opt_adjectival then |
if parms.opt_adjectival and want_name then |
||
sep1 = '-' |
|||
if parms.opt_flip then |
|||
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 |
||
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 |
||
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] |