Module:Util: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
(Added function that finds the difference between two tables) |
||
Line 1,267: | Line 1,267: | ||
end | end | ||
return tbl | return tbl | ||
end | |||
function util.table.diff(tbl1, tbl2) | |||
-- Finds the difference between two tables, returning a table containing the | |||
-- values in tbl1 that are not in tbl2. Indexing is ignored; only values are | |||
-- compared. | |||
local diff = {} | |||
for _, k in pairs(tbl1) do | |||
if not util.table.contains(tbl2, k) then | |||
table.insert(diff, k) | |||
end | |||
end | |||
return diff | |||
end | end | ||
Revision as of 23:45, 21 July 2024
This is a meta module.
This module is meant to be used only by other modules. It should not be invoked in wikitext.
Overview
Provides utility functions for programming modules.
Structure
Group | Description |
---|---|
util.cast | utilities for casting values (i.e. from arguments) |
util.html | shorthand functions for creating some html tags |
util.misc | miscellaneous functions |
Usage
This module should be loaded with require()
.
The above documentation is transcluded from Module:Util/doc.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
-------------------------------------------------------------------------------
--
-- Module:Util
--
-- This meta module contains a number of utility functions
-------------------------------------------------------------------------------
local xtable -- Lazy load require('Module:Table')
local getArgs -- Lazy load require('Module:Arguments').getArgs
local m_cargo -- Lazy load require('Module:Cargo')
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = mw.loadData('Module:Util/config')
local i18n = cfg.i18n
local util = {}
-- ----------------------------------------------------------------------------
-- util.cast
-- ----------------------------------------------------------------------------
util.cast = {}
function util.cast.text(value, args)
-- Takes an arbitary value and converts it to text.
--
-- Also strips any categories
--
-- args
-- cast_nil - Cast lua nil value to "nil" string
-- Default: false
-- discard_empty - if the string is empty, return nil rather then empty string
-- Default: true
args = args or {}
if args.discard_empty == nil then
args.discard_empty = true
end
if value == nil and not args.cast_nil then
return
end
value = tostring(value)
if value == '' and args.discard_empty then
return
end
-- Reassign to variable before returning since string.gsub returns two values
value = string.gsub(value, '%[%[Category:[%w_ ]+%]%]', '')
return value
end
function util.cast.boolean(value, args)
-- Takes an arbitrary value and attempts to convert it to a boolean.
--
-- for strings false will be according to i18n.bool_false
--
-- args
-- cast_nil if set to false, it will not cast nil values
args = args or {}
local t = type(value)
if t == 'nil' then
if args.cast_nil == nil or args.cast_nil == true then
return false
else
return
end
elseif t == 'boolean' then
return value
elseif t == 'number' then
if value == 0 then return false end
return true
elseif t == 'string' then
local tmp = string.lower(value)
for _, v in ipairs(i18n.bool_false) do
if v == tmp then
return false
end
end
return true
else
error(string.format(i18n.errors.not_a_boolean, tostring(value), t))
end
end
function util.cast.number(value, args)
-- Takes an arbitrary value and attempts to convert it to a number.
--
-- args
-- default for strings, if default is nil and the conversion fails, an error will be returned
-- min error if <min
-- max error if >max
args = args or {}
local t = type(value)
local val
if t == 'nil' then
val = nil
elseif t == 'boolean' then
if value then
val = 1
else
val = 0
end
elseif t == 'number' then
val = value
elseif t == 'string' then
val = tonumber(value)
end
if val == nil then
if args.default ~= nil then
val = args.default
else
error(string.format(i18n.errors.not_a_number, tostring(value), t))
end
end
if args.min ~= nil and val < args.min then
error(string.format(i18n.errors.number_too_small, val, args.min))
end
if args.max ~= nil and val > args.max then
error(string.format(i18n.errors.number_too_large, val, args.max))
end
return val
end
function util.cast.table(value, args)
-- Takes an arbitrary value and attempts to convert it to a table.
--
-- args
-- split_args If true, create an association table (rather than an array)
-- pattern The pattern to split strings by. Default: ',%s*'
-- split_args_pattern The pattern to split keys from values by. Ignored if split_args is not true.
-- Default: '%s*=%s*'
-- callback A callback function to call on each value
args = args or {}
local pattern = args.pattern or ',%s*'
local split_args_pattern = args.split_args_pattern or '%s*=%s*'
local tbl
if type(value) == 'string' then
if args.split_args then
tbl = util.string.split_args(value, { sep = pattern, kvsep = split_args_pattern } )
else
tbl = util.string.split(value, pattern)
end
elseif type(value) ~= 'table' then
tbl = {value}
else
tbl = value
end
if args.callback then
for k, v in ipairs(tbl) do
tbl[k] = args.callback(v)
end
end
return tbl
end
function util.cast.version(value, args)
-- Takes a string value and returns as version number
-- If the version number is invalid an error is raised
--
-- args:
-- return_type: defaults to "table"
-- table - Returns the version number broken down into sub versions as a table
-- string - Returns the version number as string
--
if args == nil then
args = {}
end
local result
if args.return_type == 'table' or args.return_type == nil then
result = util.string.split(value, '%.')
if #result ~= 3 then
error(string.format(i18n.errors.malformed_version_string, value))
end
result[4] = string.match(result[3], '%a+')
result[3] = string.match(result[3], '%d+')
for i=1,3 do
local v = tonumber(result[i])
if v == nil then
error(string.format(i18n.errors.non_number_version_component, value))
end
result[i] = v
end
elseif args.return_type == 'string' then
result = string.match(value, '%d+%.%d+%.%d+%a*')
end
if result == nil then
error(string.format(i18n.errors.unrecognized_version_number, value))
end
return result
end
function util.cast.replace_if_match(value, args)
-- Returns a function that returns its input unchanged, unless the string value
-- matches the 'pattern' argument, in which case the 'replacewith' value is returned.
if ((args == nil) or (args.pattern == nil) or (value == nil)) then
return value
elseif string.find(tostring(value),args.pattern) then
return args.replacewith
else
return value
end
end
-- ----------------------------------------------------------------------------
-- util.validate
-- ----------------------------------------------------------------------------
util.validate = {}
util.validate.factory = {}
function util.validate.factory.number_in_range(args)
-- Returns a function that validates whether a number is within a range of
-- values. An error is thrown if the value is not a number or if it is not
-- within the specified range.
args = args or {}
args.min = args.min or -math.huge
args.max = args.max or math.huge
return function (value)
if type(value) ~= 'number' then
error(string.format(i18n.errors.not_a_number, tostring(value), type(value)))
end
if value < args.min or value > args.max then
error(string.format(args.errmsg or i18n.errors.number_out_of_range, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2)
end
return value
end
end
function util.validate.factory.string_length(args)
-- Returns a function that validates whether a string has has the correct
-- length. An error is thrown if the value is not a string or if its length
-- restrictions are not met.
args = args or {}
args.min = args.min or 0
args.max = args.max or math.huge
return function (value)
if type(value) ~= 'string' then
error(string.format(i18n.errors.not_a_string, tostring(value), type(value)))
end
local length = mw.ustring.len(value)
if length < args.min or length > args.max then
error(string.format(args.errmsg or i18n.errors.string_length_incorrect, tostring(value), tostring(args.min), tostring(args.max)), args.errlvl or 2)
end
return value
end
end
function util.validate.factory.in_table(args)
-- Returns a function that validates whether a table contains a value.
-- An error is thrown if the value is not found.
args = args or {}
return function (value)
if not util.table.contains(args.tbl or {}, value) then
error(string.format(args.errmsg or i18n.errors.value_not_in_table, tostring(value)), args.errlvl or 2)
end
return value
end
end
function util.validate.factory.in_table_keys(args)
-- Returns a function that validates whether a table has a value as one of
-- its keys. An error is thrown if the key does not exist.
args = args or {}
return function (value)
if not util.table.has_key(args.tbl or {}, value) then
error(string.format(args.errmsg or i18n.errors.value_not_in_table_keys, tostring(value)), args.errlvl or 2)
end
return value
end
end
--
-- util.cast.factory
--
-- This section is used to generate new functions for common argument parsing tasks based on specific options
--
-- All functions return a function which accepts two arguments:
-- tpl_args - arguments from the template
-- frame - current frame object
--
-- All factory functions accept have two arguments on creation:
-- k - the key in the tpl_args to retrive the value from
-- args - any addtional arguments (see function for details)
util.cast.factory = {}
function util.cast.factory.array_table(k, args)
-- Arguments:
-- tbl - table to check against
-- errmsg - error message if no element was found; should accept 1 parameter
xtable = xtable or require('Module:Table')
args = args or {}
return function (tpl_args, frame)
local elements
if tpl_args[k] ~= nil then
elements = util.string.split(tpl_args[k], ',%s*')
for _, element in ipairs(elements) do
local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
if r == nil then
error(string.format(args.errmsg or i18n.errors.missing_element, element))
end
end
tpl_args[args.key_out or k] = xtable:new(elements)
end
end
end
function util.cast.factory.table(k, args)
args = args or {}
return function (tpl_args, frame)
args.value = tpl_args[k]
if args.value == nil then
return
end
local value = util.table.find_in_nested_array(args)
if value == nil then
error(string.format(args.errmsg or i18n.errors.missing_element, k))
end
tpl_args[args.key_out or k] = value
end
end
function util.cast.factory.assoc_table(k, args)
-- Arguments:
--
-- tbl
-- errmsg
-- key_out
return function (tpl_args, frame)
local elements
if tpl_args[k] ~= nil then
elements = util.string.split(tpl_args[k], ',%s*')
for _, element in ipairs(elements) do
if args.tbl[element] == nil then
error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)})
end
end
tpl_args[args.key_out or k] = elements
end
end
end
function util.cast.factory.number(k, args)
args = args or {}
return function (tpl_args, frame)
tpl_args[args.key_out or k] = tonumber(tpl_args[k])
end
end
function util.cast.factory.boolean(k, args)
args = args or {}
return function(tpl_args, frame)
if tpl_args[k] ~= nil then
tpl_args[args.key_out or k] = util.cast.boolean(tpl_args[k])
end
end
end
function util.cast.factory.percentage(k, args)
args = args or {}
return function (tpl_args, frame)
local v = tonumber(tpl_args[k])
if v == nil then
return util.html.error{msg=string.format(i18n.errors.invalid_argument, k)}
end
if v < 0 or v > 100 then
return util.html.error{msg=string.format(i18n.errors.not_a_percentage, k)}
end
tpl_args[args.key_out or k] = v
end
end
-- ----------------------------------------------------------------------------
-- util.args
-- ----------------------------------------------------------------------------
util.args = {}
function util.args.stats(argtbl, args)
-- in any prefix spaces should be included
--
-- argtbl: argument table to work with
-- args:
-- prefix: prefix if any
-- property_prefix: property prefix if any
-- subobject_prefix: subobject prefix if any
-- properties: table of properties to add if any
args = args or {}
args.prefix = args.prefix or ''
local i = 0
local stats = {}
repeat
i = i + 1
local prefix = string.format('%s%s%s_%s', args.prefix, i18n.args.stat_infix, i, '%s')
local id = {
id = string.format(prefix, i18n.args.stat_id),
min = string.format(prefix, i18n.args.stat_min),
max = string.format(prefix, i18n.args.stat_max),
value = string.format(prefix, i18n.args.stat_value),
}
local value = {}
for key, args_key in pairs(id) do
value[key] = argtbl[args_key]
end
if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then
if value.value then
value.value = util.cast.number(value.value)
argtbl[id.value] = value.value
else
value.min = util.cast.number(value.min)
argtbl[id.min] = value.min
value.max = util.cast.number(value.max)
argtbl[id.max] = value.max
-- Also set average value
value.avg = (value.min + value.max)/2
argtbl[string.format('%sstat%s_avg', args.prefix, i)] = value.avg
end
argtbl[string.format('%sstat%s', args.prefix, i)] = value
stats[#stats+1] = value
elseif util.table.has_all_value(value, {'id', 'min', 'max', 'value'}, nil) then
value = nil
-- all other cases should be improperly set value
else
error(string.format(i18n.errors.improper_stat, args.prefix, i))
end
until value == nil
argtbl[string.format('%sstats', args.prefix)] = stats
end
function util.args.spawn_weight_list(argtbl, args)
args = args or {}
args.input_argument = i18n.args.spawn_weight_prefix
args.output_argument = 'spawn_weights'
args.cargo_table = 'spawn_weights'
util.args.weight_list(argtbl, args)
end
function util.args.generation_weight_list(argtbl, args)
args = args or {}
args.input_argument = i18n.args.generation_weight_prefix
args.output_argument = 'generation_weights'
args.cargo_table = 'generation_weights'
util.args.weight_list(argtbl, args)
end
function util.args.weight_list(argtbl, args)
-- Parses a weighted pair of lists and sets properties
--
-- argtbl: argument table to work with
-- args:
-- output_argument - if set, set arguments to this value
-- input_argument - input prefix for parsing the arguments from the argtbl
-- subobject_name - name of the subobject
m_cargo = m_cargo or require('Module:Cargo')
args = args or {}
args.input_argument = args.input_argument or 'spawn_weight'
local i = 0
local id = nil
local value = nil
if args.output_argument then
argtbl[args.output_argument] = {}
end
repeat
i = i + 1
id = {
tag = string.format('%s%s_tag', args.input_argument, i),
value = string.format('%s%s_value', args.input_argument, i),
}
value = {
tag = argtbl[id.tag],
value = argtbl[id.value],
}
if value.tag ~= nil and value.value ~= nil then
if args.output_argument then
argtbl[args.output_argument][i] = value
end
if args.cargo_table then
m_cargo.store({
_table = args.cargo_table,
ordinal = i,
tag = value.tag,
weight = util.cast.number(value.value, {min=0}),
})
end
elseif not (value.tag == nil and value.value == nil) then
error(string.format(i18n.errors.invalid_weight, id.tag, id.value))
end
until value.tag == nil
end
function util.args.version(argtbl, args)
-- in any prefix spaces should be included
--
-- argtbl: argument table to work with
-- args:
-- set_properties: if defined, set properties on the page
-- variables: table of prefixes
-- ignore_unknowns: if defined, treat a version number of '?' as if it
-- were not present
-- noquery: For testing; if defined, skips the query
-- return_ids_and_keys: For testing; on return, args.version_ids and
-- args.versionkeys are set to the IDs and keys found
args = args or {}
args.variables = args.variables or {
release = {},
removal = {},
}
local version_ids={}
local version_keys={}
for key, data in pairs(args.variables) do
local full_key = string.format('%s_version', key)
if args.ignore_unknowns and (argtbl[full_key] == '?') then
argtbl[full_key] = nil
elseif argtbl[full_key] ~= nil then
local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
argtbl[full_key] = value
if value ~= nil then
data.value = value
if data.property ~= nil then
version_ids[#version_ids+1] = value
version_keys[value] = key
end
end
end
end
-- no need to do a query if nothing was fetched
if (args.noquery == nil) and (#version_ids > 0) then
for i, id in ipairs(version_ids) do
version_ids[i] = string.format('Versions.version="%s"', id)
end
local results = m_cargo.query(
{'Versions'},
{'release_date', 'version'},
{
where = table.concat(version_ids, ' OR '),
}
)
if #results ~= #version_ids then
error(string.format(i18n.too_many_versions, #results, #version_ids))
end
for _, row in ipairs(results) do
local key = version_keys[row.version]
argtbl[string.format('%s_date', key)] = row.release_date
end
end
if args.return_ids_and_keys ~= nil then
args.version_ids = version_ids
args.version_keys = version_keys
end
end
function util.args.from_cargo_map(args)
m_cargo = m_cargo or require('Module:Cargo')
return m_cargo.store_mapped_args(args)
end
function util.args.template_to_lua(str)
--[[
Convert templates to lua format. Simplifes debugging and creating
examples.
Parameters
----------
str : string
The entire template wrapped into string. Tip: Use Lua's square
bracket syntax for defining string literals.
Returns
-------
out : table
out.template - Template name.
out.args - arguments in table format.
out.args_to_str - arguments in readable string format.
]]
local out = {}
-- Get the template name:
out.template = string.match(str, '{{(.-)%s*|')
-- Remove everything but the arguments:
str = string.gsub(str, '%s*{{.-|', '')
str = string.gsub(str, '%s*}}%s*', '')
-- Split up the arguments:
out.args = {}
for i, v in ipairs(util.string.split(str, '%s*|%s*')) do
local arg = util.string.split(v, '%s*=%s*')
out.args[arg[1]] = arg[2]
out.args[#out.args+1] = arg[1]
end
-- Concate for easy copy/pasting:
local tbl = {}
for i, v in ipairs(out.args) do
tbl[#tbl+1]= string.format("%s='%s'", v, out.args[v])
end
out.args_to_str = table.concat(tbl, ',\n')
return out
end
-- ----------------------------------------------------------------------------
-- util.html
-- ----------------------------------------------------------------------------
util.html = {}
function util.html.abbr(text, title, options)
-- Outputs html tag <abbr> as string or as mw.html node.
--
-- options
-- class: class attribute
-- output: set to mw.html to return a mw.html node instead of a string
if not title then
return text
end
options = options or {}
local abbr = mw.html.create('abbr')
abbr:attr('title', title)
local class
if type(options) == 'table' and options.class then
class = options.class
else
class = options
end
if type(class) == 'string' then
abbr:attr('class', class)
end
abbr:wikitext(text)
if options.output == mw.html then
return abbr
end
return tostring(abbr)
end
function util.html.error(args)
-- Create an error message box
--
-- args
-- msg str The error message
args = args or {}
local err = mw.html.create('strong')
:addClass('error')
:tag('span')
:addClass('module-error')
:wikitext(i18n.errors.module_error .. (args.msg or ''))
:done()
return tostring(err)
end
function util.html.poe_color(label, text, class)
if text == nil or text == '' then
return nil
end
local em = mw.html.create('em')
:addClass('tc -' .. label)
:addClass(class or '')
:wikitext(text)
return tostring(em)
end
util.html.poe_colour = util.html.poe_color
function util.html.tooltip(abbr, text, class)
return string.format('<span class="hoverbox c-tooltip %s"><span class="hoverbox__activator c-tooltip__activator">%s</span><span class="hoverbox__display c-tooltip__display">%s</span></span>', class or '', abbr or '', text or '')
end
util.html.td = {}
function util.html.td.na(options)
--
-- options:
-- as_tag
-- output: set to mw.html to return a mw.html node instead of a string
options = options or {}
-- N/A table row, requires mw.html.create instance to be passed
local td = mw.html.create('td')
td
:attr('class', 'table-na')
:wikitext(i18n.na)
:done()
if options.as_tag or options.output == mw.html then
return td
end
return tostring(td)
end
function util.html.format_value(tpl_args, value, options)
-- value: table
-- min:
-- max:
-- options: table
-- func: Function to transform the value retrieved from the database
-- fmt: Format string (or function that returns format string) to use for the value.
-- Default: '%s'
-- fmt_range: Format string to use for range value.
-- Default: '(%s-%s)'
-- color: poe_color code to use for the value. False for no color.
-- Default: 'value' if value is unmodified; 'mod' if modified
-- class: Additional css class added to color tag
-- inline: Format string to use for the output
-- inline_color: poe_color code to use for the output. False for no color.
-- Default: Inherits from value color
-- inline_class: Additional css class added to inline color tag
-- no_color: (Deprecated; use color=false instead)
-- return_color: (Deprecated; returns both value.out and value without this)
-- Make shallow copy to avoid modifying the original table
local value_copy = {}
for k, v in pairs(value) do
value_copy[k] = v
end
local default_color = 'value'
local base = {
min = value_copy.base_min or value_copy.base,
max = value_copy.base_max or value_copy.base,
}
if value_copy.min ~= base.min or value_copy.max ~= base.max then
default_color = 'mod'
end
if options.color ~= false and options.no_color == nil then
value_copy.color = options.color or default_color
end
if options.func then
value_copy.min = options.func(tpl_args, value_copy.min)
value_copy.max = options.func(tpl_args, value_copy.max)
end
local fmt = options.fmt or '%s'
if type(fmt) == 'function' then -- Function that returns the format string
fmt = fmt(tpl_args, value_copy)
end
if value_copy.min == value_copy.max then -- Static value
value_copy.out = string.format(fmt, value_copy.min)
else -- Range value
local fmt_range = options.fmt_range or i18n.range
value_copy.out = string.format(
string.format(fmt_range, fmt, fmt),
value_copy.min,
value_copy.max
)
end
local inline = options.inline
if type(inline) == 'function' then
inline = inline(tpl_args, value_copy)
end
inline = inline ~= '' and inline or nil -- TODO: Eliminate the need for this?
local inline_color = options.inline_color
if value_copy.color and (not inline or inline_color ~= nil) then
value_copy.out = util.html.poe_color(value_copy.color, value_copy.out, options.class)
end
if inline then
value_copy.out = string.format(inline, value_copy.out)
if inline_color or inline_color == nil and options.color ~= false then
inline_color = inline_color or value_copy.color or default_color
value_copy.out = util.html.poe_color(inline_color, value_copy.out, options.inline_class)
end
end
if options.return_color then
return value_copy.out, value_copy.color
end
return value_copy.out, value_copy
end
function util.html.wikilink(page, text)
if text then
return string.format('[[%s|%s]]', page, text)
end
return string.format('[[%s]]', page)
end
function util.html.url_link(url, text)
return string.format('[%s %s]', url, text)
end
-- ----------------------------------------------------------------------------
-- util.misc
-- ----------------------------------------------------------------------------
util.misc = {}
function util.misc.invoker_factory(func, options)
-- Returns a function that can be called directly or with #invoke.
return function (frame)
frame = frame or {}
local args
if type(frame.args) == 'table' then
-- Called via #invoke, so use getArgs().
getArgs = getArgs or require('Module:Arguments').getArgs
args = getArgs(frame, options)
else
-- Called from another module or from the debug console, so assume args
-- are passed in directly.
args = frame
end
return func(args)
end
end
function util.misc.is_frame(frame)
-- the type of the frame is a table containing the functions, so check whether some of these exist
-- should be enough to avoid collisions.
return not(frame == nil or type(frame) ~= 'table' or (frame.argumentPairs == nil and frame.callParserFunction == nil))
end
function util.misc.get_frame(frame)
-- OBSOLETE. Use mw.getCurrentFrame() instead.
return mw.getCurrentFrame()
end
function util.misc.get_args_raw(frame)
-- Simple method for getting arguments. Use this instead of Module:Arguments
-- when the extra options provided by the latter would be overkill.
if util.misc.is_frame(frame) then
-- Called via {{#invoke:}}, so use the args that were passed into the
-- template.
return frame.args
end
-- Called from another module or from the debug console, so assume args
-- are passed in directly.
return frame
end
function util.misc.maybe_sandbox(module_name)
-- Did we load or {{#invoke:}} a module sandbox?
if module_name and package.loaded[string.format('Module:%s/sandbox', module_name)] ~= nil or string.find(mw.getCurrentFrame():getTitle(), 'sandbox', 1, true) then
return true
end
return false
end
function util.misc.add_category(categories, args)
-- categories: table of categories
-- args: table of extra arguments
-- namespace: id of namespace to validate against
-- ignore_blacklist: set to non-nil to ignore the blacklist
-- sub_page_blacklist: blacklist of subpages to use (if empty, use default)
-- namespace_blacklist: blacklist of namespaces to use (if empty, use default)
if type(categories) == 'string' then
categories = {categories}
end
if args == nil then
args = {}
end
local title = mw.title.getCurrentTitle()
local sub_blacklist = args.sub_page_blacklist or cfg.misc.category_blacklist.sub_pages
local ns_blacklist = args.namespace_blacklist or cfg.misc.category_blacklist.namespaces
if args.namespace ~= nil and title.namespace ~= args.namespace then
return ''
end
if args.ignore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
return ''
end
local cats = {}
for i, cat in ipairs(categories) do
cats[i] = string.format('[[Category:%s]]', cat)
end
return table.concat(cats)
end
-- ----------------------------------------------------------------------------
-- util.Error
-- ----------------------------------------------------------------------------
-- Prototype error object
local Error_prototype = {
message = i18n.errors.unspecified,
code = 'module_error',
issue = true, -- Whether to issue error
level = 2,
}
Error_prototype.__index = Error_prototype
function Error_prototype:throw(force)
if force or self.issue then
error(self.message, self.level)
end
return self
end
function Error_prototype:get_html()
return util.html.error{msg=self.message}
end
function Error_prototype:get_category(args)
return util.misc.add_category(self.category, args)
end
function util.Error(obj)
-- Create a new error object
obj = obj or {}
setmetatable(obj, Error_prototype)
return obj
end
-- ----------------------------------------------------------------------------
-- util.string
-- ----------------------------------------------------------------------------
util.string = {}
function util.string.trim(str, charset)
--[[
Trims leading and trailing characters in charset from a string.
Charset is '%s' by default, which matches whitespace characters
This works much like mw.text.trim, using the string library instead
of the ustring library. This function may return erroneous results
if the charset needs to be Unicode-aware.
--]]
charset = charset or '%s'
str = string.gsub(str, '^[' .. charset .. ']*(.-)[' .. charset .. ']*$', '%1')
return str
end
function util.string.strip_wikilinks(str)
--[[
Removes wikilinks from a string, leaving the plain text
--]]
str = mw.ustring.gsub(str, '%[%[:?([^%]|]+)%]%]', '%1')
str = mw.ustring.gsub(str, '%[%[:?[^|]+|([^%]|]+)%]%]', '%1')
return str
end
function util.string.strip_html(str)
--[[
Removes html tags from a string, leaving the plain text
--]]
str = mw.ustring.gsub(str, '<[^>]*>', '')
return str
end
function util.string.split(str, pattern, plain)
--[[
Splits a string into a table
This does essentially the same thing as mw.text.split, but with
significantly better performance. This function may return erroneous
results if the pattern needs to be Unicode-aware.
str String to split
pattern Pattern to use for splitting
plain If true, pattern is interpreted as a literal string
--]]
local out = {}
local init = 1
local split_start, split_end = string.find(str, pattern, init, plain)
while split_start do
out[#out+1] = string.sub(str, init, split_start-1)
init = split_end+1
split_start, split_end = string.find(str, pattern, init, plain)
end
out[#out+1] = string.sub(str, init)
return out
end
function util.string.split_outer(str, pattern, outer)
--[[
Split a string into a table according to the pattern, ignoring
matching patterns inside the outer patterns.
Parameters
----------
str : string
String to split.
pattern : string
Pattern to split on.
outer : table of strings where #outer = 2.
Table with 2 strings that defines the opening and closing patterns
to match, for example parantheses or brackets.
Returns
-------
out : table
table of split strings.
Examples
--------
-- Nesting at the end:
str = 'mods.id, CONCAT(mods.id, mods.name)'
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
table#1 {
"mods.id",
"CONCAT(mods.id, mods.name)",
}
-- Nesting in the middle:
str = 'mods.id, CONCAT(mods.id, mods.name), mods.required_level'
mw.logObject(util.split_outer(str, ',%s*', {'%(', '%)'}))
table#1 {
"mods.id",
"CONCAT(mods.id, mods.name)",
"mods.required_level",
}
]]
local out = {}
local nesting_level = 0
local i = 0
local pttrn = '(.-)' .. '(' .. pattern .. ')'
for v, sep in string.gmatch(str, pttrn) do
if nesting_level == 0 then
-- No nesting is occuring:
out[#out+1] = v
else
-- Nesting is occuring:
out[#out] = (out[math.max(#out, 1)] or '') .. v
end
-- Increase nesting level:
if string.find(v, outer[1]) then -- Multiple matches?
nesting_level = nesting_level + 1
end
if string.find(v, outer[2]) then
nesting_level = nesting_level - 1
end
-- Add back the separator if nesting is occuring:
if nesting_level ~= 0 then
out[#out] = out[#out] .. sep
end
-- Get the last index value:
i = i + #v + #sep
end
-- Complement with the last part of the string:
if nesting_level == 0 then
out[#out+1] = string.sub(str, math.max(i+1, 1))
else
out[#out] = out[#out] .. string.sub(str, math.max(i+1, 1))
-- TODO: Check if nesting level is zero?
end
return out
end
function util.string.split_args(str, args)
-- Splits arguments string into a table
--
-- str: String of arguments to split
-- args: table of extra arguments
-- sep: separator to use (default: ,)
-- kvsep: separator to use for key value pairs (default: =)
local out = {}
if args == nil then
args = {}
end
args.sep = args.sep or ','
args.kvsep = args.kvsep or '='
if str ~= nil then
local row
for _, str in ipairs(util.string.split(str, args.sep)) do
row = util.string.split(str, args.kvsep)
if #row == 1 then
out[#out+1] = row[1]
elseif #row == 2 then
out[row[1]] = row[2]
else
error(string.format(i18n.number_of_arguments_too_large, #row))
end
end
end
return out
end
function util.string.format(format, ...)
--[[
String replacement with support for numbered argument conversion
specifications. This is useful for i18n, as translating can sometimes
change the order of words around.
The format can contain either numbered argument conversion specifications
(i.e., "%n$"), or unnumbered argument conversion specifications (i.e., "%"),
but not both.
If numbered argument conversion specifications are not needed, consider
using string.format() from the Lua string library instead.
Example:
local format = 'Bubba ate %2$d %1$s. That\'s a lot of %1$s!'
util.string.format(format, 'hotdogs', 26)
-> Bubba ate 26 hotdogs. That's a lot of hotdogs!
]]
local values = {}
for v in string.gmatch(format, '%%(%d+)%$') do
values[#values+1] = select(v, ...)
end
if #values == 0 then
-- Using unnumbered argument conversion specifications, so just pass
-- args to string.format().
return string.format(format, ...)
end
format = string.gsub(format, '%%%d+%$', '%%')
return string.format(format, unpack(values))
end
function util.string.first_to_upper(str)
--[[
Converts the first letter of a string to uppercase
--]]
-- Reassign to variable before returning since string.gsub returns two values
str = str:gsub('^%l', string.upper)
return str
end
util.string.pattern = {}
function util.string.pattern.valid_var_name()
--[[
Get a pattern for a valid variable name.
]]
return '%A?([%a_]+[%w_]*)[^%w_]?'
end
-- ----------------------------------------------------------------------------
-- util.table
-- ----------------------------------------------------------------------------
util.table = {}
function util.table.length(tbl)
-- Get number of elements in a table. Counts both numerically indexed
-- elements and associative elements. Does not count nil elements.
local count = 0
for _ in pairs(tbl) do
count = count + 1
end
return count
end
util.table.count = util.table.length
function util.table.contains(tbl, value)
-- Checks whether a table contains a value
for _, v in pairs(tbl) do
if v == value then
return true
end
end
return false
end
function util.table.has_key(tbl, key)
-- Checks whether a table has a key
return tbl[key] ~= nil
end
function util.table.has_any_key(tbl, keys)
-- Checks whether a table has at least one of the keys
for _, key in ipairs(keys or {}) do
if tbl[key] ~= nil then
return true
end
end
return false
end
function util.table.has_all_keys(tbl, keys)
-- Checks whether a table has all of the keys
for _, key in ipairs(keys or {}) do
if tbl[key] == nil then
return false
end
end
return true
end
function util.table.keys(tbl)
-- Returns the keys of a table
local keys = {}
for k, _ in pairs(tbl) do
keys[#keys+1] = k
end
return keys
end
util.table.assoc_to_array = util.table.keys
function util.table.column(tbl, colkey, idxkey)
--[[
Returns the values of one column of a multi-dimensional table
tbl A multi-dimensional table
colkey The column key from the inner tables
idxkey If provided, the column from the inner tables to index the
returned values by. Default: nil
--]]
local col = {}
for _, row in pairs(tbl) do
if type(row) == 'table' and row[colkey] ~= nil then
if idxkey ~= nil and row[idxkey] ~= nil then
col[row[idxkey]] = row[colkey]
else
col[#col+1] = row[colkey]
end
end
end
return col
end
function util.table.merge(...)
--[[
Merges the keys and values of multiple tables into a single table. If
the input tables share non-numerical keys, then the later values for those
keys will overwrite the previous ones. Numerical keys are instead appended
and renumbered, incrementing from 1.
--]]
local tbl = {}
for _, t in ipairs({...}) do
for k, v in pairs(t) do
if type(k) == 'number' then
table.insert(tbl, v)
else
tbl[k] = v
end
end
end
return tbl
end
function util.table.diff(tbl1, tbl2)
-- Finds the difference between two tables, returning a table containing the
-- values in tbl1 that are not in tbl2. Indexing is ignored; only values are
-- compared.
local diff = {}
for _, k in pairs(tbl1) do
if not util.table.contains(tbl2, k) then
table.insert(diff, k)
end
end
return diff
end
function util.table.has_all_value(tbl, keys, value)
-- Whether all the table values with the specified keys are the specified value
for _, k in ipairs(keys or {}) do
if tbl[k] ~= value then
return false
end
end
return true
end
function util.table.has_one_value(tbl, keys, value)
-- Whether one of table values with the specified keys is the specified value
for _, k in ipairs(keys or {}) do
if tbl[k] == value then
return true
end
end
return false
end
function util.table.find_in_nested_array(args)
-- Iterates thoguh the given nested array and finds the given value
--
-- ex.
-- data = {
-- {a=5}, {a=6}}
-- find_nested_array{arg=6, tbl=data, key='a'} -> 6
-- find_nested_array(arg=10, tbl=data, key='a'} -> nil
-- -> returns "6"
--
-- args: Table containing:
-- value: value of the argument
-- tbl: table of valid options
-- key: key or table of key of in tbl
-- rtrkey: if key is table, return this key instead of the value instead
-- rtrvalue: default: true
local rtr
if type(args.key) == 'table' then
for _, item in ipairs(args.tbl) do
for _, k in ipairs(args.key) do
if item[k] == args.value then
rtr = item
break
end
end
end
elseif args.key == nil then
for _, item in ipairs(args.tbl) do
if item == args.value then
rtr = item
break
end
end
else
for _, item in ipairs(args.tbl) do
if item[args.key] == args.value then
rtr = item
break
end
end
end
if rtr == nil then
return rtr
end
if args.rtrkey ~= nil then
return rtr[args.rtrkey]
elseif args.rtrvalue or args.rtrvalue == nil then
return args.value
else
return rtr
end
end
-- ----------------------------------------------------------------------------
-- util.Struct
-- ----------------------------------------------------------------------------
util.Struct = function(map)
local this = {map = map}
-- sets a value to a field
function this:set(field, value)
if not field or not value then
error('One or more arguments are nils')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
if _.validate then
_.value = _.validate(value)
else
_.value = value
end
-- this happen if 'validate' returns nil
if _.required == true and _.value == nil then
error(string.format('Field "%s" is required but has been set to nil', field))
end
end
-- adds a new prop to a field
function this:set_prop(field, prop, value)
if not field or not prop or not value then
error('One or more arguments are nils')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
_[prop] = value
end
-- gets a value from a field
function this:get(field)
if not field then
error('Argument field is nil')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
return _.value
end
-- gets a value from a prop field
function this:get_prop(field, prop)
if not field or not prop then
error('One or more arguments are nils')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
return _[prop]
end
-- shows a value from a field
function this:show(field)
if not field then
error('Argument field is nil')
end
local _ = self.map[field]
if not _ then
error(string.format('Field "%s" doesn\'t exist', field))
end
if _.show then
return _.show(_)
else
return _.value
end
end
return this
end
-- ----------------------------------------------------------------------------
return util