Module:Util

From Path of Exile 2 Wiki
Revision as of 22:20, 24 September 2016 by >OmegaK2 (i -> em because of nesting issues)
Jump to navigation Jump to search
Module documentation[view] [edit] [history] [purge]


This is a meta module.

This module is meant to be used only by other modules. It should not be invoked in wikitext.

Lua logo

This module depends on the following other modules:

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().

-- Utility stuff

local xtable = require('Module:Table')
local util = {}

-- ----------------------------------------------------------------------------
-- util.cast
-- ----------------------------------------------------------------------------

util.cast = {}
util.cast.bool_false = {'false', '0', 'disabled', 'off', 'no', ''}

function util.cast.boolean(value)
    -- Takes an abitary value and casts it to a bool value
    -- 
    -- for strings false will be according to util.cast.bool_false
    local t = type(value)
    if t == 'nil' then
        return false
    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(util.cast.bool_false) do
            if v == tmp then
                return false
            end
        end
        return true
    else
        error(string.format('value "%s" of type "%s" is not a boolean', value, t))
    end
    
end

function util.cast.number(value, args)
    -- Takes an abitary value and attempts to cast it to int
    --
    -- 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
    if args == nil then
        args = {}
    end
    
    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('value "%s" of type "%s" is not an integer', tostring(value), t))
        end
    end
    
    if args.min ~= nil and val < args.min then
        error(string.format('"%i" is too small. Minimum: "%i"', val, args.min))
    end
    
    if args.max ~= nil and val > args.max then
        error(string.format('"%i" is too large. Maximum: "%i"', val, args.max))
    end
    
    return val
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('Malformed version string "%s"', 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('"%s" has an non-number 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('"%s" is not a recognized version number', value))
    end
    
    return result
end

-- ----------------------------------------------------------------------------
-- util.args
-- ----------------------------------------------------------------------------

util.args = {}

util.args.stat_properties = {
    id = 'Has %sstat id',
    min = 'Has minimum %sstat value',
    max = 'Has maximum %sstat value',
    avg = 'Has average %sstat value',
    value = 'Has %sstat value',
}

function util.args.stats(argtbl, args)
    -- in any prefix spaces should be included
    --
    -- argtbl: argument table to work with
    -- args:
    --  prefix: prefix if any
    --  frame: frame used to set subobjects; if not set dont set properties
    --  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 ''
    args.property_prefix = args.property_prefix or ''
    args.subobject_prefix = args.subobject_prefix or ''
    args.properties = args.properties or {}
    
    local i = 0
    local stats = {}
    repeat
        i = i + 1
        local id = {
            id = string.format('%sstat%s_id', args.prefix, i),
            min = string.format('%sstat%s_min', args.prefix, i),
            max = string.format('%sstat%s_max', args.prefix, i),
            value = string.format('%sstat%s_value', args.prefix, i),
        }
        
        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
            local properties = {}
            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
            
            if args.frame ~= nil then
                local properties = {}
                for property, value in pairs(args.properties) do
                    properties[property] = value
                end
                
                for key, property in pairs(util.args.stat_properties) do
                    properties[string.format(property, args.property_prefix)] = value[key]
                end
                
                util.smw.subobject(args.frame, string.format('%sstat%s_%s', args.subobject_prefix, i, value.id), properties)
            end
        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('%sstat%s is improperly set; id and either value or min/max must be specified.', args.prefix, i))
        end
    until value == nil
    
    argtbl[string.format('%sstats', args.prefix)] = stats
end

function util.args.version (argtbl, args)
    -- in any prefix spaces should be included
    --
    -- argtbl: argument table to work with
    -- args:
    --  frame: frame for queries
    --  set_properties: if defined, set properties on the page
    --  variables: table of prefixes of
    --   property: property; if not set skip fetching and setting release date
    args = args or {}
    args.variables = args.variables or {
        release = {
            property = 'Has release',
        },
        removal = {
            property = 'Has removal',
        },
    }
    
    local version_ids = {}
    local version_keys = {}
    
    for key, data in pairs(args.variables) do
        local full_key = string.format('%s_version', key)
        if argtbl[full_key] ~= nil then
            local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
            argtbl[full_key] = value
            data.value = value
            if data.property ~= nil then
                version_ids[#version_ids+1] = value
                version_keys[value] = key
            end
        end
    end
    
    -- no need to do a query if nothing was fetched
    if #version_ids > 0 then
        if args.frame == nil then
            error('Properties were set, but frame was not')
        end
    
        local query = {}
        query[#query+1] = string.format('[[Is version::%s]]', table.concat(version_ids, '||'))
        query[#query+1] = '?Has release date#'
        query[#query+1] = '?Is version'
        
        local results = util.smw.query(query, args.frame)
        
        if #results ~= #version_ids then
            error(string.format('The number of results (%s) does not match the number version arguments (%s)', #results, #version_ids))
        end
        
        for _, row in ipairs(results) do
            local key = version_keys[row['Is version']]
            argtbl[string.format('%s_date', key)] = row['Has release date']
        end
    end
    
    if args.set_properties ~= nil then
        local properties = {}
        for key, data in pairs(args.variables) do
            if data.property ~= nil then
                properties[string.format('%s version', data.property)] = argtbl[string.format('%s_version', key)]
                properties[string.format('%s date', data.property)] = argtbl[string.format('%s_date', key)]
            end
        end
        
        util.smw.set(args.frame, properties)
    end
end

-- ----------------------------------------------------------------------------
-- util.html
-- ----------------------------------------------------------------------------

util.html = {}
function util.html.abbr(abbr, text, class)
    return string.format('<abbr title="%s" class="%s">%s</abbr>', text or '', class or '', abbr or '')
end

function util.html.error(args)
    -- Create an error message box
    --
    -- Args:
    --  msg - message
    if args == nil then
        args = {}
    end
    
    local err = mw.html.create('span')
    err
        :attr('class', 'module-error')
        :wikitext('Module Error: ' .. (args.msg or ''))
        :done()
        
    return tostring(err)
end

function util.html.poe_color(label, text)
    if text == nil or text == '' then
        return nil
    end
    return tostring(mw.html.create('em')
        :attr('class', 'tc -' .. label)
        :wikitext(text))
end

util.html.td = {}
function util.html.td.na(args)
    --
    -- Args:
    --  as_tag
    args = args 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('N/A')
        :done()
    if args.as_tag then
        return td
    else
        return tostring(td)
    end
end

-- ----------------------------------------------------------------------------
-- util.misc
-- ----------------------------------------------------------------------------

util.misc = {}
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)
    if util.misc.is_frame(frame) then
        return frame
    end
    return mw.getCurrentFrame()
end

util.misc.category_blacklist = {}
util.misc.category_blacklist.sub_pages = {
    doc = true,
    sandbox = true,
    sandbox2 = true,
    testcases = true,
}

util.misc.category_blacklist.namespaces = {
    Template = true,
    Template_talk = true,
    Module = true,
    Module_talk = true,
}

function util.misc.add_category(categories, args)
    -- categories: table of categories
    -- args: table of extra arguments
    --  namespace: id of namespace to validate against
    --  ingore_blacklist: set to non-nil to ingore 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 util.misc.category_blacklist.sub_pages
    local ns_blacklist = args.namespace_blacklist or util.misc.category_blacklist.namespaces
    
    if args.namespace ~= nil and title.namespace ~= args.namespace then
        return ''
    end
    
    if args.ingore_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

function util.misc.raise_error_or_return(args)
    -- 
    -- Arguments:
    -- args: table of arguments to this function (must be set)
    --  One required:
    --  raise_required: Don't raise errors and return html errors instead unless raisae is set in arguments
    --  no_raise_required: Don't return html errors and raise errors insetad unless no_raise is set in arguments
    --
    --  Optional:
    --  msg: error message to raise or return, default: nil
    --  args: argument directory to validate against (e.x. template args), default: {}
    args.args = args.args or {}
    args.msg = args.msg or ''
    if args.raise_required ~= nil then
        if args.args.raise ~= nil then
            error(args.msg)
        else
            return util.html.error{msg=args.msg}
        end
    elseif args.no_raise_required ~= nil then
        if args.args.no_raise ~= nil then
            return util.html.error{msg=args.msg}
        else
            error(args.msg)
        end
    else
        error('Invalid usage of raise_error_or_return.')
    end
end

-- ----------------------------------------------------------------------------
-- util.smw
-- ----------------------------------------------------------------------------

util.smw = {}

util.smw.data = {}
util.smw.data.rejected_namespaces = xtable:new({'User'})

function util.smw._parser_function(frame, parser_function, args)
    -- Executes a semantic parser functions and sets the arguments args
    --
    -- This function is a helper for handling tables since +sep= parameter
    -- appears to be broken.
    --
    -- frame          : frame object
    -- parser_function: the whole parser function string
    -- args           : table of arguments    
    for k, v in pairs(args) do
        if type(v) == 'table' then
            for _, value in ipairs(v) do
                frame:callParserFunction(parser_function, {[k] = value})
            end
            args[k] = nil
        elseif type(v) == 'boolean' then
            args[k] = tostring(v)
        end
    end
    frame:callParserFunction(parser_function, args)
end

function util.smw.set(frame, args)
    util.smw._parser_function(frame, '#set:', args)
end

function util.smw.subobject(frame, id, args)
    util.smw._parser_function(frame, '#subobject:' .. id, args)
end

function util.smw.query(query, frame)
    -- Executes a semantic media wiki #ask query and returns the result as an
    -- array containing each row as table.
    --
    -- query: table of query arguments to pass
    -- frame: current frame object
    
    -- the characters here for sep/header/propsep are control characters; I'm farily certain they should not appear in regular text.
    query.sep = '�'
    query.propsep = '<PROP>'
    query.headersep = '<HEAD>'
    query.format = 'array'
    query.headers = 'plain'
    
    local result = frame:callParserFunction('#ask', query)
    
    -- "<span class=\"smw-highlighter\" data-type=\"4\" data-state=\"inline\" data-title=\"Error\"><span class=\"smwtticon warning\"></span><div class=\"smwttcontent\">Some subquery has no valid condition.</div></span>"
    if mw.ustring.find(result, 'data%-title="Error"') ~= nil then
        error(mw.ustring.sub(result, mw.ustring.find(result, '<span class="smw-highlighter"', 1, true), -1))
    end

    local out = {}
    
    for row_string in string.gmatch(result, '[^�]+') do
        local row = {}
        for _, str in ipairs(util.string.split(row_string, query.propsep)) do
            local kv = util.string.split(str, query.headersep)
            if #kv == 1 then
                row[#row+1] = kv[1] 
            elseif #kv == 2 then
                row[kv[1]] = kv[2]
            end
        end
        out[#out+1] = row
    end
    
    return out
end

function util.smw.safeguard(args)
    -- Used for safeguarding data entry so it doesn't get added on user space stuff
    --
    -- Args:
    --  smw_ingore_safeguard - ingore safeguard and return true
    if args == nil then
        args = {}
    end

    if args.smw_ingore_safeguard then
        return true
    end
    
    local namespace = mw.site.namespaces[mw.title.getCurrentTitle().namespace].name
    if util.smw.data.rejected_namespaces:contains(namespace) then
        return false
    end
    
    return true
end


-- ----------------------------------------------------------------------------
-- util.string
-- ----------------------------------------------------------------------------

util.string = {}
function util.string.split(str, pattern)
    -- Splits string into a table
    --
    -- str: string to split
    -- pattern: pattern to use for splitting
    out = {}
    local i = 1
    local split_start, split_end = string.find(str, pattern, i)
    while split_start do
        out[#out+1] = string.sub(str, i, split_start-1)
        i = split_end+1
        split_start, split_end = string.find(str, pattern, i)
    end
    out[#out+1] = string.sub(str, i)
    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('Number of arguments near = is too large (%s).', #row))
            end
        end
    end
    
    return out
end

-- ----------------------------------------------------------------------------
-- util.table
-- ----------------------------------------------------------------------------

util.table = {}
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

-- ----------------------------------------------------------------------------

return util