Module:Core

Documentation for this module may be created at Module:Core/doc

local Utils = {}

-- * Function functions :V

function Utils.id(...)
	return ...
end

function Utils.second(_, x)
    return x
end

-- * Number functions.

function Utils.round(x)
	return math.floor(x + 0.5)
end

function Utils.divMod(x, y)
	return math.floor(x / y), x % y
end

-- * Collection functions.

function Utils.isize(xs)
	local r = 0
	for _ in ipairs(xs) do
		r = r + 1
	end
	return r
end

function Utils.size(xs)
	local r = 0
	for _ in pairs(xs) do
		r = r + 1
	end
	return r
end

function Utils.removekey(xs, k)
	local e = xs[k]
	xs[k] = nil
	return e
end

function Utils.isArray(xs)
	for k, _ in pairs(xs) do
		return k == 1
	end
	return true
end

function Utils.findBy(xs, p)
    if Utils.isArray(xs) then
	    for k, v in ipairs(xs) do
            if p(v, k) then
			    return v, k
		    end
        end
    else
	    for k, v in pairs(xs) do
		    if p(v, k) then
			    return v, k
		    end
        end
    end
	return nil
end

function Utils.find(tbl, v_, k_)
	for _, v in pairs(tbl) do
		if k_ and v and v[k_] == v_ or not k_ and v == v_ then
			return v
		end
	end
	return false
end

function Utils.includes(tbl, v_, k_)
	for _, v in pairs(tbl) do
		if k_ and v and v[k_] == v_ or not k_ and v == v_ then
			return true
		end
	end
	return false
end

function Utils.map(tbl, fn)
	local result = {}
	for k, v in pairs(tbl) do
		table.insert(result, fn(v, k))
	end
	return result
end
 
function Utils.filter(tbl, pred)
	local result = {}
	for k, v in pairs(tbl) do
		if pred(v, k) then
			table.insert(result, v)
		end
	end
	return result
end

function Utils.first(tbl)
    for k, v in pairs(tbl) do
        return k, v
    end
end

function Utils.keys(tbl)
	local result = {}
	for k, _ in pairs(tbl) do
		table.insert(result, k)
	end
	return result
end

function Utils.values(tbl)
	local result = {}
	for _, v in pairs(tbl) do
		table.insert(result, v)
	end
	return result
end

function Utils.ifind(arr, v_)
	for i, v in ipairs(arr) do
		if v == v_ then
			return i
		end
	end
	return false
end

function Utils.ifindBy(arr, p)
	for i, v in ipairs(arr) do
		if p(v) then
			return v, i
		end
	end
	return false
end

function Utils.imax(arr, def)
    local maxValue
    local maxIndex
	for i, v in ipairs(arr) do
		if not maxValue or v > maxValue then
			maxValue = v
			maxIndex = i
		end
	end
	return maxValue or def
end

function Utils.imin(arr, def)
    local minValue
    local minIndex
	for i, v in ipairs(arr) do
		if not minValue or v < minValue then
			minValue = v
			minIndex = i
		end
	end
	return minValue or def
end

function Utils.imap(arr, fn)
	local result = {}
	for i, v in ipairs(arr) do
		table.insert(result, fn(v, i))
	end
	return result
end

function Utils.ifilter(arr, pred)
	local result = {}
	for i, v in ipairs(arr) do
		if pred(v, i) then
			table.insert(result, v)
		end
	end
	return result
end

function Utils.ifirst(arr)
    for k, v in ipairs(arr) do
        return k, v
    end
end

function Utils.ilast(arr)
	return arr[#arr]
end

function Utils.insertNew(arr, el)
    if not Utils.find(arr, el) then
        table.insert(arr, el)
    end
end

function Utils.concat(arr1, arr2)
    for i = 1, #arr2 do
        arr1[#arr1 + 1] = arr2[i]
    end
    return arr1
end

Utils.join = table.concat

function Utils.ijoin(arr, sep)
    sep = sep or ""
    return table.concat(arr, sep)
end

function Utils.joinLines(arr)
  return table.concat(arr, "\n")
end

function Utils.icopy(arr)
    return Utils.imap(arr, function(v) return v end)
end

function Utils.sort(tbl, f)
    table.sort(tbl, f)
    return tbl
end

function Utils.isort(arr, f)
    local result = Utils.icopy(arr)
    table.sort(result, f)
    return result
end

function Utils.isum(arr, result)
    result = result or 0
    for _, v in ipairs(arr) do
        result = result + (v or 0)
    end
    return result
end

-- * Number functions.

function Utils.round(x)
    return x % 2 ~= 0.5 and math.floor(x + 0.5) or x - 0.5
end

-- * String functions.

function Utils.pad(s, n, c)
    c = c or " "
    n = n or 0
    s = tostring(s) or ""
    return #s < n and string.rep(c, n - #s) .. s or s
end

function Utils.trim(s)
    return string.gsub(s, "^%s*(.-)%s*$", "%1")
end

function Utils.startsWith(s, ss)
    return string.sub(s, 1, string.len(ss)) == ss
end

-- Capitalize each word in a string.
function Utils.capitalize(s)
    s = s:gsub("(%s)(%l)", function(a, b) return a .. string.upper(b) end)
    s = s:gsub("^(%l)", function(a) return string.upper(a) end)
    return s
end

-- * Wikitext/HTML functions.

function Utils.category(name)
    return "[[" .. "Category:" .. name .. "]]"
end

function Utils.red(s)
    return Utils.format{[[<span style="color:red">${s}</span>]], s = s}
end

-- * Calling arbitrary Lua functions using #invoke.

-- Used to call Formatting:tooltip in Template:Tooltip, mainly because Lua code properly escapes characters,
-- so that span's title attribute always works.
function Utils.method(frame)
	local m = require("Module:" .. frame.args[1])
	local f = frame.args[2]
	local args = {}
	for k, v in ipairs(frame.args) do
		if type(k) == "number" and k >= 3 and type(v) == "string" then
			table.insert(args, v)
		end
	end
	return m[f](m, unpack(args))
end

-- * Frame functions.

local getArgs = require("Module:GetArgs")

-- Unused.
function Utils.getContext(frame)
    local frame1 = frame:getParent()
    if frame1 then
        local frame2 = frame1:getParent()
        if frame2 then
            return { pagename = frame2:getTitle(), args = getArgs{ frame = frame2 } }
        else
            return { pagename = frame1:getTitle(), args = getArgs{ frame = frame1 } }
        end
    else
        return { pagename = frame:getTitle(), args = getArgs{ frame = frame } }
    end
end

-- getParent -> getArgs
function Utils.getParentArgs(frame)
    local frame1 = frame:getParent()
    if frame1 then
        return getArgs{ frame = frame1 }
    else
        return nil
    end
end

-- getArgs + getParent -> getArgs, "implicit" args can be defined in the template (e.g. pagename={{PAGENAME}})
-- "explicit" args are user defined.
function Utils.getTemplateArgs(frame)
    local frame1 = frame:getParent()
    if frame1 then
        return { implicit = getArgs{ frame = frame }, explicit = getArgs{ frame = frame1 } }
    else
        return { implicit = getArgs{ frame = frame }, explicit = {} }
    end
end

function Utils.requireModule(name)
    local success, data = pcall(function () return require(string.format("Module:%s", name)) end)
    -- module without return (or empty, nil, false, true return) gives success = true, data = true
    if data == true then
        return false, nil
    else
        return success, data
    end
end

function Utils.loadData(name)
    local success, data = pcall(function () return mw.loadData(string.format("Module:%s", name)) end)
    -- TODO: ???
    if data == true then
        return false, nil
    else
        return success, data
    end
end

-- * Testing functions.

function Utils.debugPrint(x, i)
    i = i or 0
    if type(x) == "table" then
        for k, v in pairs(x) do
            mw.log(
                string.rep("  ", i) .. tostring(k) .. " : " .. type(k) .. " = " ..
                (type(v) == "table" and "table" or tostring(v) .. " : " .. type(v))
            )
            if type(v) == "table" then
                Utils.debugPrint(v, i + 1)
            end
        end
    else
        mw.log(tostring(x) .. " : " .. type(x))
    end
end

local function showValue(v)
    return type(v) == "string" and string.format('"%s"', v) or type(v) == "function" and '"function"' or tostring(v)
end

function Utils.js(x, i)
    i = i or 0
    local r = ""
    if type(x) == "table" then
        r = "{\n"
        for k, v in pairs(x) do
            if type(v) == "table" then
                r = r .. string.rep("  ", i + 1) .. tostring(k) .. ": " .. Utils.js(v, i + 1)
            else
                r = r .. string.rep("  ", i + 1) .. tostring(k) .. ": " .. showValue(v) .. ",\n"
            end
        end
        r = r .. string.rep("  ", i) .. (i == 0 and "}\n" or "},\n")
    else
        return showValue(x)
    end
    return r
end

function Utils.registerFormatTests(obj, tests, fn)
    obj.run_format_tests = function()
        for _, test in ipairs(tests) do
            local result = obj.format(nil, test)
            mw.log(fn and fn(result) or result)
        end
    end
end

function Utils.registerTableTests(obj, tests, fn)
    obj.run_table_tests = function()
        for _, test in ipairs(tests) do
            local result = obj:Table(test)
            mw.log(fn and fn(result) or result)
        end
    end
end

function Utils.test(obj, desc, fn)
    obj.test = function()
        mw.log(desc)
        fn(obj)
        mw.log("ok")
    end
end

Utils.log = mw.log

function Utils.format(s, lookup)
	if not lookup then
		lookup = s
		s = lookup[1]
		table.remove(lookup, 1)
	end
	return (string.gsub(s, '${([^%.%!%:%}%[%]]+)%[?([^%.%!%:%}%]]*)%]?%.?([^%!%:%}]*)!?([^%:%}]?):?([^%}]*)}', 
		function(name, element, attribute, conversion, format_spec)
			--local start_of_value, end_of_value, value = string.find(x, '^${(.-)[[.!:}]')
			--local start_of_access, end_of_access, access = string.find(x, '^${.-%[(.-)%][!:}]')
			--if not access then
			--	start_of_access, end_of_access, access = string.find(x, '^${[^:]-%.(.-)[!:}]')
			--end
			--local start_of_conversion, end_of_conversion, conversion = string.find(x, '^${.-!(.)[:}]')
			--local start_of_format_spec, end_of_format_spec, format_spec = string.find(x, ':(.*)}$')

			local value = lookup[name]
			if string.len(element) > 0 then
				value = value[element]
			elseif string.len(attribute) > 0 then
				value = value[attribute]
			end

			if string.len(conversion) > 0 then
				if conversion == 's' then
					value = tostring(value)
				end
			end

			if string.len(format_spec) > 0 then
				local start_of_sign, end_of_sign, sign = string.find(format_spec, '([+%- ])')
				local start_of_width, end_of_width, width, comma, precision, option = string.find(format_spec, '(%d*)(,?)([.0-9]*)([bcdeEfFgGnosxX%%]?)$')
				precision = string.sub(precision, 2)
				local number = tonumber(value)
				if #width > 0 then
					if number then
						value = string.format(string.format(number % 1 ~= 0 and "%%0%s.%sf" or "%%0%sd", width, #precision > 0 and precision or 0), number)
					end
					value = string.format(string.format("%%0%ss", width), value)
				elseif #precision > 0 and number then
					value = string.format(string.format("%%0%s.%sf", width, precision), number)
				end
				if sign then
					if number and number > 0 then
						if sign == "+" then
							value = "+" .. value
						elseif sign == " " then
							value = " " .. value
						end
					end
				end
			end

			return value
		end))
end

function Utils.split(string, separator, max_split, plain)
	assert(separator ~= '')
	assert(max_split == nil or max_split >= 1)

	local default_separator = false

	local result = {}

	if not separator or separator == '' then
		separator = '%s+'
		plain = false
		string = mw.text.trim(string)
		if string == '' then
			return result
		end
	end

	max_split = max_split or -1

	local item_index, start_index = 1, 1
	local separator_start, separator_end = mw.ustring.find(string, separator, start_index, plain)
	while separator_start and max_split ~= 0 do
		result[item_index] = mw.ustring.sub(string, start_index, separator_start - 1)
		item_index = item_index + 1
		start_index = separator_end + 1
		separator_start, separator_end = mw.ustring.find(string, separator, start_index, plain)
		max_split = max_split - 1
	end
	result[item_index] = mw.ustring.sub(string, start_index)

	return result
end

Utils.romanizable = {'zh-hans', 'zh-hant', 'zh-cn', 'zh-tw', 'zh-hk', 'zh-sg', 'zh-mo', 'zh-my', 'ja', 'ko', 'vi'}

Utils.normalization_table = {[' '] = ' ', ['~'] = '~', ['!'] = '!', ['?'] = '?'}

-- checks if string is set and if it's non-empty
function Utils.isset(target)
  return target ~= nil and target ~= ""
end

--[[ simulates the (a ? b : c) notation of C/C++ and PHP languages.
     Similar to {{#if:{{{a|}}}|{{{b|}}}|{{{c|}}}}} from parser functions. ]]
function Utils.cv(a, b, c)
  if a then return b else return c end
end

--slices a table to return another table containing values within a certain range
--source: http://snippets.luacode.org/snippets/Table_Slice_116
function Utils.sliceTable (values,i1,i2)
    local res = {}
    local n = #values
    -- default values for range
    i1 = i1 or 1
    i2 = i2 or n
    if i2 < 0 then
        i2 = n + i2 + 1
    elseif i2 > n then
        i2 = n
    end
    if i1 < 1 or i1 > n then
        return {}
    end
    local k = 1
    for i = i1,i2 do
        res[k] = values[i]
        k = k + 1
    end
    return res
end

-- checks if a given page exists
function Utils.exists(page)
  if not Utils.isset(page) then return false end
  return mw.getCurrentFrame():preprocess('{{#ifexist:' .. page .. '|1|0}}') == '1'
end

--[[ Tries to get contents of a page with given name.
     If the function fails it returns nil (page doesn't exist or can't be loaded)
     On success it returns the contents of page (it can be be partially preprocessed, so watch out for parser markers). ]]
function Utils.getPage(name)
  if not Utils.isset(name) then return nil end

  local frame = mw.getCurrentFrame()

  -- do a safe call to catch possible errors, like "template doesn't exist"
  local stat,page = pcall(frame.expandTemplate, frame, {title = ':' .. name})

  if not stat then
    -- TODO: 'page' contains the error message. Do some debugging?
    return nil
  end

  return page
end

function Utils.stripTags(text)
  if Utils.isset(text) then
    local tmp
    repeat
      tmp = text
      -- a pair of tags, like <td style="">...</td>
      text = string.gsub(text, '<%s*(%w+).->(.-)<%s*/%s*%1%s*>', '%2')
      -- closed tag, like <br/>
      text = string.gsub(text, '<%s*%w+%s*/%s*>', '')
    until tmp == text
  end
  return text
end

--[[ Sort table and remove repeating elements.
     Since tbl is passed as reference, any changes in it will affect the passed table. ]]
function Utils.trunkTable(tbl)
  table.sort(tbl)
  local last
  local redo
  repeat
    redo = false
    last = nil
    for k,v in pairs(tbl) do
      if v ~= last then
        last = v
      else
        table.remove(tbl, k)
        redo = true
        break
      end
    end
  until not redo
end

--[[ Checks if a given value is among the elements of a table and returns its index.
      Returns nil if it can't find it. ]]
function Utils.isInTable(tbl, val)
  for k,v in pairs(tbl) do
    if v == val then return k end
  end
  return nil
end

--[[ Compare 'n' elements in two tables, starting from 's1' in first table and 's2' in second table. ]]
function Utils.partialTableCompare(t1, t2, s1, s2, n)
  if n < 1 then return true end -- basically there's nothing to compare, so no differences were found

  for i = 0,(n-1) do
    -- Note that nil values are also valid.
    if t1[s1+i] ~= t2[s2+i] then return false end
  end

  return true
end

-- naming based on "Navbox" elements
Utils.main_colors = {
  game = {border = '#A88580', title = '#FFC9C2', above = '#FFD1CA', group = '#FFD9D2', subgroup = '#FFE1DA', dark = '#FFEEE8', background = '#FFF4EE'};
  music = {border = '#A8A077', title = '#FFF3B4', above = '#FFF6C0', group = '#FFF7C8', subgroup = '#FFF8D0', dark = '#FFFBE4', background = '#FFFBEE'};
  printwork = {border = '#9298A8', title = '#DDE6FF', above = '#E1E7FF', group = '#E6E9FF', subgroup = '#EAECFF', dark = '#EDF2FF', background = '#F4F9FF'};
}

-- function for easy adding of styles to html tags
function Utils.addStyle(tbl, val)
  if Utils.isset(val) then
    local av = tostring(val):gsub("^%s*(.-)%s*$", "%1")
    if string.sub(av, -1) ~= ';' then av = av .. ';' end
    tbl[#tbl+1] = av
  end
end

return Utils