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