Module:Convert/makeunits: Difference between revisions

Content deleted Content added
something (mw.text.nowiki?) is only preserving the first tab character on each line and is misaligning the second tab, so use spaces second tab for less ugly result
accept abbr=off for an alias so it uses unit name; add pernames (names for second unit in a per) for ukwiki
 
(22 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%%-9s= %%%s,', want_quotes and 'q' or 's')
add_line(first_item)
results:add(fmt:format(k, v))
for _, k in ipairs(spec) do
end
local v = unit[k]
end
if v then
results:add(last_item)
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
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