Module:Convert/makeunits: Difference between revisions

Content deleted Content added
fix so {{#invoke:convert/makeunits|makeunits}} uses nowiki in pre tags; tweak error messages; fix evaluate issue
accept abbr=off for an alias so it uses unit name; add pernames (names for second unit in a per) for ukwiki
 
(23 intermediate revisions by 4 users not shown)
Line 1:
-- This module generates the wiktextwikitext required at [[Module:Convert/data]]
-- by reading and processing the wikitext of the unitmaster definitionslist atof units
-- (see conversion_data for the page title).
-- [[Module:Convert/documentation/conversion data/doc]].
--
-- Usage: Put the following line (with nothing else) in a sandbox:
-- {{#invoke:convert/makeunits|makeunits}}
-- Previewing the sandbox should display the required wikitext.
--
-- It is also possible to use the following where PAGE is replaced
-- with the title of the page holding the unit definitions.
-- {{#invoke:convert/makeunits|makeunits|PAGE}}
--
-- LATER:
-- If support additional currencies, need to update table:
-- local currency = { ['$'] = true, ['£'] = true }
--
-- Script method:
Line 74 ⟶ 62:
-- ...
-- }
 
local ulower = mw.ustring.lower
local usub = mw.ustring.sub
local text_code
 
local specials = {
-- This table is used to add extra fields when defining some units which
-- require exceptions to normal processing.
-- Each key is in the local language, while each value is fixed text,.
-- howeverHowever, this script isshould NOT be edited to localize the keys. Instead,
-- Instead, the translation_table --in [[Module:Convert/text]] iscan be edited, and this script replaces sections
-- and this script --will replace sections of the following with localized definitions, if given.
-- definitions from Module:Convert/text, if given.
-- LATER: It would be better if this was defined in the conversions table.
-- Ask for assistance at [[:en:Module talk:Convert]].
utype = {
-- LATER: It would be better if this was defined in the conversion data.
-- ["unit type in local language"] = "name_used_in_this_script"
utype = {
["fuel efficiency"] = "type_fuel_efficiency",
-- ["unit type in local language"] = "name_used_in_this_script"
["length"] = "type_length",
["fuel efficiency"] = "type_fuel_efficiency",
["temperature"] = "type_temperature",
["volumelength"] = "type_volumetype_length",
["temperature"] = "type_temperature",
},
["volume"] = "type_volume",
ucode = {
},
exception = {
ucode = {
-- ["unit code in local language"] = "name_used_in_module_convert"
exception = {
["ft"] = "integer_more_precision",
-- ["unit code in local language"] = "name_used_in_module_convert"
["in"] = "subunit_more_precision",
["ft"] = "integer_more_precision",
},
["in"] = "subunit_more_precision",
istemperature = {
["lb"] = "integer_more_precision",
-- Common temperature scales (not keVT or MK).
},
-- ["unit code in local language"] = 1
istemperature = {
["C"] = true,
-- Common temperature scales (not keVT or MK).
["F"] = true,
-- ["unit code in local ["Klanguage"] = true,
["RC"] = true,
["F"] = true,
},
["K"] = true,
usesymbol = {
["R"] = true,
-- Use unit symbol not name if abbr not specified.
},
-- ["unit code in local language"] = 1
usesymbol = {
["C"] = 1,
-- Use unit symbol not name if abbr not specified.
["F"] = 1,
-- ["unit code in local ["Klanguage"] = 1,
["RC"] = 1,
["C-changeF"] = 1,
["F-changeK"] = 1,
["K-changeR"] = 1,
["C-change"] = 1,
},
["F-change"] = 1,
alttype = {
["K-change"] = 1,
-- Unit has an alternate type that is valid conversion.
},
-- ["unit code in local language"] = "alternate type in local language"
alttype = {
["Nm"] = "energy",
-- Unit has an alternate type that is a valid conversion.
["ftlb"] = "torque",
-- ["unit code in local language"] = "alternate type in local language"
["ftlb-f"] = "torque",
["ftlbfNm"] = "torqueenergy",
["inlbftlb"] = "torque",
["inlbftlb-f"] = "torque",
["inlbfftlbf"] = "torque",
["inoz-finlb"] = "torque",
["inozfinlb-f"] = "torque",
["inlbf"] = "torque",
},
["inoz-f"] = "torque",
},
["inozf"] = "torque",
},
},
}
 
-- Module text for the local language (localization).
-- When a number is formatted as a string, the result can indicate
-- A default table of text for enwiki is provided here.
-- that the number is not valid. For example, on some Lua installs,
-- If needed for another wiki, wanted sections from the table can be
-- tostring(1/0) includes "#INF", and tostring(0/0) includes "#IND".
-- copied into translation_table in Module:Convert/text.
-- On Scribunto, the results are "inf" and "nan" (each includes "n").
-- For example, copying and modifying only the titles section may give:
-- A testing program can set the global variable is_test_run.
--
local bad_number_char = is_test_run and '#' or 'n'
-- local translation_table = {
-- ... -- other items
-- mtext = {
-- titles = {
-- -- name_used_in_this_script = 'Title of page'
-- conversion_data = 'Modul:Convert/documentation/conversion data/dok',
-- },
-- },
-- }
local mtext = {
section_names = {
-- name_used_in_this_script = 'Section title used in conversion data'
overrides = 'Overrides',
conversions = 'Conversions',
outmultiples = 'Output multiples',
combinations = 'Combinations',
inmultiples = 'Input multiples',
defaults = 'Defaults',
links = 'Links',
perunits = 'Automatic per units',
varnames = 'Variable names',
pernames = 'Names for second unit in a per',
},
titles = {
-- name_used_in_this_script = 'Title of page'
conversion_data = 'Module:Convert/documentation/conversion data',
},
messages = {
-- name_used_in_this_script = 'Error message ($1 = first parameter, $2 = second)'
m_als_bad = 'Alias has invalid text in field "$1".',
m_als_dup = 'Alias "$1" already defined.',
m_als_link = 'Alias "$1" must include a wikilink ("[[...]]") in the symlink text.',
m_als_mul = 'Alias "$1" has multiplier "$2" which is not a number.',
m_als_same = 'Should omit "$1" for alias "$2" because it is the same as its target.',
m_als_type = 'Target of alias "$1" has wrong type.',
m_als_undef = 'Primary unit must be defined before alias "=$1"',
m_cmb_miss = 'Missing unit code for a combination.',
m_cmb_none = 'No units specified for combination "$1"',
m_cmb_one = 'Only one unit specified for combination "$1"',
m_cmb_type = 'Unit "$1" in combination "$2" has wrong type.',
m_cmb_undef = 'Unit "$1" in combination "$2" not defined.',
m_cmp_def = 'Composite "$1" must specify a default unit code.',
m_cmp_int = 'Composite "$1" has components where scale ratios are not integers.',
m_cmp_inval = 'Composite "$1" has a component with an invalid scale, "$2".',
m_cmp_many = 'Composite "$1" has too many fields.',
m_cmp_miss = 'Missing unit code for a composite.',
m_cmp_order = 'Composite "$1" has components in wrong order or with invalid scales.',
m_cmp_scale = 'Alternate unit "$1" in composite "$2" has wrong scale.',
m_cmp_two = 'Composite "$1" must specify exactly two unit codes.',
m_cmp_type = 'Unit "$1" in composite "$2" has wrong type.',
m_cmp_undef = 'Unit "$1" in composite "$2" not defined.',
m_def_cond = 'Invalid condition in default "$1" for unit "$2".',
m_def_fmt = 'Default output "$1" for unit "$2" should have 2 or 3 "!".',
m_def_rpt = 'Default output "$1" for unit "$2" is repeated.',
m_def_same = 'Default output for unit "$1" is the same unit.',
m_def_type = 'Default output "$1" for unit "$2" has wrong type.',
m_def_undef = 'Default output "$1" for unit "$2" is not defined.',
m_dfs_code = 'Defaults section: no unit code specified.',
m_dfs_dup = 'Defaults section: unit "$1" has already been specified.',
m_dfs_none = 'Defaults section: unit "$1" has no default specified.',
m_dfs_sym = 'Defaults section: unit "$1" must have a symbol.',
m_dfs_two = 'Defaults section: unit "$1" should have two fields only.',
m_dfs_undef = 'Defaults section: unit "$1" is not defined.',
m_dup_code = 'Unit code "$1" has already been defined.',
m_error = 'Error:',
m_ftl_read = 'Could not read wikitext from "[[$1]]".',
m_ftl_table = '[[$1]] should export table "$2".',
m_ftl_type = 'Fatal error: unknown data type for "$1"',
m_hdg_lev2 = 'Level 2 heading "$1" not found.',
m_hdg_lev3 = 'No level 3 heading before: $1',
m_line_num = ' (line $1).',
m_lnk_brack = 'Link "$1" has wrong number of brackets.',
m_lnk_dup = 'Link exception "$1" is already defined.',
m_lnk_miss = 'Missing unit code for a link.',
m_lnk_none = 'No link defined for unit "$1".',
m_lnk_sym = 'Unit code "$1" for a link must have a symbol.',
m_lnk_two = 'Row for unit "$1" link should have two fields only.',
m_lnk_type = 'Link exception "$1" has wrong type.',
m_lnk_undef = 'Unit code "$1" for a link is not defined.',
m_miss_code = 'Missing unit code.',
m_miss_sym = 'Missing symbol.',
m_miss_type = 'Missing unit type.',
m_mul_int = 'Multiple "$1" has components where scale ratios are not integers.',
m_mul_miss = 'Missing unit code for a multiple.',
m_mul_none = 'No units specified for multiple "$1"',
m_mul_one = 'Only one unit specified for multiple "$1"',
m_mul_order = 'Multiple "$1" has components in wrong order or with invalid scales.',
m_mul_scale = 'Multiple "$1" has a component with an invalid scale, "$2".',
m_mul_std = 'Unit "$1" in multiple "$2" must be a standard unit.',
m_mul_type = 'Unit "$1" in multiple "$2" has wrong type.',
m_mul_undef = 'Unit "$1" in multiple "$2" not defined.',
m_no_title = 'Need title of page with unit definitions.',
m_ovr_dup = 'Override "$1" is already defined.',
m_ovr_miss = 'Missing unit code for an override.',
m_per_dup = 'Per unit "$1" already defined.',
m_per_empty = 'Unit "$1" has an empty field in the "per".',
m_per_fuel = 'Unit "$1" has invalid unit types for fuel efficiency.',
m_per_inv = 'Invalid field for a "per".',
m_per_two = 'Unit "$1" does not have exactly 2 fields in the "per".',
m_per_undef = 'Unit "$1" has undefined unit code "$2" in the "per".',
m_percent_s = 'Field "$1" must not contain "%s".',
m_pnm_cnt = 'Names for second unit in a per section: each row must have two columns.',
m_pnm_dup = 'Unit "$1" already has a per name.',
m_pnm_miss = 'Missing field for a per name.',
m_pnm_undef = 'Unit "$1" in per names is not defined.',
m_pfx_bad = 'Unknown prefix: "$1".',
m_pfx_name = 'Unit with Prefix set must include Name.',
m_scl_bad = 'Scale expression is invalid: "$1".',
m_scl_miss = 'Missing scale.',
m_scl_oflow = 'Scale expression gives an invalid value: "$1".',
m_var_cnt = 'Variable names section: each row must have the configured number of columns.',
m_var_dup = 'Unit "$1" already has a variable name.',
m_var_miss = 'Missing field for a variable name.',
m_var_undef = 'Unit "$1" in variable names is not defined.',
m_warning = 'Warning:',
m_wrn_more = ' (and more not shown)',
m_wrn_nbsp = 'Line $1 contains a nonbreaking space.',
m_wrn_nodef = 'Units with the following unit codes have no default output.',
m_wrn_ucode = ' $1',
},
}
 
local function message(key, ...)
-- Return a message from the message table, which can be localized.
-- '$1', '$2', ... are replaced with the first, second, ... parameters,
-- each of which must be a string or a number.
-- The global variable is_test_run can be set by a testing program to
-- check the messages generated by this program.
local rep = {}
for i, v in ipairs({...}) do
rep['$' .. i] = v
end
key = key or '???'
local extra
if is_test_run and key ~= 'm_line_num' then
extra = key .. ': '
else
extra = ''
end
return extra .. string.gsub(mtext.messages[key] or key, '$%d+', rep)
end
 
local function quit(key, ...)
-- Use error() to pass an error message to the surrounding pcall().
error(message(key, ...), 0)
end
 
local function quit_no_message()
-- Throw an error.
-- This is used in some functions which can throw an error with a message,
-- but where the message is in fact never displayed because the calling
-- function uses pcall to catch errors, and any message is ignored.
-- Using this function documents that the message (which may be useful in
-- some other application) does not need translation as it never appears.
error('this message is not displayed', 0)
end
 
local function collection()
-- Return a table to hold items.
return {
n = 0,
add = function (self, item)
self.n = self.n + 1
self[self.n] = item
end,
pop = function (self, item)
if self.n > 0 then
local top = self[self.n]
self.n = self.n - 1
return top
end
end
end,
join = function (self, sep)
return table.concat(self, sep or '\n')
end,
}
}
end
 
local warnings = collection()
local function add_warning(textkey, ...)
-- Add a warning that will be inserted abovebefore the final result.
warnings:add(textmessage(key, ...))
end
 
Line 172 ⟶ 323:
 
local operators = {
['+'] = { precedence = 1, associativity = 1, func = function (a, b) return a + b end },
['-'] = { precedence = 1, associativity = 1, func = function (a, b) return a - b end },
['*'] = { precedence = 2, associativity = 1, func = function (a, b) return a * b end },
['/'] = { precedence = 2, associativity = 1, func = function (a, b) return a / b end },
['^'] = { precedence = 3, associativity = 2, func = function (a, b) return a ^ b end },
['('] = '(',
[')'] = ')',
}
 
local function tokenizer(text)
-- Function 'next' returns the next token which is one of:
-- number
-- table (operator)
-- string ('(' or ')')
-- nil (end of text)
-- If invalid, an error is thrown.
-- The number is unsigned (unary operators are not supported).
return {
pos = 1,
maxpos = #text,
text = text,
next = function(self)
if self.pos <= self.maxpos then
local p1, p2, hit = self.text:find('^%s*([+%-*/^()])', self.pos)
if hit then
self.pos = p2 + 1
return operators[hit]
end
end
p1, p2, hit = self.text:find('^%s*(%d*%.?%d*[eE][+-]?%d*)', self.pos)
if not hit then
p1, p2, hit = self.text:find('^%s*(%d*%.?%d*)', self.pos)
end
end
local value = tonumber(hit)
if value then
self.pos = p2 + 1
return value
end
end
error quit_no_message('invalid number "' .. self.text:sub(self.pos) .. '"', 0)
end
end
end
}
}
end
 
local function evaluate_tokens(tokens, inparens)
-- Return the value from evaluating tokenized expression, or throw an error.
local numstack, opstack = collection(), collection()
local function perform_ops(precedence, associativity)
while opstack.n > 0 and (opstack[opstack.n].precedence > precedence or
(opstack[opstack.n].precedence == precedence and associativity == 1)) do
local rhs = numstack:pop()
local lhs = numstack:pop()
if not (rhs and lhs) then errorquit_no_message('missing number', 0) end
local op = opstack:pop()
numstack:add(op.func(lhs, rhs))
end
end
local token_last
local function set_state(token_type)
if token_last == token_type then
local missing = (token_type == 'number') and 'operator' or 'number'
error quit_no_message('missing ' .. missing, 0)
end
token_last = token_type
end
while true do
local token = tokens:next()
if type(token) == 'number' then
set_state('number')
numstack:add(token)
elseif type(token) == 'table' then
set_state('operator')
perform_ops(token.precedence, token.associativity)
opstack:add(token)
elseif token == '(' then
set_state('number')
numstack:add(evaluate_tokens(tokens, true))
elseif token == ')' then
if inparens then
break
end
end
error quit_no_message('unbalanced parentheses', 0)
else
break
end
end
perform_ops(0)
if numstack.n > 1 then errorquit_no_message('missing operator', 0) end
if numstack.n < 1 then errorquit_no_message('missing number', 0) end
return numstack:pop()
end
 
local function evaluate(expression)
-- Return value (a number) from evaluating expression (a string),
-- or throw an error if invalid.
-- This is not bullet proof, but it should support the expressions used.
return evaluate_tokens(tokenizer(expression))
end
---End code to evaluate expressions-------------------------------------
---Begin code adapted from Module:Convert-------------------------------
 
local plural_suffix = 's' -- may be changed from translation.plural_suffix below
local SIprefixes, eng_scales
local plural_suffix = 's'
 
local function shallow_copy(t)
-- Return a shallow copy of t.
-- Do not need the features and overhead of mw.clone() provided by Scribunto.
local result = {}
for k, v in pairs(t) do
result[k] = v
end
return result
end
 
local function split(text, delimiter)
-- Return a numbered table with fields from splitting text.
-- The delimiter is used in a regex without escaping (for example, '.' would fail).
-- Each field has any leading/trailing whitespace removed.
local t = {}
text = text .. delimiter -- to get last item
for item in text:gmatch('%s*(.-)%s*' .. delimiter) do
table.insert(t, item)
end
return t
end
 
local unit_mt = {
-- Metatable to get missing values for a unit that does not accept SI prefixes,.
-- Warning: The boolean value 'false' is returned for any missing field
-- or a for a unit that accepts prefixes but where no prefix was used.
-- so __index is not called twice for the same field in a given unit.
-- In the latter case, and before use, fields symbol, name1, name1_us
__index = function (self, key)
-- must be set from _symbol, _name1, _name1_us respectively.
local value
__index = function (self, key)
if key == 'name1' or key == 'sym_us' then
local value
value = self.symbol
if key == 'name1' or key == 'sym_us' then
elseif key == 'name2' then
value = self.symbol
value = self.name1 .. plural_suffix
elseif key == 'name2' then
elseif key == 'name1_us' then
value = self.name1 .. plural_suffix
value = self.name1
elseif key == 'name1_us' then
if not rawget(self, 'name2_us') then
value = self.name1
-- If name1_us is 'foot', do not make name2_us by appending plural_suffix.
if not rawget(self, 'name2_us') then
self.name2_us = self.name2
-- If name1_us is 'foot', do not make name2_us by appending plural_suffix.
end
self.name2_us = self.name2
elseif key == 'name2_us' then
end
local raw1_us = rawget(self, 'name1_us')
elseif key == 'name2_us' then
if raw1_us then
local raw1_us = rawget(self, 'name1_us')
value = raw1_us .. plural_suffix
if raw1_us then
else
value = raw1_us .. plural_suffix
value = self.name2
else
end
value = self.name2
elseif key == 'link' then
end
value = self.name1
elseif key == 'link' then
else
value = self.name1
value = false
else
end
return nil
rawset(self, key, value)
end
return value
rawset(self, key, value)
end
return value
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 = {
-- Metatable to get missing values for a unit that accepts SI prefixes,.
-- Before use, fields si_name, si_prefix must be defined.
-- and where a prefix has been used.
-- The unit must define _symbol, _name1 and
-- Before use, fields si_name, si_prefix must be defined.
-- may define _sym_us, _name1_us, _name2_us
__index = function (self, key)
-- (_sym_us, _name2_us may be defined for a language using sp=us
local value
-- to refer to a variant unrelated to U.S. units).
if key == 'symbol' then
__index = function (self, key)
value = self.si_prefix .. self._symbol
local value
elseif key == 'sym_us' then
if key == 'symbol' then
value = self.symbol -- always the same as sym_us for prefixed units
value = self.si_prefix .. self._symbol
elseif key == 'name1' then
elseif key == 'sym_us' then
local pos = rawget(self, 'prefix_position') or 1
value = rawget(self._name1, '_sym_us')
if value then
value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
value = self.si_prefix .. value
elseif key == 'name2' then
else
value = self.name1 .. plural_suffix
value = self.symbol
elseif key == 'name1_us' then
end
value = rawget(self, '_name1_us')
elseif key == if value'name1' then
value = prefixed_name(self, self._name1, 1)
local pos = rawget(self, 'prefix_position') or 1
elseif key == 'name2' then
value = value:sub(1, pos - 1) .. self.si_name .. value:sub(pos)
value = rawget(self, '_name2')
else
if value = self.name1then
value = prefixed_name(self, value, 2)
end
else
elseif key == 'name2_us' then
value = self.name1 .. plural_suffix
if rawget(self, '_name1_us') then
end
value = self.name1_us .. plural_suffix
elseif key == 'name1_us' then
else
value = rawget(self.name2, '_name1_us')
if value then
end
value = prefixed_name(self, value, 3)
elseif key == 'link' then
else
value = self.name1
value = self.name1
else
end
return nil
elseif key == 'name2_us' then
end
value = rawsetrawget(self, key, value'_name2_us')
if value then
return value
value = prefixed_name(self, value, 4)
end
elseif rawget(self, '_name1_us') then
value = self.name1_us .. plural_suffix
else
value = self.name2
end
elseif key == 'link' then
value = self.name1
else
value = false
end
rawset(self, key, value)
return value
end
}
 
local function lookup(units, unitcode, sp, what)
-- Return a copy of the unit if found, or return nil.
-- In this cut-down code, sp is always nil, and what is ignored.
local t = units[unitcode]
if t then
if t.shouldbe then
return nil
end
local result = shallow_copy(t)
if result.prefixes then
result.symbolsi_name = result._symbol''
result.name1si_prefix = result._name1''
return setmetatable(result, unit_prefixed_mt)
result.name1_us = result._name1_us
end
return setmetatable(result, unit_mt)
end
local SIprefixes = text_code.SIprefixes
for plen = 2, 1, -1 do
for plen = SIprefixes[1] or 2, 1, -1 do
-- Look for an SI prefix; should never occur with an alias.
-- CheckLook for longeran SI prefix; should never firstoccur ('dam'with isan decametre)alias.
-- Check for longer prefix first ('dam' is decametre).
-- Micro (µ) is two bytes in utf-8, so is found with plen = 2.
-- SIprefixes[1] = prefix maximum #characters (as seen by mw.ustring.sub).
local prefix = string.sub(unitcode, 1, plen)
local prefix = usub(unitcode, 1, plen)
local si = SIprefixes[prefix]
local si = SIprefixes[prefix]
if si then
if si then
local t = units[unitcode:sub(plen+1)]
local t = units[usub(unitcode, plen+1)]
if t and t.prefixes then
if t and t.prefixes then
local result = shallow_copy(t)
local result = shallow_copy(t)
if (sp == 'us' or t.sp_us) and si.name_us then
if (sp == 'us' or resultt.si_namesp_us) =and si.name_us then
result.si_name = si.name_us
else
else
result.si_name = si.name
result.si_name = si.name
end
end
result.si_prefix = si.prefix or prefix
result.si_prefix = si.prefix or prefix
-- In this script, each scale is a string.
-- In this script, each scale is a string.
result.scale = tostring(tonumber(t.scale) * 10 ^ (si.exponent * t.prefixes))
result.prefixes = nil -- a prefixed unit does not take more prefixes (in this script, the returned unit may be added to the list of units)
return setmetatable(result, unit_prefixed_mt)
end
end
end
end
local exponent, baseunit = unitcode:match('^e(%d+)(.*)')
if exponent then
local engscale = text_code.eng_scales[exponent]
if engscale then
local result = lookup(units, baseunit, sp, 'no_combination')
if not result then return nil end
if not (result.offset or result.builtin ifor result.engscale == nil) then
result.defkey = unitcode -- key to lookup default exception
result.engscale.exponent = exponentengscale
-- Do not set result.scale as this code is called for units where that is not set.
result.engscale = engscale
return result
-- Do not set result.scale as this code is called for units where that is not set.
end
return result
end
end
end
return nil
end
return nil
end
 
local function evaluate_condition(value, condition)
-- Return true or false from applying a conditional expression to value,
-- or throw an error if invalid.
-- A very limited set of expressions is supported:
-- v < 9
-- v * 9 < 9
-- where
-- 'v' is replaced with value
-- 9 is any number (as defined by Lua tonumber)
-- '<' can also be '<=' or '>' or '>='
-- In addition, the following form is supported:
-- LHS and RHS
-- where
-- LHS, RHS = any of above expressions.
local function compare(value, text)
local arithop, factor, compop, limit = text:match('^%s*v%s*([*]?)(.-)([<>]=?)(.*)$')
if arithop == nil then
error quit_no_message('Invalid default expression.', 0)
elseif arithop == '*' then
factor = tonumber(factor)
if factor == nil then
error quit_no_message('Invalid default expression.', 0)
end
end
value = value * factor
end
limit = tonumber(limit)
if limit == nil then
error quit_no_message('Invalid default expression.', 0)
end
if compop == '<' then
return value < limit
elseif compop == '<=' then
return value <= limit
elseif compop == '>' then
return value > limit
elseif compop == '>=' then
return value >= limit
end
error quit_no_message('Invalid default expression.', 0) -- should not occur
end
local lhs, rhs = condition:match('^(.-%W)and(%W.*)')
if lhs == nil then
return compare(value, condition)
end
return compare(value, lhs) and compare(value, rhs)
end
 
---End code adapted from Module:Convertcode-----------------------------------------------------
 
local function strip(text)
-- Return text with no leading/trailing whitespace.
return text:match("^%s*(.-)%s*$")
end
 
local function empty(text)
-- Return true if text is nil or empty (assuming a string).
return text == nil or text == ''
end
 
Line 480 ⟶ 668:
local per_index = {} -- all "per" units (to detect attempts to define more than once)
 
local function get_unit(ucode, utype)
-- Look up unit code in our cache of units.
-- If utype == nil, the unit should already have been defined.
if empty(ucode) then
-- Otherwise, ucode may represent an automatically generated combination
return nil
-- where each component must have the given utype; a dummy unit is returned.
end
if empty(ucode) then
return lookup(units_index, ucode)
return nil
end
local unit = lookup(units_index, ucode)
if unit or not utype then
return unit
end
local combo = collection()
if ucode:find('+', 1, true) then
for item in (ucode .. '+'):gmatch('%s*(.-)%s*%+') do
if item ~= '' then
combo:add(item)
end
end
elseif ucode:find('%s') then
for item in ucode:gmatch('%S+') do
combo:add(item)
end
end
if combo.n > 1 then
local result = setmetatable({ utype = utype }, {
__index = function (self, key)
error('Bug: invalid use of automatically generated unit')
end })
for _, v in ipairs(combo) do
local component = lookup(units_index, v)
if not component or component.shouldbe or component.combination then
return nil
end
if utype ~= component.utype then
result.utype = component.utype -- set wrong type which caller will detect
break
end
end
return result
end
end
 
Line 491 ⟶ 714:
 
local function insert_unique_unit(data, unit, index)
-- After inserting any required built-in data, insert the unit into the
-- data table and (if index not nil) add to index,
-- but not if the unit code is already defined.
local ucode = unit.unitcode
local known = get_unit(ucode)
if known and not overrides[ucode] then
quit('m_dup_code', ucode)
error('Unit code "' .. ucode .. '" has already been defined.', 0)
end
for item, t in pairs(specials.ucode) do
unit[item] = t[ucode]
end
if index then
index[ucode] = unit
end
table.insert(data, unit)
end
 
local function check_condition(condition)
-- Return true if condition appears to be valid; otherwise return false.
for _, value in ipairs({ 0, 0.1, 1, 1.1, 10, 100, 1000, 1e4, 1e5 }) do
local success, result = pcall(evaluate_condition, value, condition)
if not success then
return false
end
end
return true
end
 
local function check_default_expression(default, ucode)
-- Return a numbered table of names present in param default
-- (two names if an expression, or one name (param default) otherwise).
-- Throw an error if a problem occurs.
-- An expression uses pipe-delimited fields with 'v' representing
-- the input value for the conversion.
-- Example (suffix is optional): 'v < 120 ! small ! big ! suffix'
-- returns { 'smallsuffix', 'bigsuffix' }.
if ifnot default:find('!', 1, true) == nil then
return { default }
end
local t = {}
for item in (default .. '!'):gmatch('%s*(.-)%s*!') do
t[#t+1] = item -- split on '!', removing leading/trailing whitespace
end
if not (#t == 3 or #t == 4) then
quit('m_def_fmt', default, ucode)
error('Default output "' .. default .. '" for unit "' .. ucode .. '" should have 2 or 3 "!".', 0)
end
local condition, default1, default2 = t[1], t[2], t[3]
if #t == 4 then
default1 = default1 .. t[4]
default2 = default2 .. t[4]
end
if not check_condition(condition) then
quit('m_def_cond', default, ucode)
error('Invalid condition in default "' .. default .. '" for unit "' .. ucode .. '".', 0)
end
return { default1, default2 }
end
 
local function check_default(default, ucode, utype, unit_table)
-- Check the given name (or expression) of a default output.
-- Normally a unit must not define itself as its default. However,
-- Throw an error if a problem occurs.
-- some units are defined merely for use in per units, and they have
local done = {}
-- the same ucode, utype and default.
for _, default in ipairs(check_default_expression(default, ucode)) do
-- Example: unit cent which cannot be converted to anything other than
if done[default] then
-- a cent, but which can work, for example, in cent/km and cent/mi.
error('Default output "' .. default .. '" for unit "' .. ucode .. '" is repeated.', 0)
-- Throw an error if a problem occurs.
end
local done = {}
if default == ucode then
for _, default in ipairs(check_default_expression(default, ucode)) do
error('Default output for unit "' .. ucode .. '" is the same unit.', 0)
if done[default] then
end
quit('m_def_rpt', default, ucode)
local default_table = get_unit(default)
end
if default_table == nil then
if default == ucode and ucode ~= utype then
error('Default output "' .. default .. '" for unit "' .. ucode .. '" is not defined.', 0)
quit('m_def_same', ucode)
end
end
if not (utype == unit_table.utype and utype == default_table.utype) then
local default_table = get_unit(default, utype)
error('Default output "' .. default .. '" for unit "' .. ucode .. '" has wrong type.', 0)
if not default_table then
end
quit('m_def_undef', default, ucode)
done[default] = true
end
if not (utype == unit_table.utype and utype == default_table.utype) then
quit('m_def_type', default, ucode)
end
done[default] = true
end
end
 
local function check_all_defaults(unitscfg, maxerrorsunits)
-- Check each default in units and warn if needed.
-- This is done after all input data has been processed.
-- Throw an error if a problem occurs.
local errors = collection()
local missing = collection() -- unitcodes with missing defaults
for _, unit in ipairs(units) do
if ifnot unit.shouldbe ==and nil andnot unit.combination == nil then
-- This is a standard unit or an alias/per (not shouldbe, combo).
-- An alias may have a default defined, but it is optional.
local default = unit.default
local ucode = unit.unitcode
if empty(default) then
if ifnot unit.target == nil then -- unit should have a default
missing:add(ucode)
end
end
else
local ok, msg = pcall(check_default, default, ucode, unit.utype, unit)
if not ok then
errors:add(msg)
if errors.n >= cfg.maxerrors then
break
break
end
end
end
end
end
end
end
end
if errors.n > 0 then
error(errors:join(), 0)
end
if missing.n > 0 then
add_warning('m_wrn_nodef')
add_warning('Units with the following unit codes have no default output.')
local limit = cfg.maxerrors
for _, v in ipairs(missing) do
limit = limit - 1
if limit < 0 then
add_warning(' (and more not shown)m_wrn_more')
break
end
end
add_warning(' m_wrn_ucode' .., v)
end
end
end
 
local function check_all_pers(unitscfg, maxerrorsunits)
-- Check each component of each "per" unit and warn if needed.
-- In addition, add any required extra fields for some types of units.
-- This is done after all input data has been processed.
-- Throw an error if a problem occurs.
local errors = collection()
local currency = { ['$'] = true, ['£'] = true }
local errorsfunction =errmsg(key, collection(...)
errors:add(message(key, ...))
local function errmsg(text)
end
errors:add(text)
for _, unit in ipairs(units) do
end
local per = unit.per
for _, unit in ipairs(units) do
if per then
local per = unit.per
local ucode = unit.unitcode
if per then
if #per ~= 2 then
local ucode = unit.unitcode
errmsg('m_per_two', ucode)
if #per ~= 2 then
else
errmsg('Unit "' .. ucode .. '" does not have exactly 2 fields in the "per".')
local types = {}
else
for i, v in ipairs(per) do
local types = {}
if empty(v) then
for i, v in ipairs(per) do
errmsg('m_per_empty', ucode)
if empty(v) then
end
errmsg('Unit "' .. ucode .. '" has an empty field in the "per".')
if not text_code.currency[v] then
end
local t = get_unit(v)
if not currency[v] then
if t then
local t = get_unit(v)
types[i] = t.utype
if t then
else
types[i] = t.utype
errmsg('m_per_undef', ucode, v)
else
end
errmsg('Unit "' .. ucode .. '" has undefined unit code "' .. v .. '" in the "per".')
end
end
end
end
if specials.utype[unit.utype] == 'type_fuel_efficiency' then
end
local expected = { type_volume = 1, type_length = 2 }
if specials.utype[unit.utype] == 'type_fuel_efficiency' then
local top_type = expected[specials.utype[types[1]]]
local expected = { type_volume = 1, type_length = 2 }
local top_typebot_type = expected[specials.utype[types[12]]]
if top_type and bot_type and top_type ~= bot_type then
local bot_type = expected[specials.utype[types[2]]]
unit.iscomplex = true
if top_type and bot_type and top_type ~= bot_type then
if top_type == 1 then
unit.iscomplex = true
unit.invert = 1
if top_type == 1 then
else
unit.invert = 1
unit.invert = -1
else
end
unit.invert = -1
else
end
errmsg('m_per_fuel', ucode)
else
end
errmsg('Unit "' .. ucode .. '" has invalid unit types for fuel efficiency.')
end
end
end
end
end
end
if errors.n >= cfg.maxerrors then
end
break
if errors.n >= maxerrors then
end
break
end
if errors.n > 0 then
end
if error(errors.n >:join(), 0 then)
end
error(errors:join(), 0)
end
end
 
local function update_units(units, composites, varnames, pernames)
-- Update some unit definitions with extra data defined in other sections.
-- This is done after all input data has been processed.
for _, unit in ipairs(units) do
local comp = composites[unit.unitcode]
if comp then
unit.subdivs = '{ ' .. table.concat(comp.subdivs, ', ') .. ' }'
end
if varnames[unit.unitcode] then
end
unit.varname = varnames[unit.unitcode]
end
if pernames[unit.unitcode] then
unit.pername = pernames[unit.unitcode]
end
end
end
 
local function make_override(cfg, data)
-- Return a function which, when called, stores a unit code that is not to be
-- checked for a duplicate. The table is stored in data (also a table).
return function (utype, fields)
local ucode = fields[1]
if empty(ucode == nil) then
quit('m_ovr_miss')
error('Missing unit code for an override.', 0)
end
if data[ucode] then
quit('m_ovr_dup', ucode)
error('Override "' .. ucode .. '" is already defined.', 0)
end
data[ucode] = true
end
end
 
local function make_default(cfg, data)
-- Return a function which, when called, stores a table that defines a
-- default output unit. The table is stored in data (also a table).
local defaults_index = {} -- to detect attempts to define a default twice
return function (utype, fields)
-- Store a table defining a unit.
-- This is for a unit such as 'kg' that has a default output unit
-- different from what is defined for the base unit ('g').
-- Throw an error if a problem occurs.
local ucode = fields[1]
local default = fields[2]
if empty(ucode) then
quit('m_dfs_code')
error('Defaults section: no unit code specified.', 0)
end
if empty(default) then
quit('m_dfs_none', ucode)
error('Defaults section: unit "' .. ucode .. '" has no default specified.', 0)
end
if #fields ~= 2 then
quit('m_dfs_two', ucode)
error('Defaults section: unit "' .. ucode .. '" should have two fields only.', 0)
end
local unit_table = get_unit(ucode)
if ifnot unit_table == nil then
quit('m_dfs_undef', ucode)
error('Defaults section: unit "' .. ucode .. '" is not defined.', 0)
end
local symbol = unit_table.defkey or unit_table.symbol
if empty(symbol) then
quit('m_dfs_sym', ucode)
error('Defaults section: unit "' .. ucode .. '" must have a symbol.', 0)
end
check_default(default, ucode, utype, unit_table)
if defaults_index[ucode] then
quit('m_dfs_dup', ucode)
error('Defaults section: unit "' .. ucode .. '" has already been specified.', 0)
end
defaults_index[ucode] = default
table.insert(data, { symbol = symbol, default = default })
end
end
 
local function clean_link(link, name)
-- Return link, customary where:
-- link = given link after removing any '[[...]]' wiki formatting
-- and removing any leading '+' or '*' or '@';
-- customary = 1 if leading '+', or 2 if '*' or 3 if '@', or nil
-- (for extra "US" or "U.S." or "Imperial" customary units link).
-- Result has leading/trailing whitespace removed, and is nil if empty
-- or if link matches the name, if a name is specified.
-- Exception: If the resulting link is nil,empty noand linkthe fieldname isstarts storedwith '[[', and
-- if athe link is required,stored itas will'' be(for seta fromunit thename unit'swhich nameis always linked).
-- If the resulting link is nil, no link field is stored, and
if empty(link) then
-- if a link is required, it will be set from the unit's name.
return nil
local original = link
end
if empty(link) then
local prefixes = { ['+'] = 1, ['*'] = 2, ['@'] = 3 }
return (name and name:sub(1, 2) == '[[') and '' or nil
local customary = prefixes[link:sub(1, 1)]
end
if customary then
local prefixes = { ['+'] = 1, ['*'] = 2, ['@'] = 3 }
link = strip(link:sub(2))
local customary = prefixes[link:sub(1, 1)]
end
if link:sub(1, 2) == '[['customary then
link = strip(link:sub(32))
end
if link:sub(-1, 2) == ']][[' then
link = link:sub(1, -3)
end
if link:sub(-2) == strip(link)']]' then
link if= link:sub(1, 1) == '[' or link:sub(-13) == ']' then
end
error('Link "' .. link .. '" has wrong number of brackets.', 0)
link = strip(link)
end
if link:sub(1, 1) == if'[' or link:sub(-1) == ']' then
quit('m_lnk_brack', original)
link = nil
end
elseif name then
if link == '' then
local l = link:sub(1, 1):lower() .. link:sub(2)
link = nil
local n = name:sub(1, 1):lower() .. name:sub(2)
elseif if l == nname then
local l = ulower(usub(link, 1, 1)) .. usub(link, 2)
link = nil -- link == name, ignoring case of first letter
local n = ulower(usub(name, 1, 1)) .. usub(name, 2)
end
if l == n endthen
link = nil -- link == name, ignoring case of first letter
return link, customary
end
end
return link, customary
end
 
local function make_link(cfg, data)
-- Return a function which, when called, stores a table that defines a
-- link exception. The table is stored in data (also a table).
local links_index = {} -- to detect attempts to define a link twice
return function (utype, fields)
-- Store a table defining a unit.
-- This is for a unit such as 'kg' that has a linked article
-- different from what is defined for the base unit ('g').
-- Throw an error if a problem occurs.
local ucode = fields[1]
local link = clean_link(fields[2])
if empty(ucode) then
quit('m_lnk_miss')
error('Missing unit code for a link.', 0)
end
if empty(link) then
quit('m_lnk_none', ucode)
error('No link defined for unit "' .. ucode .. '".', 0)
end
if #fields ~= 2 then
quit('m_lnk_two', ucode)
error('Row for unit "' .. ucode .. '" link should have two fields only.', 0)
end
local unit_table = get_unit(ucode)
if ifnot unit_table == nil then
quit('m_lnk_undef', ucode)
error('Unit code "' .. ucode .. '" for a link is not defined.', 0)
end
if utype ~= unit_table.utype then
quit('m_lnk_type', ucode)
error('Link exception "' .. ucode .. '" has wrong type.', 0)
end
local symbol = unit_table.symbol
if empty(symbol) then
quit('m_lnk_sym', ucode)
error('Unit code "' .. ucode .. '" for a link must have a symbol.', 0)
end
if links_index[ucode] then
quit('m_lnk_dup', ucode)
error('Link exception "' .. ucode .. '" is already defined.', 0)
end
links_index[ucode] = link
table.insert(data, { symbol = symbol, link = link })
end
end
 
local function clean_scale(scale)
-- Return cleaned scale as a string, after evaluating any expression.
-- It would be better to retain scale expressions like "5/9" so that
-- the expression is evaluated on the server and maintains the full
-- resolution of the server. However, there are many such expressions
-- in the table of all units, and it seems pointless to require the
-- server to evaluate all of them just to do one convert.
if empty(scale) then
quit('m_scl_miss')
error('Missing scale.', 0)
end
assert(type(scale) == 'string', 'Bug: scale has an unexpected type')
scale = string.gsub(scale, ',', '') -- remove comma separators
if tonumber(scale) then -- not an expression
return scale
end
local status, value = pcall(evaluate, scale)
if not (status and type(value) == 'number') then
quit('m_scl_bad', scale)
error('Scale expression is invalid: "' .. scale .. '".', 0)
end
local result = string.format('%.30g17g', value)
if result:find(bad_number_char, 1, true'[#n]') then
-- Lua can give results like "#INF" while Scribunto gives "inf". Either is an error.
error('Scale expression gives an invalid value: "' .. scale .. '".', 0)
quit('m_scl_oflow', scale)
end
end
-- Omit redundant zeros from results like '1.2e-005'.
-- Omit redundant zeros from results like '1.2e-005'.
-- Do not bother looking for results like '1.2e+005' as none occur in practice.
local lhs, zeros, rhs = result:match('^(.-e%-)(0+)(.*)')
if zeros then
result = lhs .. rhs
end
return result
end
 
local function add_alias_optional_fields(unit, start, fields, target)
-- Inspect fields[i] for i = start, start+1 ..., and extract any
-- definitions appropriate for an alias or "per", and add them to unit.
-- For an alias, target is a valid unit; for a "per", target is nil.
-- Throw error if encounter an invalid entry.
for i = start, #fields do
local field = fields[i]
if not empty(field) then
local lhs, rhs = field:match('^%s*(.-)%s*=%s*(.-)%s*$')
local good
if not empty(rhs) then
for _, item in ipairs({ 'sp', 'default', 'link', 'multiplier', 'symbol', 'symlink', 'abbr' }) do
if lhs == item then
if item == 'sp' then
if rhs == 'us' then
unit.sp_us = true
good = true
end
end
elseif item == 'link' then
local tlink
if target then
tlink = target[item]
end
end
local link, customary = clean_link(rhs, tlink)
if link then
unit[item] = link
end
end
if customary then
unit.customary = customary
end
end
good = true
elseif item == 'symlink' then
local pos1 = rhs:find('[[', 1, true)
local pos2 = rhs:find(']]', 1, true)
if not (pos1 and pos2 and (pos1 < pos2)) then
quit('m_als_link', unit.unitcode)
error('Alias "' .. unit.unitcode .. '" must include a wikilink ("[[...]]") in the symlink text.', 0)
end
end
unit.symlink = rhs
good = true
elseif item == 'multiplier' then
if not tonumber(rhs) then
quit('m_als_mul', unit.unitcode, rhs)
error('Alias "' .. unit.unitcode .. '" has multiplier "' .. rhs .. '" which is not a number.', 0)
end
end
unit[item] = rhs
good = true
elseif item == 'abbr' then
else
if target and rhs == target[item]'off' then
unit.usename = 1
error('Should omit "' .. item .. '" for alias "' .. unit.unitcode .. '" because it is the same as its target.', 0)
good = true
end
end
unit[item] = rhs
else
good = true
if target and rhs == target[item] then
end
quit('m_als_same', item, unit.unitcode)
break
end
end
unit[item] = rhs
end
good = true
end
end
if not good then
break
error('Alias has invalid text in field "' .. field .. '".', 0)
end
end
end
end
if not good then
quit('m_als_bad', field)
end
end
end
end
 
local function make_alias(fields, ucode, utype, symbol)
-- Return a new alias unit, or return nil if symbol is not already
-- defined as the unit code of the target unit.
-- Throw an error if invalid.
local target = get_unit(symbol)
if not target then
return nil
end
local unit = { unitcode = ucode, utype = utype, target = symbol }
add_alias_optional_fields(unit, 3, fields, target)
if alias_index[ucode] then
quit('m_als_dup', ucode)
error('Alias "' .. ucode .. '" already defined.', 0)
else
alias_index[ucode] = unit
end
if target.utype ~= utype then
quit('m_als_type', ucode)
error('Target of alias "' .. ucode .. '" has wrong type.', 0)
end
return unit
end
 
local function make_per(fields, ucode, utype, symbol)
-- Return a new "per" unit, or return nil if symbol is not of form "x/y".
-- Throw an error if invalid.
-- The top, bottom unit codes are checked later, after all units are defined.
local top, bottom = symbol:match('^(.-)/(.*)$')
if not top then
return nil
end
local unit = { unitcode = ucode, utype = utype, per = { strip(top), strip(bottom) } }
add_alias_optional_fields(unit, 3, fields)
if per_index[ucode] then
quit('m_per_dup', ucode)
error('Per unit "' .. ucode .. '" already defined.', 0)
else
per_index[ucode] = unit
end
return unit
end
 
local function make_unit(cfg, data)
-- Return a function which, when called, stores a table that defines a
-- single unit. The table is stored in data (also a table).
local fieldnames = {
-- Fields in the Conversions section are assumed to be in the following order.
'unitcode',
'symbol',
'sym_us',
'scale',
'extra',
'name1',
'name2',
'name1_us',
'name2_us',
'prefixes',
'default',
'link',
}
}
return function (utype, fields)
-- Store a table defining a unit.
-- Throw an error if a problem occurs.
local ucode, symbol = fields[1], fields[2]
if empty(utype) then
quit('m_miss_type')
error('Missing unit type.', 0)
end
if empty(ucode) then
quit('m_miss_code')
error('Missing unit code.', 0)
end
if empty(symbol) then
quit('m_miss_sym')
error('Missing symbol.', 0)
end
local prefix = symbol:sub(1, 1)
if prefix == '~' or prefix == '=' or prefix == '!' or prefix == '*' then
if symbol:sub(1, 2) == '==' then
prefix = symbol:sub(1, 2)
end
end
symbol = strip(symbol:sub(#prefix + 1)) -- omit prefix and any following whitespace
fields[2] = symbol
else
prefix = nil -- not a valid prefix
end
if prefix == '=' or prefix == '==' then
-- ucode is an alias (a fake unit code used in a convert template), or
-- defines a "per" unit like "$/acre" or "BTU/h".
-- For an alias, symbol is the unit code of the actual unit.
-- For a "per", symbol is of form "x/y" where x and y are unit codes,
-- or x is a recognized currency symbol and y is a unit code.
-- Checking that x and y are valid is deferred until all units have
-- been defined so, for example, "BTU/h" can be defined before "h".
local unit
if prefix == '=' then
unit = make_alias(fields, ucode, utype, symbol)
else
unit = make_per(fields, ucode, utype, symbol)
end
end
if not unit then
-- Do not define an alias in terms of another alias.
quit('m_als_undef', symbol)
error('Primary unit must be defined before alias =' .. symbol, 0)
end
end
insert_unique_unit(data, unit, units_index)
return
elseif prefix == '!' then
-- ucode may be incorrectly entered as a unit code.
-- symbol is a message saying what unit code should be used.
local unit = { unitcode = ucode, shouldbe = symbol }
insert_unique_unit(data, unit, nil)
return
end
-- Make the unit.
local unit = { utype = utype }
for i, name in ipairs(fieldnames) do
if not empty(fields[i]) then
unit[name] = fields[i]
end
end
end
-- Remove redundancy from unit.
if unit.sym_us == symbol then
unit.sym_us = nil
end
local prefixes = unit.prefixes
local name1, name2 = unit.name1, unit.name2
if empty(prefixes) then
if name1 then
prefixes = nil
if name1 == symbol and not prefixes then
end
-- A unit which takes an SI prefix must not have a nil name because,
local name1, name2 = unit.name1, unit.name2
-- for example, the name for "kW" = "kilo" .. "watt" (name for "W").
if name1 then
-- The "not prefixes" test is needed for bnwiki where the
if name1 == symbol and not prefixes then
-- watt unit has the same name and symbol.
-- A unit which takes an SI prefix must not have a nil name because,
unit.name1 = nil
-- for example, the name for "kW" = "kilo" .. "watt" (name for "W").
end
-- The "not prefixes" test is needed for bn.wikipedia where the
else
-- watt unit has the same name and symbol.
unit. name1 = nilsymbol
end
end
if name2 then
else
if name2 == name1 .. plural_suffix then
name1 = symbol
unit.name2 = nil
end
end
if name2 then
else
if plural_suffix ~= '' and name2 == name1 .. plural_suffix then
name2 = name1 .. plural_suffix
unit.name2 = nil
end
end
local name1_us, name2_us = unit.name1_us, unit.name2_us
end
if name1_us then
local name1_us, name2_us = unit.name1_us, unit.name2_us
if name1_us == if name1_usname1 then
if unit.name1_us == name1 thennil
end
unit.name1_us = nil
end
end
if name2_us then
else
if unit.name1_us = name1then
if name2_us == unit.name1_us .. plural_suffix then
end
unit.name2_us = nil
if name2_us then
end
if plural_suffix ~= '' and name2_us == name1_us .. plural_suffix then
elseif unit.name2_us == name2 nilthen
unit.name2_us = nil
end
end
end
-- Other changes to unit.
-- Other changes to unit.
unit.scale = clean_scale(unit.scale)
local extra = unit.extra
if not empty(extra) then
-- Set appropriate fields for a unit that needs more than a simple
-- multiplication by a ratio of unit scales to convert values.
unit.iscomplex = true
if extra == 'volume/length' then
unit.invert = 1
elseif extra == 'length/volume' then
unit.invert = -1
elseif specials.utype[utype] == 'type_temperature' then
unit.offset = extra
elseif extra == 'invert' then
else
unit.builtininvert = extra-1
else
end
unit.builtin = extra
end
end
if prefix == '~' then
end
-- Magic code for units like "acre" where the symbol is not really a
if prefix == '~' then
-- symbol, and output should use the singular or plural name instead.
-- Magic code for units like "acre" where the symbol is not really a
unit.usename = 1
-- symbol, and output should use the singular or plural name instead.
end
unit.usename = 1
local name_for_link
elseif prefix == '*' then
if prefixes then
-- Magic code for units like "pitch" which have a symbol that is the same as
-- If set name_for_link = name1 for a prefixed unit like g, then the
-- another unit with entries defined in the default or link exceptions tables.
-- link is "kilogram" for kg, and "yottagram" for Yg, and so on for all
unit.defkey = ucode -- key for default exceptions
-- prefixes. That might be good for some units, but not all.
unit.linkey = ucode -- key for link exceptions
if prefixes == 'SI' then
end
unit.prefixes = 1
local name_for_link
elseif prefixes == 'SI2' then
if unit.prefixes = 2then
elseif if prefixes == 'SI3SI' then
unit.prefixes = 31
elseif prefixes == 'SI2' then
else
unit.prefixes = 2
error('Unknown prefix: "' .. prefixes .. '".', 0)
elseif prefixes == 'SI3' then
end
unit.prefixes = 3
else
else
name_for_link = name1
quit('m_pfx_bad', prefixes)
end
end
unit.link, unit.customary = clean_link(unit.link, name_for_link)
else
if prefixes then
-- Only units which do not accept SI prefixes have name_for_link set.
local name1, name1_us = unit.name1, unit.name1_us -- after redundancy removed
-- That is because, for example, if set name_for_link if= name1 ==for nilunit theng,
-- then the link is "kilogram" for kg, and "yottagram" for Yg, and so on
error('Unit with Prefix set must include Name.', 0)
-- for all prefixes. That might be desirable for some units, but not all.
end
name_for_link = name1
if unit.name2 or unit.name2_us then
end
error('Unit with Prefix set must have plural name that is Name + "s".', 0)
unit.link, unit.customary = clean_link(unit.link, name_for_link)
end
if unit.sym_usprefixes then
-- The SI prefix is always at the start (position = 1) for symbol and sym_us.
error('Unit with Prefix set must have same Symbol and US symbol.', 0)
-- However, each name (name1, name2, name1_us, name2_us) can have the SI prefix
end
-- at any position, and that position can be different for each name.
local pos = name1:find('%s', 1, true)
-- For enwiki, the only units with names where the prefix is not at the start
local pos_us
-- are "square metre" and "cubic metre" ("square meter" and "cubic meter" for sp=us).
if name1_us then
-- Some other wikis want the flexibility that the prefix position can be different
pos_us = name1_us:find('%s', 1, true)
-- so the position is stored as nil (if always 1), or N (an integer, if always N),
else
-- or a string of four comma-separated numbers such as "5,7,9,11" which means the
pos_us = pos
-- prefix position for (name1, name2, name1_us, name2_us) is (5, 7, 9, 11)
end
-- respectively.
if pos ~= pos_us then
local name1, name1_us = unit.name1, unit.name1_us -- after redundancy removed
-- The only cases with "%s" are "square %smetre" and "cubic %smetre" (or "meter" for US),
if not name1 then
-- so do not bother having a procedure to handle different positions of "%s".
quit('m_pfx_name')
error('Unit with Prefix set and "%s" in Name and US name, must have the "%s" at the same position.', 0)
end
end
local positions = collection()
if pos then
for i, k in ipairs({ 'name1', 'name2', 'name1_us', 'name2_us' }) do
if pos == 1 then
local name = unit[k]
-- Omit leading "%s"; convert will assume position = 1 since not stored.
local pos
unit.name1 = name1:sub(3)
if name1_usname then
pos = name:find('%s', 1, true)
unit.name1_us = name1_us:sub(3)
if pos then
end
unit[k] = name:sub(1, pos - 1) .. name:sub(pos + 2)
else
end
-- Omit "%s" and store its position.
elseif i == 2 or i == 3 then
unit.name1 = name1:sub(1, pos - 1) .. name1:sub(pos + 2)
pos = positions[1]
if name1_us then
elseif i == 4 then
unit.name1_us = name1_us:sub(1, pos - 1) .. name1_us:sub(pos + 2)
pos = positions[unit.name1_us and 3 or 2]
end
end
unit.prefix_position = pos
positions:add(pos or 1)
end
end
end
local pos = positions[1]
for _, name in ipairs({ 'symbol', 'name1', 'name1_us' }) do
for i = 2, positions.n do
unit['_' .. name] = unit[name]
if pos ~= positions[i] then
unit[name] = nil -- force call to __index metamethod so any SI prefix can be handled
pos = '"' .. positions:join(',') .. '"'
end
break
end
end
for name, v in pairs(unit) do
end
-- Reject if a string field includes "%s" (should never occur after above).
if pos ~= 1 then
if type(v) == 'string' and v:find('%s', 1, true) then
unit.prefix_position = pos
error('Field "' .. name .. '" must not contain "%s".', 0)
end
end
for _, name in ipairs({ 'symbol', 'sym_us', 'name1', 'name1_us', 'name2', 'name2_us' }) do
end
unit['_' .. name] = unit[name]
insert_unique_unit(data, unit, units_index)
unit[name] = nil -- force call to __index metamethod so any SI prefix can be handled
end
end
end
for name, v in pairs(unit) do
-- Reject if a string field includes "%s" (should not occur after above).
if type(v) == 'string' and v:find('%s', 1, true) then
quit('m_percent_s', name)
end
end
insert_unique_unit(data, unit, units_index)
end
end
 
local function make_combination(cfg, data)
-- Return a function which, when called, stores a table that defines a
-- single combination unit. The table is stored in data (also a table).
return function (utype, fields)
-- Store a table defining a unit.
-- This is for a combination unit that specifies more than one output.
-- The target units must be defined first.
-- Throw an error if a problem occurs.
local unit = { utype = utype, combination = {} }
for i, v in ipairs(fields) do
if i == 1 then -- unitcode
if v == '' then
quit('m_cmb_miss')
error('Missing unit code for a combination.', 0)
end
end
unit.unitcode = v
elseif v == '' then
-- Ignore empty fields.
else
local target = get_unit(v)
if ifnot target == nil then
quit('m_cmb_undef', v, unit.unitcode)
error('Unit "' .. v .. '" in combination "' .. unit.unitcode .. '" not defined.', 0)
end
end
if target.utype ~= utype then
quit('m_cmb_type', v, unit.unitcode)
error('Unit "' .. v .. '" in combination "' .. unit.unitcode .. '" has wrong type.', 0)
end
end
table.insert(unit.combination, v)
end
end
end
if #unit.combination < 2 then
quit(#unit.combination == 0 and 'm_cmb_none' or 'm_cmb_one', unit.unitcode)
local msg
end
if #unit.combination == 0 then
insert_unique_unit(data, unit, units_index)
msg = 'No units specified for combination "' .. unit.unitcode .. '"'
end
else
end
msg = 'Only one unit specified for combination "' .. unit.unitcode .. '"'
 
end
local function make_perunit(cfg, data)
error(msg, 0)
-- Return a function which, when called, stores a table that defines a
end
-- fixup for an automatic per unit. The table is stored in data (also a table).
insert_unique_unit(data, unit, units_index)
local pertype_index = {} -- to detect attempts to define a fixup twice
end
return function (utype, fields)
-- Store a table to define a fixup.
-- Typos or other errors in the input are not detected!
-- Parameter utype is ignored (it is nil).
-- Throw an error if a problem occurs.
local lhs, rhs, link, multiplier
for i, v in ipairs(fields) do
if v == '' then
-- Ignore empty fields.
elseif i == 1 then
lhs = v -- like "length/time"
elseif i == 2 then
rhs = v -- like "speed"
elseif i == 3 then
link = v
elseif i == 4 then
if not tonumber(v) then
quit('m_per_inv')
end
multiplier = v
else
quit('m_per_inv')
end
end
if lhs and (rhs or link or multiplier) then
if link or multiplier then
local parts = collection()
if rhs then
parts:add('utype = "' .. rhs .. '"')
end
if link then
parts:add('link = "' .. link .. '"')
end
if multiplier then
parts:add('multiplier = ' .. multiplier)
end
rhs = '{ ' .. parts:join(', ') .. ' }'
else
rhs = '"' .. rhs .. '"'
end
if pertype_index[lhs] then
quit('m_per_dup', lhs)
end
pertype_index[lhs] = rhs
table.insert(data, { lhs = lhs, rhs = rhs })
else
quit('m_per_inv')
end
end
end
 
local function make_varname(cfg, data)
-- Return a function which, when called, stores a table that defines a
-- variable name for a unit. The table is stored in data (also a table).
return function (utype, fields)
-- Set or update an entry in the data table to record that a unit has a variable name.
-- This is for slwiki where a unit name depends on the value.
-- The target units must be defined first.
-- Parameter utype is ignored (it is nil).
-- Throw an error if a problem occurs.
local count = #fields
if count ~= cfg.varcolumns then
quit('m_var_cnt')
end
local ucode
local names = {}
for i = 1, count do
local v = fields[i]
if empty(v) then
quit('m_var_miss')
end
if i == 1 then -- unitcode
ucode = v
if not get_unit(v) then
quit('m_var_undef', v)
end
else
table.insert(names, v)
end
end
if data[ucode] then
quit('m_var_dup', ucode)
end
data[ucode] = table.concat(names, '!')
end
end
 
local function make_pername(cfg, data)
-- Return a function which, when called, stores a table that defines a
-- per name for a unit. The table is stored in data (also a table).
return function (utype, fields)
-- Set or update an entry in the data table to record that a unit has a
-- non-standard per name if used as the second unit in a per unit (x per y).
-- The target units must be defined first.
-- Parameter utype is ignored (it is nil).
-- Throw an error if a problem occurs.
local count = #fields
if count ~= 2 then
quit('m_pnm_cnt')
end
local ucode, pername
for i = 1, count do
local v = fields[i]
if empty(v) then
quit('m_pnm_miss')
end
if i == 1 then -- unitcode
ucode = v
if not get_unit(v) then
quit('m_pnm_undef', v)
end
else
pername = v
end
end
if data[ucode] then
quit('m_pnm_dup', ucode)
end
data[ucode] = pername
end
end
 
local function reversed(t)
-- Return a numbered table in reverse order.
local reversed, count = {}, #t
for i = 1, count do
reversed[i] = t[count + 1 - i]
end
return reversed
end
 
local function make_inputmultiple(cfg, data)
-- Return a function which, when called, stores a table that defines a
-- single composite (multiple input) unit. The table is stored in data (also a table).
return function (utype, fields)
-- Set or update an entry in the data table to record that a unit
-- accepts subdivisions to make a composite input unit like '|2|ft|6|in'.
-- The target units must be defined first.
-- Throw an error if a problem occurs.
local unitcode -- dummy code required for simplicity, but which is not used in output
local alternate_code -- an alternative unit code can be specified to replace convert input
local fixed_name -- a fixed name can be specified to replace the unit's normal symbol/name
local default_code
local ucodes, scales = {}, {}
for i, v in ipairs(fields) do
-- 1=composite, 2=ucode1, 3=ucode2, 4=default, 5=alternate, 6=name
if i == 1 then
if v == '' then
quit('m_cmp_miss')
error('Missing unit code for a composite.', 0)
end
end
unitcode = v
elseif 2 <= i and i <= 5 then
if not (i == 5 and v == '') then
local target = get_unit(v, (i == 4) and utype or nil) -- the default may be an auto combination
local target = get_unit(v)
if ifnot target == nil then
quit('m_cmp_undef', v, unitcode)
error('Unit "' .. v .. '" in composite "' .. unitcode .. '" not defined.', 0)
end
end
if target.utype ~= utype then
quit('m_cmp_type', v, unitcode)
error('Unit "' .. v .. '" in composite "' .. unitcode .. '" has wrong type.', 0)
end
end
if i < 4 then
if not target.scale then
table.insert(ucodes, v)
quit('m_mul_std', v, unitcode)
table.insert(scales, target.scale)
end
elseif i == 4 then
table.insert(ucodes, v)
default_code = v
table.insert(scales, target.scale)
else
elseif i == 4 then
if scales[#scales] ~= target.scale then
default_code = v
error('Alternate unit "' .. v .. '" in composite "' .. unitcode .. '" has wrong scale.', 0)
else
end
if scales[#scales] ~= target.scale then
alternate_code = v
quit('m_cmp_scale', v, unitcode)
end
end
end
alternate_code = v
elseif i == 6 then
end
if v ~= '' then
end
fixed_name = v
elseif i == 6 then
end
if v ~= '' then
else
fixed_name = v
error('Composite "' .. unitcode .. '" has too many fields.', 0)
end
end
else
end
quit('m_cmp_many', unitcode)
if #ucodes ~= 2 then
end
error('Composite "' .. unitcode .. '" must specify exactly two unit codes.', 0)
end
if default_code#ucodes =~= nil2 then
quit('m_cmp_two', unitcode)
error('Composite "' .. unitcode .. '" must specify a default unit code.', 0)
end
if not default_code then
-- Component units must be specified from most-significant to least-significant,
quit('m_cmp_def', unitcode)
-- and each ratio of a pair of scales must be very close to an integer.
end
-- Currently, there will be exactly two scales and one ratio.
-- Component units must be specified from most-significant to least-significant,
local ratios, count = {}, #scales
-- and each ratio of a pair of scales must be very close to an integer.
for i = 1, count do
-- Currently, there will be exactly two scales and one ratio.
local scale = tonumber(scales[i])
local ratios, count = {}, #scales
if scale == nil or scale <= 0 then
for i = 1, count do
error('Composite "' .. unitcode .. '" has a component with an invalid scale, "' .. scales[i] .. '".', 0)
local scale = tonumber(scales[i])
end
if scale == nil or scale <= 0 then
scales[i] = scale
quit('m_cmp_inval', unitcode, scales[i])
end
end
for i = 1, count - 1 do
local ratio = scales[i] /= scales[i + 1]scale
end
local rounded = math.floor(ratio + 0.5)
for i = 1, count - 1 do
if rounded < 2 then
local ratio = scales[i] / scales[i + 1]
error('Composite "' .. unitcode .. '" has components in wrong order or with invalid scales.', 0)
local rounded = math.floor(ratio + 0.5)
end
if math.abs(ratio - rounded)/ratio >< 1e-62 then
quit('m_cmp_order', unitcode)
error('Composite "' .. unitcode .. '" has components where scale ratios are not integers.', 0)
end
end
if math.abs(ratio - rounded)/ratio > 1e-6 then
ratios[i] = rounded
quit('m_cmp_int', unitcode)
end
end
local text = { tostring(ratios[1]) }
ratios[i] = rounded
local function add_text(key, value)
end
table.insert(text, string.format('%s = %q', key, value))
local text = { tostring(ratios[1]) }
end
local function add_text(key, value)
if default_code then
table.insert(text, string.format('%s = %q', key, value))
add_text('default', default_code)
end
if alternate_codedefault_code then
add_text('unitdefault', alternate_codedefault_code)
end
if fixed_namealternate_code then
add_text('nameunit', fixed_namealternate_code)
end
if fixed_name then
local subdiv = string.format('["%s"] = { %s }', ucodes[2], table.concat(text, ', '))
add_text('name', fixed_name)
local main_code = ucodes[1]
end
local item = data[main_code]
local subdiv = string.format('["%s"] = { %s }', ucodes[2], table.concat(text, ', '))
if item then
local main_code = ucodes[1]
table.insert(item.subdivs, subdiv)
local item = data[main_code]
else
if item then
data[main_code] = { subdivs = { subdiv } }
table.insert(item.subdivs, subdiv)
end
else
end
data[main_code] = { subdivs = { subdiv } }
end
end
end
 
local function make_outputmultiple(cfg, data)
-- Return a function which, when called, stores a table that defines a
-- single multiple output unit. The table is stored in data (also a table).
return function (utype, fields)
-- Store a table defining a unit.
-- This is for a multiple unit like 'ydftin' (result in yards, feet, inches).
-- The target units must be defined first.
-- Throw an error if a problem occurs.
local unit = { utype = utype }
local ucodes, scales = {}, {}
for i, v in ipairs(fields) do
if i == 1 then -- unitcode
if v == '' then
quit('m_mul_miss')
error('Missing unit code for a multiple.', 0)
end
end
unit.unitcode = v
elseif v == '' then
-- Ignore empty fields.
else
local target = get_unit(v)
if ifnot target == nil then
quit('m_mul_undef', v, unit.unitcode)
error('Unit "' .. v .. '" in multiple "' .. unit.unitcode .. '" not defined.', 0)
end
end
if target.utype ~= utype then
quit('m_mul_type', v, unit.unitcode)
error('Unit "' .. v .. '" in multiple "' .. unit.unitcode .. '" has wrong type.', 0)
end
end
if not target.scale then
table.insert(ucodes, v)
quit('m_mul_std', v, unit.unitcode)
table.insert(scales, target.scale)
end
end
table.insert(ucodes, v)
end
table.insert(scales, target.scale)
if #ucodes < 2 then
end
local msg
end
if #ucodes == 0 then
if #ucodes < 2 then
msg = 'No units specified for multiple "' .. unit.unitcode .. '"'
quit(#ucodes == 0 and 'm_mul_none' or 'm_mul_one', unit.unitcode)
else
end
msg = 'Only one unit specified for multiple "' .. unit.unitcode .. '"'
-- Component units must be specified from most-significant to least-significant
end
-- (so scale values will be in descending order),
error(msg, 0)
-- and each ratio of a pair of scales must be very close to an integer.
end
-- The componenets and ratios are stored in reverse order (least significant first).
-- Component units must be specified from most-significant to least-significant
-- This script stores a unit scale as a string (might be an expression like "5/9"),
-- (so scale values will be in descending order),
-- but scales in a multiple are handled as numbers (should never be expressions).
-- and each ratio of a pair of scales must be very close to an integer.
local ratios, count = {}, #scales
-- The componenets and ratios are stored in reverse order (least significant first).
for i = 1, count do
-- This script stores a unit scale as a string (might be an expression like "5/9"),
local scale = tonumber(scales[i])
-- but scales in a multiple are handled as numbers (should never be expressions).
if scale == nil or scale <= 0 then
local ratios, count = {}, #scales
quit('m_mul_scale', unit.unitcode, scales[i])
for i = 1, count do
end
local scale = tonumber(scales[i])
scales[i] = scale
if scale == nil or scale <= 0 then
end
error('Multiple "' .. unit.unitcode .. '" has a component with an invalid scale, "' .. scales[i] .. '".', 0)
for i = 1, count - 1 enddo
local ratio = scales[i] / scales[i] =+ scale1]
local rounded = math.floor(ratio + 0.5)
end
if rounded < 2 then
for i = 1, count - 1 do
quit('m_mul_order', unit.unitcode)
local ratio = scales[i] / scales[i + 1]
end
local rounded = math.floor(ratio + 0.5)
if math.abs(ratio if- rounded)/ratio <> 21e-6 then
quit('m_mul_int', unit.unitcode)
error('Multiple "' .. unit.unitcode .. '" has components in wrong order or with invalid scales.', 0)
end
end
ratios[i] = rounded
if math.abs(ratio - rounded)/ratio > 1e-6 then
end
error('Multiple "' .. unit.unitcode .. '" has components where scale ratios are not integers.', 0)
unit.combination = reversed(ucodes)
end
unit.multiple = reversed(ratios)
ratios[i] = rounded
insert_unique_unit(data, unit, units_index)
end
end
unit.combination = reversed(ucodes)
unit.multiple = reversed(ratios)
insert_unique_unit(data, unit, units_index)
end
end
 
-- To make updating Module:Convert/the data module easier, this script inserts a preamble
-- and a postamble so the result can be used to replace the whole page.
local data_preamble = [=[
-- Conversion data used by [[Module:Convert]] which uses mw.loadData() for
-- read-only access to this module so that it is loaded only once per page.
-- See [[:en:Template:Convert/Transwiki guide]] if copying to another wiki.
--
-- These data tables follow:
Line 1,387 ⟶ 1,761:
--
-- These tables are generated by a script which reads the wikitext of a page that
-- documents the required properties of each unit; see [[:en:Module:Convert/doc]].
]=]
 
local data_postamble = [=[
return {
all_units = all_units,
default_exceptions = default_exceptions,
link_exceptions = link_exceptions,
per_unit_fixups = per_unit_fixups,
}]=]
 
Line 1,414 ⟶ 1,789:
---------------------------------------------------------------------------
local default_exceptions = {
-- Prefixed units with a default different from that of the base unit.
-- Each key item is a prefixed symbol (unitcode for engineering notation).]]
 
local out_default_suffix = [[
Line 1,422 ⟶ 1,797:
 
local out_default_item = [[
["{symbol}"] = "{default}",]]
 
local out_link_prefix = [[
Line 1,430 ⟶ 1,805:
---------------------------------------------------------------------------
local link_exceptions = {
-- Prefixed units with a linked article different from that of the base unit.
-- Each key item is a prefixed symbol (not unitcode).]]
 
local out_link_suffix = [[
Line 1,438 ⟶ 1,813:
 
local out_link_item = [[
["{symbol}"] = "{link}",]]
 
local out_perunit_prefix = [[
---------------------------------------------------------------------------
-- Do not change the data in this table because it is created by running --
-- a script that reads the wikitext from a wiki page (see note above). --
---------------------------------------------------------------------------
local per_unit_fixups = {
-- Automatically created per units of form "x/y" may have their unit type
-- changed, for example, "length/time" is changed to "speed".
-- Other adjustments can also be specified.]]
 
local out_perunit_suffix = [[
}
]]
 
local out_perunit_item = [[
["{lhs}"] = {rhs},]]
 
local combination_specification = { -- pure combination like 'm ft', or a multiple like 'ftin'
'combination',
'multiple',
'utype',
}
 
local alias_specification = {
'target',
'symbol',
'sp_us',
'defaultusename',
'linkdefault',
'symlinklink',
'symlink',
'customary',
'customary',
'multiplier',
'multiplier',
}
 
local per_specification = {
'per',
'symbol',
'sp_us',
'utype',
'invert',
'iscomplex',
'default',
'link',
'symlink',
'customary',
'multiplier',
}
 
local shouldbe_specification = {
'shouldbe',
}
 
local unit_specification = {
'_name1',
'_name1_us',
'_symbol_name2',
'_name2_us',
'prefix_position',
'name1_symbol',
'_sym_us',
'name1_us',
'prefix_position',
'name2',
'name1',
'name2_us',
'name1_us',
'symbol',
'sym_usname2',
'name2_us',
'usename',
'pername',
'usesymbol',
'utypevarname',
'alttypesymbol',
'builtinsym_us',
'scaleusename',
'usesymbol',
'offset',
'invertutype',
'alttype',
'iscomplex',
'builtin',
'istemperature',
'scale',
'exception',
'offset',
'prefixes',
'defaultinvert',
'iscomplex',
'subdivs',
'istemperature',
'link',
'exception',
'customary',
'prefixes',
'sp_us',
'default',
'subdivs',
'defkey',
'linkey',
'link',
'customary',
'sp_us',
}
 
local no_quotes = {
combination = true,
customary = true,
multiple = true,
multiplier = true,
offset = true,
per = true,
prefix_position = true,
scale = true,
subdivs = true,
}
 
local function add_unit_lines(results, unit, spec)
-- Add lines of Lua source to define a unit to the results collection.
local function add_line(line)
local first_item = ' ["' .. unit.unitcode .. '"] = {'
-- Had planned to replace sequences of spaces with 4-column tabs here
local last_item = ' },'
-- (because the CodeEditor now assumes the use of such tabs).
results:add(first_item)
-- However, 4-column tabs are only visible when editing a module
for _, k in ipairs(spec) do
-- with browser scripting and the CodeEditor enabled, and that is rare.
local v = unit[k]
-- A module is usually viewed (with 8-column tabs), and some indents
if v then
-- would be messed up unless 8-column tabs are used. Therefore,
local want_quotes = (type(v) == 'string' and not no_quotes[k])
-- have decided to simply replace 8 spaces at start of line with a single
if type(v) == 'boolean' then
-- tab which reduces the size of the module, and is correct for viewing.
v = tostring(v)
if line:sub(1, 8) == string.rep(' ', 8) then
elseif type(v) == 'number' or k == 'scale' then
line = '\t' .. line:sub(9)
-- Replace results like '1e-006' with '1e-6'.
end
v = string.gsub(tostring(v), '(e[+-])0+([1-9].*)', '%1%2', 1)
results:add(line)
elseif type(v) ~= 'string' then
end
error('Fatal error: unknown data type for ' .. unit.unitcode)
local first_item = ' ["' .. unit.unitcode .. '"] end= {'
local last_item = ' },'
local fmt = string.format('\t%%s%s = %%%s,',
add_line(first_item)
(#k < 8) and '\t' or '',
for _, k in ipairs(spec) do
want_quotes and 'q' or 's')
local v = unit[k]
results:add(fmt:format(k, v))
if v then
end
local want_quotes = (type(v) == 'string' and not no_quotes[k])
end
if type(v) == 'boolean' then
results:add(last_item)
v = tostring(v)
elseif type(v) == 'number' or k == 'scale' then
-- Replace results like '1e-006' with '1e-6'.
v = string.gsub(tostring(v), '(e[+-])0+([1-9].*)', '%1%2', 1)
elseif type(v) ~= 'string' then
quit('m_ftl_type', unit.unitcode)
end
local fmt = string.format('%8s%%-9s= %%%s,', '', want_quotes and 'q' or 's')
add_line(fmt:format(k, v))
end
end
add_line(last_item)
end
 
local function numbered_table_as_string(data, unit)
local t = {}
for _, v in ipairs(data) do
if type(v) == 'string' then
table.insert(t, '"' .. v .. '"')
elseif type(v) == 'number' then
table.insert(t, tostring(v))
else
error quit('Fatal error: unknown data type for m_ftl_type' .., unit.unitcode)
end
end
return '{ ' .. table.concat(t, ', ') .. ' }'
end
 
local function extract_heading(line)
-- Return n, s where n = heading level number (nil if none), and
-- s = heading text (with leading/trailing whitespace removed).
local pattern = '^(==+)%s*(.-)%s*(==+)%s*$'
local before, heading, after = line:match(pattern)
if heading and #heading > 0 then
-- Don't bother checking if before == after.
return #before, heading
end
end
 
local function fields(line)
-- Return a numbered table of fields split from line.
-- Items are delimited by "||".
-- Each item has leading/trailing whitespace removed, and any encoded pipe
-- characters are decoded.
-- The second field (for symbol when processing units) is adjusted to
-- remove any "colspan" at the front of lines like:
-- "| unitcode || colspan="11" | !Text to display for an error message".
local t = {}
line = line .. "||" -- to get last field
for item in line:gmatch("%s*(.-)%s*||") do
table.insert(t, (item:gsub('&#124;', '|')))
end
if t[2] then
local cleaned = t[2]:match('^%s*colspan%s*=.-|%s*(.*)$')
local cleaned = t[2]:match('^%s*colspan%s*=.-|%s*(.*)$')
if cleaned then
if cleaned then
t[2] = cleaned
t[2] = cleaned
end
end
return t
end
return t
end
 
local function prepare_section(cfg, maker, lines, section, maxerrorsneed_section, need_utype)
-- Process the first level-two section with the given section name
-- in the given table of lines of wikitext.
-- If successful, maker inserts each item into a table.
-- Otherwise, an error is thrown.
local skip = true
local errors = collection()
local utype -- unit type (from level-three heading)
local nbsp = '\194\160' -- nonbreaking space is utf-8 encoded as hex c2 a0
for linenumber, line in ipairs(lines) do
if skip then
-- Skip down to and including the starting heading.
local level, heading = extract_heading(line)
if level == 2 and heading == section then
skip = false
end
end
else
-- Accummulate unit definitions.
local c1 = line:sub(1, 1)
local c2 = line:sub(2, 2)
if c1 == '|' and not (c2 == '-' or c2 == '}') then
if need_utype ifand empty(utype) then
quit('m_hdg_lev3', line)
error('No level 3 heading before: ' .. line, 0)
end
end
if line:find(nbsp, 1, true) then
-- For example, "acre ft" does not work if it contains nbsp.
add_warning('Line m_wrn_nbsp' .., linenumber .. ' contains a nonbreaking space')
end
end
local ok, msg = pcall(maker, utype, fields(line:sub(2)))
if not ok then
if msg:sub(-1) == '.' then msg = msg:sub(1, -2) end
errors:add(msg .. ' message(line ' ..m_line_num', linenumber .. ').')
if errors.n >= cfg.maxerrors then
break
break
end
end
end
end
else
local level, heading = extract_heading(line)
if level == 3 then
utype = ulower(heading:lower()
elseif level == 2 then
break
break
end
end
end
end
end
end
if skip and if skipneed_section then
quit('m_hdg_lev2', section)
error('Level 2 heading "' .. section ..'" not found.', 0)
end
if errors.n > 0 then
error(errors:join(), 0)
end
end
 
local function get_page_lines(page_title)
-- Read the wikitext of the page at the given title; split the text into
-- lines with leading and trailing space removed from each line.
-- Return a numbered table of the lines, or throw an error.
if empty(page_title) then
quit('m_no_title')
error('Need title of page with unit definitions.', 0)
end
local t = mw.title.new(page_title)
if t then
local content = t:getContent()
if content then
if content:sub(-1) ~= '\n' then
local lines = collection()
content = content .. '\n'
for line in string.gmatch(content, '[\t ]*(.-)[\t ]*\n') do
end
lines:add(line)
local lines = collection()
end
for line in string.gmatch(content, '[\t ]*(.-)[\t\r ]*\n') do
return lines
lines:add(line)
end
end
return lines
error('Could not read wikitext from [["' .. page_title .. ']]".', 0)
end
end
quit('m_ftl_read', page_title)
end
 
local function prepare_data(conversion_data_titlecfg, maxerrorsis_sandbox)
-- Read the page of conversion data, and process the wikitext
-- in the sections with wanted level-two headings.
-- Return units, defaults, links (three tables).
-- Throw an error if a problem occurs.
local composites, defaults, links, units, perunits, varnames, pernames = {}, {}, {}, {}, {}, {}, {}
local sections = {
{ 'overrides' , make_override , overrides , 0 },
-- LATER: Section names must be in English to match following text.
{ 'conversions' , make_unit { 'Overrides' , make_override units , overrides 0 },
{ 'Conversionsoutmultiples', make_outputmultiple, make_unit units , units 0 },
{ 'Output multiplescombinations', make_outputmultiplemake_combination , units , 0 },
{ 'inmultiples' , make_inputmultiple , composites, 0 }, -- after all units defined so default will be defined
{ 'Combinations' , make_combination , units },
{ 'defaults' , make_default , defaults , 0 },
{ 'Input multiples' , make_inputmultiple , composites }, -- after all units defined so default will be defined
{ 'links' {, 'Defaults'make_link , make_default , links , defaults 0 },
{ 'Linksperunits' , make_linkmake_perunit , perunits , links 1 },
{ 'varnames' , make_varname , varnames , 1 },
}
{ 'pernames' , make_pername , pernames , 1 },
local lines = get_page_lines(conversion_data_title)
}
for _, section in ipairs(sections) do
local lines = get_page_lines(cfg.data_title)
local heading = section[1]
for _, section in ipairs(sections) do
local maker = section[2](section[3])
local heading = mtext.section_names[section[1]]
prepare_section(maker, lines, heading, maxerrors)
local maker = section[2](cfg, section[3])
end
local code = section[4]
check_all_defaults(units, maxerrors)
local need_section, need_utype
check_all_pers(units, maxerrors)
if code == 0 and not is_sandbox then
update_units(units, composites)
need_section = true
return units, defaults, links
end
if code == 0 then
need_utype = true
end
prepare_section(cfg, maker, lines, heading, need_section, need_utype)
end
check_all_defaults(cfg, units)
check_all_pers(cfg, units)
update_units(units, composites, varnames, pernames)
return units, defaults, links, perunits
end
 
local function _makeunits(resultscfg, conversion_data_title, text_module_titleresults)
-- Read the wikitext for the conversion data.
-- Append output to given results collection, or throw error if a problem.
local text_code = require(text_module_titlecfg.text_title)
for _, name in ipairs({ 'SIprefixes', ='eng_scales', text_code.SIprefixes'currency' }) do
if type(text_code[name]) ~= 'table' then
eng_scales = text_code.eng_scales
quit('m_ftl_table', cfg.text_title, name)
local translation = text_code.translation_table
end
if translation then
end
if translation.plural_suffix then
local translation = text_code.translation_table
plural_suffix = translation.plural_suffix
if translation then
end
local ts = if translation.specialsplural_suffix then
plural_suffix = translation.plural_suffix
if ts then
end
if ts.utype then
local ts = translation.specials
specials.utype = ts.utype
if ts then
end
if ts.ucodeutype then
specials.ucodeutype = ts.ucodeutype
end
end
if ts.ucode then
end
specials.ucode = ts.ucode
end
end
local units, defaults, links = prepare_data(conversion_data_title, 20)
end
results:add(data_preamble)
local tm = translation.mtext
results:add(out_unit_prefix)
if tm then
for _, unit in ipairs(units) do
if tm.section_names then
local spec
mtext.section_names = tm.section_names
if unit.target then
end
spec = alias_specification
if tm.titles then
elseif unit.per then
mtext.titles = tm.titles
spec = per_specification
end
unit.per = numbered_table_as_string(unit.per, unit)
if tm.messages then
elseif unit.shouldbe then
mtext.messages = tm.messages
spec = shouldbe_specification
end
elseif unit.combination then
end
spec = combination_specification
end
unit.combination = numbered_table_as_string(unit.combination, unit)
local is_sandbox
if unit.multiple then
local conversion_data_title = mtext.titles.conversion_data
unit.multiple = numbered_table_as_string(unit.multiple, unit)
if cfg.data_title and cfg.data_title ~= conversion_data_title then
end
if is_test_run then
else
is_sandbox = true
spec = unit_specification
data_preamble = nil
end
data_postamble = nil
add_unit_lines(results, unit, spec)
out_unit_prefix = 'local all_units = {'
end
out_unit_suffix = '}'
results:add(out_unit_suffix)
out_default_prefix = '\nlocal default_exceptions = {'
results:add(out_default_prefix)
out_default_suffix = '}'
for _, unit in ipairs(defaults) do
out_default_item = '\t["{symbol}"] = "{default}",'
results:add((out_default_item:gsub('{([%w_]+)}', unit)))
out_link_prefix = '\nlocal link_exceptions = {'
end
out_link_suffix = '}'
results:add(out_default_suffix)
out_link_item = '\t["{symbol}"] = "{link}",'
results:add(out_link_prefix)
out_perunit_prefix = '\nlocal per_unit_fixups = {'
for _, unit in ipairs(links) do
out_perunit_suffix = '}'
results:add((out_link_item:gsub('{([%w_]+)}', unit)))
out_perunit_item = '\t["{lhs}"] = {rhs},'
end
end
results:add(out_link_suffix)
else
results:add(data_postamble)
cfg.data_title = conversion_data_title
end
local units, defaults, links, perunits = prepare_data(cfg, is_sandbox)
if data_preamble then
results:add(data_preamble)
end
results:add(out_unit_prefix)
for _, unit in ipairs(units) do
local spec
if unit.target then
spec = alias_specification
elseif unit.per then
spec = per_specification
unit.per = numbered_table_as_string(unit.per, unit)
elseif unit.shouldbe then
spec = shouldbe_specification
elseif unit.combination then
spec = combination_specification
unit.combination = numbered_table_as_string(unit.combination, unit)
if unit.multiple then
unit.multiple = numbered_table_as_string(unit.multiple, unit)
end
else
spec = unit_specification
end
add_unit_lines(results, unit, spec)
end
results:add(out_unit_suffix)
for _, t in ipairs({
{ defaults, out_default_prefix, out_default_item, out_default_suffix },
{ links , out_link_prefix , out_link_item , out_link_suffix },
{ perunits, out_perunit_prefix, out_perunit_item, out_perunit_suffix } }) do
local data, prefix, item, suffix = t[1], t[2], t[3], t[4]
if #data > 0 or not is_sandbox then
results:add(prefix)
for _, unit in ipairs(data) do
results:add((item:gsub('{([%w_]+)}', unit)))
end
results:add(suffix)
end
end
if data_postamble then
results:add(data_postamble)
end
end
 
local function makeunits(frame)
local args = frame.args
local config = {
local conversion_data_title = args[1] or 'Module:Convert/documentation/conversion data/doc'
data_title = args[1],
local text_module_title = args[2] or 'Module:Convert/text'
text_title = args[2] or 'Module:Convert/text',
local results = collection()
varcolumns = tonumber(args.varcolumns) or 5, -- #columns in "Variable names" section; slwiki uses 5
local ok, msg = pcall(_makeunits, results, conversion_data_title, text_module_title)
maxerrors = 20,
if not ok then
}
results:add('Error:\n')
local results = collection()
results:add(msg)
local ok, msg = pcall(_makeunits, config, results)
end
if not ok then
local warn = ''
results:add(message('m_error'))
if warnings.n > 0 then
results:add('')
warn = 'Warning:\n\n' .. warnings:join() .. '\n\n'
results:add(msg)
end
end
-- Pre tags returned by a module are html tags, not like wikitext <pre>...</pre>.
local warn = ''
-- The following renders the text as is, and preserves tab characters.
if warnings.n > 0 then
return '<pre>\n' .. mw.text.nowiki(warn .. results:join()) .. '\n</pre>\n'
warn = message('m_warning') .. '\n\n' .. warnings:join() .. '\n\n'
end
-- Pre tags returned by a module are html tags, not like wikitext <pre>...</pre>.
-- The following renders the text as is, and preserves tab characters.
return '<pre>\n' .. mw.text.nowiki(warn .. results:join()) .. '\n</pre>\n'
end