# Module:Convert/makeunits

-- This module generates the wikitext required at Module:Convert/data  
-- by reading and processing the wikitext of the master list of units  
-- (see conversion_data for the page title).  
---
-- Script method:  
-- * Read lines, ignoring everything before "== Conversions ==".  
-- * Process the following lines:  
-- * Find next level-3 heading like "=== Length ===".  
-- * Parse each following line starting with "|"  
-- (but ignore lines starting with "|-" or "|}".  
-- * Split such lines into fields (delimiter "||") and trim  
-- leading/trailing whitespace from each field.  
-- Remove any "colspan" at front of second field (symbol).  
-- * Remove thousand separators (commas) from the scale field.  
-- If the scale is a number, do not change it.  
-- Otherwise, it should be an expression like "5/9", in  
-- which case it is replaced by the value of the expression.  
-- * Remove wiki formatting '...' from the link field.  
-- * Remove redundant fields from the unit to reduce size of data table.  
-- * Create alternative forms of a unit such as an alias or a combination.  
-- * Stop processing when encounter end of text or a line starting  
-- with a level-2 heading ("==" but not "===").  
-- * Repeat above for each heading listed at prepare_data.  
-- * Output Lua source for the units table.  
---
-- -- Output has the following form.  
-- local all_units = {  
-- ["unitcode"] = { -- standard format  
-- name1 = "singular name", -- omitted if redundant  
-- name1_us = "singular name sp=us", -- omitted if redundant  
-- name2 = "plural name", -- omitted if redundant  
-- name2_us = "plural name sp=us", -- omitted if redundant  
-- symbol = "symbol",  
-- sym_us = "symbol sp=us", -- omitted if redundant  
-- usename = 1, -- omitted if empty  
-- utype = "unit type", -- from level-3 heading  
-- scale = 1, -- a value, if necessary from evaluating an expression  
-- subdivs = { ["ft"] = { 5280, default = "km" }, ["yd"] = { 1760 } } -- composite input; omitted if empty  
-- link = "title of article for wikilink", -- omitted if empty or redundant  
-- ... -- other values  
-- },  
-- ["unitcode"] = { -- alternative format to generate an alias  
-- target = "unit code",  
-- ... -- optional values to override those of target  
-- },  
-- ["unitcode"] = { -- alternative format to generate a "per" unit like $/acre or BTU/h  
-- per = {u1, u2}, -- numbered table of unitcodes (u1 may be a currency symbol)  
-- ... -- optional values  
-- },  
-- ["unitcode"] = { -- alternative format to generate an error message  
-- shouldbe = "message that some other unit code should be used",  
-- },  
-- ["unitcode"] = { -- alternative format for combination outputs (like 'm ft')  
-- combination = {u1, u2, ...}, -- numbered table of unitcodes  
-- utype = "unit type", -- as for standard format  
-- },  
-- ["unitcode"] = { -- alternative format for output multiples (like 'ftin')  
-- combination = {u1, u2, ...}, -- numbered table of unitcodes  
-- multiple = {f1, f2, ...}, -- numbered table of integer factors  
-- utype = "unit type", -- as for standard format  
-- },  
-- ...  
-- }  

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.  
-- However, this script should NOT be edited.  
-- Instead, the translation_table in Module:Convert/text can be edited,  
-- and this script will replace sections of the following with localized  
-- definitions from Module:Convert/text, if given.  
-- Ask for assistance at :en:Module talk:Convert.  
-- LATER: It would be better if this was defined in the conversion data.  
utype = {  
-- ["unit type in local language"] = "name_used_in_this_script"  
["fuel efficiency"] = "type_fuel_efficiency",  
["length"] = "type_length",  
["temperature"] = "type_temperature",  
["volume"] = "type_volume",  
},  
ucode = {  
exception = {  
-- ["unit code in local language"] = "name_used_in_module_convert"  
["ft"] = "integer_more_precision",  
["in"] = "subunit_more_precision",  
["lb"] = "integer_more_precision",  
},  
istemperature = {  
-- Common temperature scales (not keVT or MK).  
-- ["unit code in local language"] = true  
["C"] = true,  
["F"] = true,  
["K"] = true,  
["R"] = true,  
},  
usesymbol = {  
-- Use unit symbol not name if abbr not specified.  
-- ["unit code in local language"] = 1  
["C"] = 1,  
["F"] = 1,  
["K"] = 1,  
["R"] = 1,  
["C-change"] = 1,  
["F-change"] = 1,  
["K-change"] = 1,  
},  
alttype = {  
-- Unit has an alternate type that is a valid conversion.  
-- ["unit code in local language"] = "alternate type in local language"  
["Nm"] = "energy",  
["ftlb"] = "torque",  
["ftlb-f"] = "torque",  
["ftlbf"] = "torque",  
["inlb"] = "torque",  
["inlb-f"] = "torque",  
["inlbf"] = "torque",  
["inoz-f"] = "torque",  
["inozf"] = "torque",  
},  
},  
}  

-- Module text for the local language (localization).  
-- A default table of text for enwiki is provided here.  
-- If needed for another wiki, wanted sections from the table can be  
-- copied into translation_table in Module:Convert/text.  
-- For example, copying and modifying only the titles section may give:  
---
-- 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',  
},  
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_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,  
join = function (self, sep)  
return table.concat(self, sep or '\n')  
end,  
}  
end  

local warnings = collection  
local function add_warning(key, ...)  
-- Add a warning that will be inserted before the final result.  
warnings:add(message(key, ...))  
end  

---Begin code to evaluate expressions-----------------------------------  
-- This is needed because Lua's loadstring is not available in Scribunto,  
-- and each scale value can be specifed as an expression such as "5/9".  
-- More complex expressions are supported, including use of parentheses  
-- and the binary operators: + - * / ^  

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  
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  
local value = tonumber(hit)  
if value then  
self.pos = p2 + 1  
return value  
end  
quit_no_message('invalid number "' .. self.text:sub(self.pos) .. '"')  
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 quit_no_message('missing number') 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'  
quit_no_message('missing ' .. missing)  
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  
quit_no_message('unbalanced parentheses')  
else  
break  
end  
end  
perform_ops(0)  
if numstack.n > 1 then quit_no_message('missing operator') end  
if numstack.n < 1 then quit_no_message('missing number') 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 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  
-- so __index is not called twice for the same field in a given unit.  
__index = function (self, key)  
local value  
if key == 'name1' or key == 'sym_us' then  
value = self.symbol  
elseif key == 'name2' then  
value = self.name1 .. plural_suffix  
elseif key == 'name1_us' then  
value = self.name1  
if not rawget(self, 'name2_us') then  
-- If name1_us is 'foot', do not make name2_us by appending plural_suffix.  
self.name2_us = self.name2  
end  
elseif key == 'name2_us' then  
local raw1_us = rawget(self, 'name1_us')  
if raw1_us then  
value = raw1_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 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.  
-- 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)  
local value  
if key == 'symbol' then  
value = self.si_prefix .. self._symbol  
elseif key == 'sym_us' then  
value = rawget(self, '_sym_us')  
if value then  
value = self.si_prefix .. value  
else  
value = self.symbol  
end  
elseif key == 'name1' then  
value = prefixed_name(self, self._name1, 1)  
elseif key == 'name2' then  
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  
value = rawget(self, '_name1_us')  
if value then  
value = prefixed_name(self, value, 3)  
else  
value = self.name1  
end  
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  
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.si_name = ''  
result.si_prefix = ''  
return setmetatable(result, unit_prefixed_mt)  
end  
return setmetatable(result, unit_mt)  
end  
local SIprefixes = text_code.SIprefixes  
for plen = SIprefixes[1] or 2, 1, -1 do  
-- Look for an SI prefix; should never occur with an alias.  
-- Check for longer prefix first ('dam' is decametre).  
-- SIprefixes[1] = prefix maximum #characters (as seen by mw.ustring.sub).  
local prefix = usub(unitcode, 1, plen)  
local si = SIprefixes[prefix]  
if si then  
local t = units[usub(unitcode, plen+1)]  
if t and t.prefixes then  
local result = shallow_copy(t)  
if (sp == 'us' or t.sp_us) and si.name_us then  
result.si_name = si.name_us  
else  
result.si_name = si.name  
end  
result.si_prefix = si.prefix or prefix  
-- 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  
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 or result.engscale) then  
result.defkey = unitcode -- key to lookup default exception  
result.engscale = engscale  
-- Do not set result.scale as this code is called for units where that is not set.  
return result  
end  
end  
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  
quit_no_message('Invalid default expression.')  
elseif arithop == '*' then  
factor = tonumber(factor)  
if factor == nil then  
quit_no_message('Invalid default expression.')  
end  
value = value * factor  
end  
limit = tonumber(limit)  
if limit == nil then  
quit_no_message('Invalid default expression.')  
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  
quit_no_message('Invalid default expression.') -- 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 adapted code-----------------------------------------------------  

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  

-- Tables of units: k = unit code, v = unit table.  
local units_index = {} -- all units: normal, alias, per, combination, or multiple  
local alias_index = {} -- all aliases (to detect attempts to define more than once)  
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.  
-- Otherwise, ucode may represent an automatically generated combination  
-- where each component must have the given utype; a dummy unit is returned.  
if empty(ucode) then  
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  

local overrides = {} -- read from input for unit codes that should not be checked for a duplicate  

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)  
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 not default:find('!', 1, true) 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)  
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)  
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,  
-- some units are defined merely for use in per units, and they have  
-- the same ucode, utype and default.  
-- Example: unit cent which cannot be converted to anything other than  
-- a cent, but which can work, for example, in cent/km and cent/mi.  
-- Throw an error if a problem occurs.  
local done = {}  
for _, default in ipairs(check_default_expression(default, ucode)) do  
if done[default] then  
quit('m_def_rpt', default, ucode)  
end  
if default == ucode and ucode ~= utype then  
quit('m_def_same', ucode)  
end  
local default_table = get_unit(default, utype)  
if not default_table then  
quit('m_def_undef', default, ucode)  
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(cfg, units)  
-- 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 not unit.shouldbe and not unit.combination 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 not unit.target then -- unit should have a default  
missing:add(ucode)  
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  
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')  
local limit = cfg.maxerrors  
for _, v in ipairs(missing) do  
limit = limit - 1  
if limit < 0 then  
add_warning('m_wrn_more')  
break  
end  
add_warning('m_wrn_ucode', v)  
end  
end  
end  

local function check_all_pers(cfg, units)  
-- 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 function errmsg(key, ...)  
errors:add(message(key, ...))  
end  
for _, unit in ipairs(units) do  
local per = unit.per  
if per then  
local ucode = unit.unitcode  
if #per ~= 2 then  
errmsg('m_per_two', ucode)  
else  
local types = {}  
for i, v in ipairs(per) do  
if empty(v) then  
errmsg('m_per_empty', ucode)  
end  
if not text_code.currency[v] then  
local t = get_unit(v)  
if t then  
types[i] = t.utype  
else  
errmsg('m_per_undef', ucode, v)  
end  
end  
end  
if specials.utype[unit.utype] == 'type_fuel_efficiency' then  
local expected = { type_volume = 1, type_length = 2 }  
local top_type = expected[specials.utype[types[1]]]  
local bot_type = expected[specials.utype[types[2]]]  
if top_type and bot_type and top_type ~= bot_type then  
unit.iscomplex = true  
if top_type == 1 then  
unit.invert = 1  
else  
unit.invert = -1  
end  
else  
errmsg('m_per_fuel', ucode)  
end  
end  
end  
end  
if errors.n >= cfg.maxerrors then  
break  
end  
end  
if errors.n > 0 then  
error(errors:join, 0)  
end  
end  

local function update_units(units, composites, varnames)  
-- 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  
local vn = varnames[unit.unitcode]  
if vn then  
unit.varname = vn  
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) then  
quit('m_ovr_miss')  
end  
if data[ucode] then  
quit('m_ovr_dup', ucode)  
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')  
end  
if empty(default) then  
quit('m_dfs_none', ucode)  
end  
if #fields ~= 2 then  
quit('m_dfs_two', ucode)  
end  
local unit_table = get_unit(ucode)  
if not unit_table then  
quit('m_dfs_undef', ucode)  
end  
local symbol = unit_table.defkey or unit_table.symbol  
if empty(symbol) then  
quit('m_dfs_sym', ucode)  
end  
check_default(default, ucode, utype, unit_table)  
if defaults_index[ucode] then  
quit('m_dfs_dup', ucode)  
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 link is empty and the name starts with '[[',  
-- the link is stored as '' (for a unit name which is always linked).  
-- If the resulting link is nil, no link field is stored, and  
-- if a link is required, it will be set from the unit's name.  
local original = link  
if empty(link) then  
return (name and name:sub(1, 2) == '[[') and '' or nil  
end  
local prefixes = { ['+'] = 1, ['*'] = 2, ['@'] = 3 }  
local customary = prefixes[link:sub(1, 1)]  
if customary then  
link = strip(link:sub(2))  
end  
if link:sub(1, 2) == '' then  
link = link:sub(3)  
end  
if link:sub(-2) == '' then  
link = link:sub(1, -3)  
end  
link = strip(link)  
if link:sub(1, 1) == '[' or link:sub(-1) == ']' then  
quit('m_lnk_brack', original)  
end  
if link == '' then  
link = nil  
elseif name then  
local l = ulower(usub(link, 1, 1)) .. usub(link, 2)  
local n = ulower(usub(name, 1, 1)) .. usub(name, 2)  
if l == n then  
link = nil -- link == name, ignoring case of first letter  
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')  
end  
if empty(link) then  
quit('m_lnk_none', ucode)  
end  
if #fields ~= 2 then  
quit('m_lnk_two', ucode)  
end  
local unit_table = get_unit(ucode)  
if not unit_table then  
quit('m_lnk_undef', ucode)  
end  
if utype ~= unit_table.utype then  
quit('m_lnk_type', ucode)  
end  
local symbol = unit_table.symbol  
if empty(symbol) then  
quit('m_lnk_sym', ucode)  
end  
if links_index[ucode] then  
quit('m_lnk_dup', ucode)  
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')  
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)  
end  
local result = string.format('%.17g', value)  
if result:find('[#n]') then  
-- Lua can give results like "#INF" while Scribunto gives "inf". Either is an error.  
quit('m_scl_oflow', scale)  
end  
-- Omit redundant zeros from results like '1.2e-005'.  
-- Do not bother looking for results like '1.2e+005' as 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' }) do  
if lhs == item then  
if item == 'sp' then  
if rhs == 'us' then  
unit.sp_us = true  
good = true  
end  
elseif item == 'link' then  
local tlink  
if target then  
tlink = target[item]  
end  
local link, customary = clean_link(rhs, tlink)  
if link then  
unit[item] = link  
end  
if customary then  
unit.customary = customary  
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)  
end  
unit.symlink = rhs  
good = true  
elseif item == 'multiplier' then  
if not tonumber(rhs) then  
quit('m_als_mul', unit.unitcode, rhs)  
end  
unit[item] = rhs  
good = true  
else  
if target and rhs == target[item] then  
quit('m_als_same', item, unit.unitcode)  
end  
unit[item] = rhs  
good = true  
end  
break  
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)  
else  
alias_index[ucode] = unit  
end  
if target.utype ~= utype then  
quit('m_als_type', ucode)  
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)  
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')  
end  
if empty(ucode) then  
quit('m_miss_code')  
end  
if empty(symbol) then  
quit('m_miss_sym')  
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  
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  
if not unit then  
-- Do not define an alias in terms of another alias.  
quit('m_als_undef', symbol)  
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  
-- 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 name1 then  
if name1 == symbol and not prefixes then  
-- A unit which takes an SI prefix must not have a nil name because,  
-- for example, the name for "kW" = "kilo" .. "watt" (name for "W").  
-- The "not prefixes" test is needed for bnwiki where the  
-- watt unit has the same name and symbol.  
unit.name1 = nil  
end  
else  
name1 = symbol  
end  
if name2 then  
if name2 == name1 .. plural_suffix then  
unit.name2 = nil  
end  
else  
name2 = name1 .. plural_suffix  
end  
local name1_us, name2_us = unit.name1_us, unit.name2_us  
if name1_us then  
if name1_us == name1 then  
unit.name1_us = nil  
end  
end  
if name2_us then  
if unit.name1_us then  
if name2_us == unit.name1_us .. plural_suffix then  
unit.name2_us = nil  
end  
elseif name2_us == name2 then  
unit.name2_us = nil  
end  
end  
-- 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  
unit.invert = -1  
else  
unit.builtin = extra  
end  
end  
if prefix == '~' then  
-- Magic code for units like "acre" where the symbol is not really a  
-- symbol, and output should use the singular or plural name instead.  
unit.usename = 1  
elseif prefix == '*' then  
-- Magic code for units like "pitch" which have a symbol that is the same as  
-- another unit with entries defined in the default or link exceptions tables.  
unit.defkey = ucode -- key for default exceptions  
unit.linkey = ucode -- key for link exceptions  
end  
local name_for_link  
if prefixes then  
if prefixes == 'SI' then  
unit.prefixes = 1  
elseif prefixes == 'SI2' then  
unit.prefixes = 2  
elseif prefixes == 'SI3' then  
unit.prefixes = 3  
else  
quit('m_pfx_bad', prefixes)  
end  
else  
-- Only units which do not accept SI prefixes have name_for_link set.  
-- That is because, for example, if set name_for_link = name1 for unit g,  
-- then the link is "kilogram" for kg, and "yottagram" for Yg, and so on  
-- for all prefixes. That might be desirable for some units, but not all.  
name_for_link = name1  
end  
unit.link, unit.customary = clean_link(unit.link, name_for_link)  
if prefixes then  
-- The SI prefix is always at the start (position = 1) for symbol and sym_us.  
-- However, each name (name1, name2, name1_us, name2_us) can have the SI prefix  
-- at any position, and that position can be different for each name.  
-- For enwiki, the only units with names where the prefix is not at the start  
-- are "square metre" and "cubic metre" ("square meter" and "cubic meter" for sp=us).  
-- Some other wikis want the flexibility that the prefix position can be different  
-- so the position is stored as nil (if always 1), or N (an integer, if always N),  
-- or a string of four comma-separated numbers such as "5,7,9,11" which means the  
-- prefix position for (name1, name2, name1_us, name2_us) is (5, 7, 9, 11)  
-- respectively.  
local name1, name1_us = unit.name1, unit.name1_us -- after redundancy removed  
if not name1 then  
quit('m_pfx_name')  
end  
local positions = collection  
for i, k in ipairs({ 'name1', 'name2', 'name1_us', 'name2_us' }) do  
local name = unit[k]  
local pos  
if name then  
pos = name:find('%s', 1, true)  
if pos then  
unit[k] = name:sub(1, pos - 1) .. name:sub(pos + 2)  
end  
elseif i == 2 or i == 3 then  
pos = positions[1]  
elseif i == 4 then  
pos = positions[unit.name1_us and 3 or 2]  
end  
positions:add(pos or 1)  
end  
local pos = positions[1]  
for i = 2, positions.n do  
if pos ~= positions[i] then  
pos = '"' .. positions:join(',') .. '"'  
break  
end  
end  
if pos ~= 1 then  
unit.prefix_position = pos  
end  
for _, name in ipairs({ 'symbol', 'sym_us', 'name1', 'name1_us', 'name2', 'name2_us' }) do  
unit['_' .. name] = unit[name]  
unit[name] = nil -- force call to __index metamethod so any SI prefix can be handled  
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')  
end  
unit.unitcode = v  
elseif v == '' then  
-- Ignore empty fields.  
else  
local target = get_unit(v)  
if not target then  
quit('m_cmb_undef', v, unit.unitcode)  
end  
if target.utype ~= utype then  
quit('m_cmb_type', v, unit.unitcode)  
end  
table.insert(unit.combination, v)  
end  
end  
if #unit.combination < 2 then  
quit(#unit.combination == 0 and 'm_cmb_none' or 'm_cmb_one', unit.unitcode)  
end  
insert_unique_unit(data, unit, units_index)  
end  
end  

local function make_perunit(cfg, data)  
-- Return a function which, when called, stores a table that defines a  
-- fixup for an automatic per unit. The table is stored in data (also a table).  
local pertype_index = {} -- to detect attempts to define a fixup twice  
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 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')  
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  
if not target then  
quit('m_cmp_undef', v, unitcode)  
end  
if target.utype ~= utype then  
quit('m_cmp_type', v, unitcode)  
end  
if i < 4 then  
if not target.scale then  
quit('m_mul_std', v, unitcode)  
end  
table.insert(ucodes, v)  
table.insert(scales, target.scale)  
elseif i == 4 then  
default_code = v  
else  
if scales[#scales] ~= target.scale then  
quit('m_cmp_scale', v, unitcode)  
end  
alternate_code = v  
end  
end  
elseif i == 6 then  
if v ~= '' then  
fixed_name = v  
end  
else  
quit('m_cmp_many', unitcode)  
end  
end  
if #ucodes ~= 2 then  
quit('m_cmp_two', unitcode)  
end  
if not default_code then  
quit('m_cmp_def', unitcode)  
end  
-- Component units must be specified from most-significant to least-significant,  
-- and each ratio of a pair of scales must be very close to an integer.  
-- Currently, there will be exactly two scales and one ratio.  
local ratios, count = {}, #scales  
for i = 1, count do  
local scale = tonumber(scales[i])  
if scale == nil or scale <= 0 then  
quit('m_cmp_inval', unitcode, scales[i])  
end  
scales[i] = scale  
end  
for i = 1, count - 1 do  
local ratio = scales[i] / scales[i + 1]  
local rounded = math.floor(ratio + 0.5)  
if rounded < 2 then  
quit('m_cmp_order', unitcode)  
end  
if math.abs(ratio - rounded)/ratio > 1e-6 then  
quit('m_cmp_int', unitcode)  
end  
ratios[i] = rounded  
end  
local text = { tostring(ratios[1]) }  
local function add_text(key, value)  
table.insert(text, string.format('%s = %q', key, value))  
end  
if default_code then  
add_text('default', default_code)  
end  
if alternate_code then  
add_text('unit', alternate_code)  
end  
if fixed_name then  
add_text('name', fixed_name)  
end  
local subdiv = string.format('["%s"] = { %s }', ucodes[2], table.concat(text, ', '))  
local main_code = ucodes[1]  
local item = data[main_code]  
if item then  
table.insert(item.subdivs, subdiv)  
else  
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')  
end  
unit.unitcode = v  
elseif v == '' then  
-- Ignore empty fields.  
else  
local target = get_unit(v)  
if not target then  
quit('m_mul_undef', v, unit.unitcode)  
end  
if target.utype ~= utype then  
quit('m_mul_type', v, unit.unitcode)  
end  
if not target.scale then  
quit('m_mul_std', v, unit.unitcode)  
end  
table.insert(ucodes, v)  
table.insert(scales, target.scale)  
end  
end  
if #ucodes < 2 then  
quit(#ucodes == 0 and 'm_mul_none' or 'm_mul_one', unit.unitcode)  
end  
-- Component units must be specified from most-significant to least-significant  
-- (so scale values will be in descending order),  
-- and each ratio of a pair of scales must be very close to an integer.  
-- The componenets and ratios are stored in reverse order (least significant first).  
-- This script stores a unit scale as a string (might be an expression like "5/9"),  
-- but scales in a multiple are handled as numbers (should never be expressions).  
local ratios, count = {}, #scales  
for i = 1, count do  
local scale = tonumber(scales[i])  
if scale == nil or scale <= 0 then  
quit('m_mul_scale', unit.unitcode, scales[i])  
end  
scales[i] = scale  
end  
for i = 1, count - 1 do  
local ratio = scales[i] / scales[i + 1]  
local rounded = math.floor(ratio + 0.5)  
if rounded < 2 then  
quit('m_mul_order', unit.unitcode)  
end  
if math.abs(ratio - rounded)/ratio > 1e-6 then  
quit('m_mul_int', unit.unitcode)  
end  
ratios[i] = rounded  
end  
unit.combination = reversed(ucodes)  
unit.multiple = reversed(ratios)  
insert_unique_unit(data, unit, units_index)  
end  
end  

-- To make updating 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:  
-- all_units all properties for a unit, including default output  
-- default_exceptions exceptions for default output ('kg' and 'g' have different defaults)  
-- link_exceptions exceptions for links ('kg' and 'g' have different links)  
---
-- 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,  
}]=]  

local out_unit_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 all_units = {  

local out_unit_suffix =  
}  

local out_default_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 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 =  
}  

local out_default_item = [[  
["{symbol}"] = "{default}",]]  

local out_link_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 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 =  
}  

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',  
'default',  
'link',  
'symlink',  
'customary',  
'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',  
'_name2',  
'_name2_us',  
'_symbol',  
'_sym_us',  
'prefix_position',  
'name1',  
'name1_us',  
'name2',  
'name2_us',  
'varname',  
'symbol',  
'sym_us',  
'usename',  
'usesymbol',  
'utype',  
'alttype',  
'builtin',  
'scale',  
'offset',  
'invert',  
'iscomplex',  
'istemperature',  
'exception',  
'prefixes',  
'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)  
-- Had planned to replace sequences of spaces with 4-column tabs here  
-- (because the CodeEditor now assumes the use of such tabs).  
-- However, 4-column tabs are only visible when editing a module  
-- with browser scripting and the CodeEditor enabled, and that is rare.  
-- A module is usually viewed (with 8-column tabs), and some indents  
-- would be messed up unless 8-column tabs are used. Therefore,  
-- have decided to simply replace 8 spaces at start of line with a single  
-- tab which reduces the size of the module, and is correct for viewing.  
if line:sub(1, 8) == string.rep(' ', 8) then  
line = '\t' .. line:sub(9)  
end  
results:add(line)  
end  
local first_item = ' ["' .. unit.unitcode .. '"] = {'  
local last_item = ' },'  
add_line(first_item)  
for _, k in ipairs(spec) do  
local v = unit[k]  
if v then  
local want_quotes = (type(v) == 'string' and not no_quotes[k])  
if type(v) == 'boolean' then  
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  
quit('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 ), 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 || | !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*(.*)$')  
if cleaned then  
t[2] = cleaned  
end  
end  
return t  
end  

local function prepare_section(cfg, maker, lines, section, need_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  
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 and empty(utype) then  
quit('m_hdg_lev3', line)  
end  
if line:find(nbsp, 1, true) then  
-- For example, "acre ft" does not work if it contains nbsp.  
add_warning('m_wrn_nbsp', linenumber)  
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('m_line_num', linenumber))  
if errors.n >= cfg.maxerrors then  
break  
end  
end  
else  
local level, heading = extract_heading(line)  
if level == 3 then  
utype = ulower(heading)  
elseif level == 2 then  
break  
end  
end  
end  
end  
if skip and need_section then  
quit('m_hdg_lev2', section)  
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')  
end  
local t = mw.title.new(page_title)  
if t then  
local content = t:getContent  
if content then  
if content:sub(-1) ~= '\n' then  
content = content .. '\n'  
end  
local lines = collection  
for line in string.gmatch(content, '[\t ]*(.-)[\t\r ]*\n') do  
lines:add(line)  
end  
return lines  
end  
end  
quit('m_ftl_read', page_title)  
end  

local function prepare_data(cfg, is_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 = {}, {}, {}, {}, {}, {}  
local sections = {  
{ 'overrides' , make_override , overrides , 0 },  
{ 'conversions' , make_unit , units , 0 },  
{ 'outmultiples', make_outputmultiple, units , 0 },  
{ 'combinations', make_combination , units , 0 },  
{ 'inmultiples' , make_inputmultiple , composites, 0 }, -- after all units defined so default will be defined  
{ 'defaults' , make_default , defaults , 0 },  
{ 'links' , make_link , links , 0 },  
{ 'perunits' , make_perunit , perunits , 1 },  
{ 'varnames' , make_varname , varnames , 1 },  
}  
local lines = get_page_lines(cfg.data_title)  
for _, section in ipairs(sections) do  
local heading = mtext.section_names[section[1]]  
local maker = section[2](cfg, section[3])  
local code = section[4]  
local need_section, need_utype  
if code == 0 and not is_sandbox then  
need_section = true  
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)  
return units, defaults, links, perunits  
end  

local function _makeunits(cfg, results)  
-- Read the wikitext for the conversion data.  
-- Append output to given results collection, or throw error if a problem.  
text_code = require(cfg.text_title)  
for _, name in ipairs({ 'SIprefixes', 'eng_scales', 'currency' }) do  
if type(text_code[name]) ~= 'table' then  
quit('m_ftl_table', cfg.text_title, name)  
end  
end  
local translation = text_code.translation_table  
if translation then  
if translation.plural_suffix then  
plural_suffix = translation.plural_suffix  
end  
local ts = translation.specials  
if ts then  
if ts.utype then  
specials.utype = ts.utype  
end  
if ts.ucode then  
specials.ucode = ts.ucode  
end  
end  
local tm = translation.mtext  
if tm then  
if tm.section_names then  
mtext.section_names = tm.section_names  
end  
if tm.titles then  
mtext.titles = tm.titles  
end  
if tm.messages then  
mtext.messages = tm.messages  
end  
end  
end  
local is_sandbox  
local conversion_data_title = mtext.titles.conversion_data  
if cfg.data_title and cfg.data_title ~= conversion_data_title then  
if is_test_run then  
is_sandbox = true  
data_preamble = nil  
data_postamble = nil  
out_unit_prefix = 'local all_units = {'  
out_unit_suffix = '}'  
out_default_prefix = '\nlocal default_exceptions = {'  
out_default_suffix = '}'  
out_default_item = '\t["{symbol}"] = "{default}",'  
out_link_prefix = '\nlocal link_exceptions = {'  
out_link_suffix = '}'  
out_link_item = '\t["{symbol}"] = "{link}",'  
out_perunit_prefix = '\nlocal per_unit_fixups = {'  
out_perunit_suffix = '}'  
out_perunit_item = '\t["{lhs}"] = {rhs},'  
end  
else  
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  
local args = .args  
local config = {  
data_title = args[1],  
text_title = args[2] or 'Module:Convert/text',  
varcolumns = tonumber(args.varcolumns) or 5, -- #columns in "Variable names" section; slwiki uses 5  
maxerrors = 20,  
}  
local results = collection  
local ok, msg = pcall(_makeunits, config, results)  
if not ok then  
results:add(message('m_error'))  
results:add('')  
results:add(msg)  
end  
local warn = ''  
if warnings.n > 0 then  
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  

return { makeunits = makeunits }