Toggle menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.
Revision as of 07:08, 7 June 2024 by 1F616EMO (talk | contribs) (1 revision imported)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

--- Miscellaneous useful functions.
local lib = {}

local util = require('libraryUtil')
local checkType = util.checkType
local checkTypeMulti = util.checkTypeMulti
local NIL_OK = true

--- Choose one of two values to return.
--  @param {boolean} cond Determines which value to return.
--  @param T The value to return if `cond` is true (or truthy).
--  @param F The value to return if `cond` is false (or falsey).
function lib.ternary(cond, T, F)
    if cond then
        return T
    end

    return F
end

--- Some functions from `mw.text` are slow or may not always work as intended.
--  This group of functions provides better alternatives for them.
--  @section `mw.text` replacements

--- 
--  Removes ASCII whitespace from the start and end of `text`.
--  Slightly more efficient than the `mw.text` implementation.
--  @see https://help.fandom.com/wiki/Extension:Scribunto#mw.text.trim_is_slow
--  @param {string} text The text to trim.
--  @return {string} The trimmed text.
function lib.trim(text)
	return (text:gsub( '^[\t\r\n\f ]+', '' ):gsub( '[\t\r\n\f ]+$', '' ))
	-- the "extra" parentheses are important for removing the second value returned from `gsub`
end


--- Returns an iterator over substrings that would be returned by @{lib.split}.
--  @see @{lib.split} for argument documentation.
--  @return {function} Iterator over substrings of `text` separated by `delim`.
function lib.gsplit(text, delim, opt)
	checkType('Feature.gsplit', 1, text, 'string')
	checkType('Feature.gsplit', 2, delim, 'string', NIL_OK)
	checkType('Feature.gsplit', 3, opt, 'table', NIL_OK)
	if delim == nil then delim = " " end
	if opt == nil then opt = {} end
	-- the mediawiki implementation uses ustring, which is slower than string
	-- and also not necessary if delim isn't a pattern.
	-- https://help.fandom.com/wiki/Extension:Scribunto#mw.text.split_is_very_slow
	
	-- local g = mw.text.gsplit(text, delim, true)
	-- local function f()
	-- 	local value = g()
	-- 	if value and not opt.noTrim then -- value is nil when the generator ends
	-- 		value = mw.text.trim(value)
	-- 	end
	-- 	if value == "" and opt.removeEmpty then
	-- 		return f()
	-- 	end
	-- 	return value
	-- end
	-- return f, nil, nil
	
	-- based on https://github.com/wikimedia/mediawiki-extensions-Scribunto/blob/1eecdac6def6418fb36829cc2f20b464c30e4b37/includes/Engines/LuaCommon/lualib/mw.text.lua#L222
	local s, l = 1, #text
	local function f()
		if s then
			local e, n = string.find( text, delim, s, true )
			local ret
			if not e then
				ret = string.sub( text, s )
				s = nil
			elseif n < e then
				-- Empty separator!
				ret = string.sub( text, s, e )
				if e < l then
					s = e + 1
				else
					s = nil
				end
			else
				ret = e > s and string.sub( text, s, e - 1 ) or ''
				s = n + 1
			end
			
			if not opt.noTrim then
				ret = lib.trim(ret)
			end
			if ret == '' and opt.removeEmpty then
				return f()
			end
			return ret
		end
	end
	return f, nil, nil
end

--- Returns a table containing the substrings of `text` that are separated by `delim`.
--  (Compared to @{mw.text.split}, this function always treats `delim` as a literal
--  string rather than a pattern, and it trims output substrings using @{lib.trim} by default.)
--  @param       {string} text The string to split.
--  @param[opt]  {string} delim The delimiter string to use when splitting `text`.
--                 (Using an empty string will split `text` into individual characters.)
--  @param[opt]  {table} opt Extra options:
--  @param[opt]  {boolean} opt.noTrim Set true to disable trimming of generated substrings
--                  using @{lib.trim}.
--  @param[opt]  {boolean} opt.removeEmpty Set true to omit empty substrings
--                  (i.e., when multiple `delim` appear consecutively, or when
--                  `delim` appears at the start or end of `text`).
--  @return {table} Substrings of `text separated by `delim`.
function lib.split(text, delim, opt)
	checkType('Feature.split', 1, text, 'string')
	checkType('Feature.split', 2, delim, 'string', NIL_OK)
	checkType('Feature.split', 3, opt, 'table', NIL_OK)
	local output = {}
	for item in lib.gsplit(text, delim, opt) do
		table.insert(output, item)
	end
	return output
end

--- A wrapper around @{mw.text.unstripNoWiki} that un-escapes
--  characters/sequences that <nowiki> escapes.
--  @see https://github.com/wikimedia/mediawiki/blob/c22d01f23b7fe754ef106e97bae32c3966f8db3e/includes/parser/CoreTagHooks.php#L146
--             for MediaWiki source code for <nowiki>
function lib.unstripNoWiki(str)
	return (mw.text.unstripNoWiki(str)
		:gsub('&lt;', '<'):gsub('&gt;', '>')
		:gsub('-&#123;', '-{'):gsub('&#125;-', '}-'))
	-- the "extra" parentheses are important for removing the second value returned from `gsub`
end

--- @section end

--- Returns an iterator over `tbl` that outputs items in the order defined by `order`.
--  @param      {table} tbl The table to iterate over.
--  @param[opt] {table|function} order The iteration order.
-- 
--                Can be specified either as an ordered list (table) of keys from `tbl`
--                or as an ordering function that accepts `tbl`, `keyA`, and `keyB`
--                and returns true when the entry in `tbl` associated wity `keyA`
--                should come before the one for `keyB`.
--
--                If not specified, the keys' natural ordering is used
--                (i.e., `function(tbl, a, b) return a < b end`).
--  @return {function} The iterator.
function lib.spairs(tbl, order)
	checkType('Feature.spairs', 1, tbl, 'table')
	checkTypeMulti('Feature.spairs', 2, order, {'table', 'function', 'nil'})
	local keys
	if type(order) == "table" then
		keys = order
	else
	    -- collect the keys
		keys = {}
		for k in pairs(tbl) do table.insert(keys, k) end
		
		-- sort the keys (using order function if given)
		if order then
		    table.sort(keys, function(a, b) return order(tbl, a, b) end)
		else
		    table.sort(keys)
		end
    end

    -- return the iterator function
	local i = 0
	return function()
		i = i + 1
		local key = keys[i]
		return key, tbl[key]
	end
end

--[[
	Parses Phantom Template Format strings into a list of maps.
	@param       {string} input A string formed by concatenating the output of Phantom Templates.
		Usually, this string is generated by DPL.
	@param[opt]  {string} key_separator Separator between the entries (key-value pairs) of items in `input`. Defaults to ';'.
	@param[opt]  {string} end_separator Separator between items in `input`. Defaults to '$'.
	@param[opt]  {string} equal_separator Separator between the key and value of each entry in `input`. Defaults to '='.
	@return      {table} A list of items from `input`; each value is a map of the item's entries.
--]]
function lib.parseTemplateFormat (inputStr, key_separator, end_separator, equal_separator)
	if key_separator == nil then key_separator = ";" end
	if end_separator == nil then end_separator = "$" end
	if equal_separator == nil then equal_separator = "=" end
	
	local arg_format = "^%s*(.-)%s*" .. equal_separator .. "%s*(.-)%s*$"
	
	local resultTable = {}
	for str in lib.gsplit(inputStr, end_separator, {noTrim=true, removeEmpty=true}) do
		local result = {}
		for param in lib.gsplit(str, key_separator) do
			local arg, val = param:match(arg_format)
			if arg then
				result[arg] = val
			else
				-- skip, i guess
				-- mw.log("Warning: Lua module found extra " .. key_separator .. " or " .. end_separator .. " separators in DPL output.")
			end
		end
		table.insert(resultTable, result)
	end
	return resultTable
end

--[=[
	Parses Phantom Template Format strings into a list of ordered maps.
	@param       {string} input A string formed by concatenating the output of Phantom Templates.
		Usually, this string is generated by DPL.
	@param[opt]  {string} key_separator Separator between the entries (key-value pairs) of items in `input`. Defaults to ';'.
	@param[opt]  {string} end_separator Separator between items in `input`. Defaults to '$'.
	@param[opt]  {string} equal_separator Separator between the key and value of each entry in `input`. Defaults to '='.
	@return[name=output] {table} A list of items from `input`; each value is a list of the item's entries.
	@return[name=output[i]] {table} The i-th item of `input`.
	@return[name=output[i].page] {string} The value of the `page` key for this item.
	@return[name=output[i][j]] {table} The j-th key-value pair of this item.
	@return[name=output[i][j].key] {string} The j-th key of this item.
	@return[name=output[i][j].value] The j-th value of this item.
--]=]
function lib.parseTemplateFormatOrdered (inputStr, key_separator, end_separator, equal_separator)
	if key_separator == nil then key_separator = ";" end
	if end_separator == nil then end_separator = "$" end
	if equal_separator == nil then equal_separator = "=" end
	
	local arg_format = "^%s*(.-)%s*" .. equal_separator .. "%s*(.-)%s*$"
		
	local resultTable = {}
	for str in lib.gsplit(inputStr, end_separator, {noTrim=true, removeEmpty=true}) do
		local result = {}
		for param in lib.gsplit(str, key_separator) do
			local arg, val = param:match(arg_format)
			if arg == 'page' then
				result['page'] = val
			else
				table.insert(result,{
					key = arg,
					value = val
				})
			end
		end
		table.insert(resultTable, result)
	end
	return resultTable
end

-- searches ordered table and returns value
function lib.orderedTableSearch(tbl, search)
    for i, obj in ipairs(tbl) do
        if obj.key == search then
            return obj.value
        end
    end
    return false
end

--- Add thousands separator to number `n`.
--  @param {number|frame} n If a frame is given, then its first argument (`frame.args[1]`) will be used as input instead.
--  @return {string} The number formatted with commas for thousands separators.
--  @see https://stackoverflow.com/questions/10989788/format-integer-in-lua/10992898#10992898
function lib.thousandsSeparator(n)
	if (n == mw.getCurrentFrame()) then
		n = n.args[1]
	elseif (type(n) == "table") then
		n = n[1]
	end
	
	local i, j, minus, int, fraction = tostring(n):find('([-]?)(%d+)([.]?%d*)')

	-- reverse the int-string and append a comma to all blocks of 3 digits
	int = int:reverse():gsub("(%d%d%d)", "%1,")
	
	-- reverse the int-string back remove an optional comma and put the optional minus and fractional part back
	return minus .. int:reverse():gsub("^,", "") .. fraction
end


--- @return {boolean} true iff string or table is empty
--  @note May not be correct for tables with metatables.
function lib.isEmpty(item)
	if item == nil or item == "" then
		return true
	end
	if type(item) == "table" then
		return next(item) == nil
	end
	return false
end

--- @return {boolean} true iff string or table is not empty
--  @note May not be correct for tables with metatables.
function lib.isNotEmpty(item)
	return not lib.isEmpty(item)
end

--- @return nil if string or table is empty, otherwise return the value.
function lib.nilIfEmpty(item)
	if lib.isEmpty(item) then
		return nil
	else
		return item
	end
end

---
-- @param {table} t A table of items
-- @param elm The item to search for
-- @returns true if `elm` is a value in `t`; false otherwise. (Does not check keys of `t`.)
-- @see http://stackoverflow.com/q/2282444
-- @see another implementation: Dev:TableTools.includes()
function lib.inArray(t, elm)
    for _, v in pairs(t) do
        if v == elm then
            return true
        end
    end
    return false
end

return lib