Module:Shared: Difference between revisions

From Melvor Idle
(Move getNamespacedID & getLocalID from Module:GameData, such that modules don't have to require an expensive module to use these functions)
m (Add functions for parsing numbers)
Tag: Reverted
Line 456: Line 456:
end
end
return namespace, localID
return namespace, localID
end
-- Converts a string to a numeric value, or the default value if the
-- string isn't a number.
function p.toNumberOrDefault(str, def)
local num = tonumber(str)
if num then
return num
else
return def
end
end
-- Attempts to convert a string to a numeric value.
-- Throws an error if the value isn't a number.
function p.toNumberOrError(str, errorMsg)
local num = tonumber(str)
local msg = errorMsg
if msg == nil then
msg = "NaN"
end
if num then
return num
else
error(msg)
end
end
end


return p
return p

Revision as of 22:38, 29 February 2024

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

--So there are a handful of functions that I'm using in a lot of places
--So rather than continue to copy across the same handful of functions to every single new module
--I'm just going to unify them here
--Here's how you use this:
-- 1. When you're making a new module, add this near the top:
--        local Shared = require( "Module:Shared" )
-- 2. When you need to make a call to one of these functions,
--    just preface it with "Shared."
--    So for example you could call tableCount like so:
--        Shared.tableCount(data)
--This whole module copied wholesale from the Warframe wiki (https://warframe.fandom.com/wiki/Module:Shared)
--I removed a couple irrelevant functions, but otherwise did not change anything

local p = {}

-- iterator sorted by keys
-- For example, if you had a table that looked something like
-- data = {["Cat"] = 5,
--         ["Bat"] = 4,
--         ["Hat"] = 7}
-- You could do
--  for k, v in skpairs(data) do...
-- And your loop would start with k="Bat", v=4 then go to k="Cat", v=5, 
--         and finally to k="Hat", v=7
--Originally snagged this from Module:VoidByReward written by User:NoBrainz
function p.skpairs(t, revert)
	local keys = {}
	for k in pairs(t) do keys[#keys + 1] = k end
	if revert ~= nil then
		table.sort(keys, function(a, b) return a > b end)
	else
		table.sort(keys)
	end

	local i = 0
	local iterator = function()
		i = i + 1
		local key = keys[i]
		if key then
			return key, t[key]
		else
			return nil
		end
	end
	return iterator
end


--General purpose function for going through a table after sorting based on a custom sort order
--Taken from https://stackoverflow.com/questions/15706270/sort-a-table-in-lua
function p.spairs(t, order)
	-- collect the keys
	local keys = {}
	for k in pairs(t) do keys[#keys+1] = k end

	-- if order function given, sort by it by passing the table and keys a, b,
	-- otherwise just sort the keys 
	if order then
		table.sort(keys, function(a,b) return order(t, a, b) end)
	else
		table.sort(keys)
	end

	-- return the iterator function
	local i = 0
	return function()
		i = i + 1
		if keys[i] then
			return keys[i], t[keys[i]]
		end
	end
end

-- conveniently shifts BLAH to Blah
-- Handy when formatting data in ALL CAPS or all lower case
--Originally snagged this from Module:VoidByReward written by User:NoBrainz
function p.titleCase(head, tail)
	if tail == nil then
		--Split into two lines because don't want the other return from gsub
		local result = string.gsub(head, "(%a)([%w_']*)", p.titleCase)
		return result
	else
		return string.upper(head) .. string.lower(tail)
	end
end

-- Returns the number of rows in a table
-- Originally snagged this from Module:VoidByReward written by User:NoBrainz
-- Note from User:Cephalon Scientia:
--      Length operator (#) doesn't work as expected for tables that have been
--      loaded into a module by mw.loadData().
--      Use this function to get all the rows in a table regardless of them
--      being keys, values, or tables
-- pre : table is a table with no explicit nil values
-- post: returns the size of table, ignoring keys with nil values and 
--       nil values themselves
--       if table is not of type 'table' then return nil
function p.tableCount(table)
	if (type(table) == 'table') then
		local count = 0
		for _ in pairs(table) do count = count + 1 end
		return count
	else
		return nil
	end
end

-- Returns true if the table is empty, false otherwise
function p.tableIsEmpty(table)
	if type(table) == 'table' then
		for k, v in pairs(table) do
			return false
		end
		return true
	else
		return nil
	end
end

-- Returns the number of indexed elements in a table
-- pre : table is a table with no explicit nil values
-- post: returns the number of indexed elements in a table
--       if table is not of type 'table' then return nil
function p.indexCount(table)
	if (type(table) == 'table') then
		local count = 0
		for _ in ipairs(table) do count = count + 1 end
		return count
	else
		return nil
	end
end

--Sorts theTable based on the listed column
function p.tableSort(theTable, sortCol, ascend)
	local sorter = function(r1, r2)
					if ascend then
						return r1[sortCol] < r2[sortCol]
					else
						return r1[sortCol] > r2[sortCol]
					end
				end
	table.sort(theTable, sorter)
end

--Splits a string based on a sent in separating character
--For example calling splitString ("Lith V1 Relic", " ") would return {"Lith", "V1", "Relic"}
function p.splitString(inputstr, sep)
	if sep == nil then
		sep = "%s"
	end
	local t = {}
	for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
		table.insert(t, str)
	end
	return t
end

--Returns 'true' if a string starts with something
--For example calling startsWith ("Lith V1 Relic", "Lith") would return true
function p.startsWith(string1, start)
	return string.sub(string1, 1, string.len(start)) == start
end

--Adds commas
function p.formatnum(number)
	if tonumber(number) == nil then
		return number
	else
		local result = number
		while true do
			-- Format in blocks of 3 digits at a time until formatting is complete
            local k
			result, k = string.gsub(result, "^(-?%d+)(%d%d%d)", '%1,%2')
			if k == 0 then
				break
			end
		end
		return result
	end
end

function p.formatNumber(frame)
	local number = frame.args ~= nil and frame.args[1] or frame
	return p.formatnum(number)
end

function p.round(val, maxDigits, minDigits)
	if val == nil then
		return nil
	else
		if type(maxDigits) == "table" then
			minDigits = maxDigits[2]
			maxDigits = maxDigits[1]
		end

		local result = val..""
		local decimals = string.find(result, "%.")
		if decimals ~= nil then
			decimals = string.len(result) - decimals
		else
			decimals = 0
		end

		if maxDigits ~= nil and decimals > maxDigits then
			result = string.format("%."..maxDigits.."f", result)
		elseif minDigits ~= nil and decimals < minDigits then
			result = string.format("%."..minDigits.."f", result)
		end

		return result
	end
end

--From http://lua-users.org/wiki/SimpleRound
function p.round2(num, numDecimalPlaces)
	local mult = 10^(numDecimalPlaces or 0)
	return math.floor(num * mult + 0.5) / mult
end

-- pre : List is a table or a string
--       Item is the element that is being searched
--       IgnoreCase is a boolean; if false, search is case-sensitive
-- post: returns a boolean; true if element exists in List, false otherwise
function p.contains(List, Item, IgnoreCase)
	if List == nil or Item == nil then 
		return false 
	end
	if IgnoreCase == nil then 
		IgnoreCase = false 
	end

	if type(List) == "table" then
		for key, value in pairs(List) do
			if value == Item then
				return true, key
			elseif IgnoreCase and string.upper(value) == string.upper(Item) then
				return true, key
			end
		end
	else
		local start = string.find(List, Item)
		return start ~= nil
	end
	return false
end
 
--Stolen from http://lua-users.org/wiki/StringTrim
--Trims whitespace. Not quite sure how it works.
--I know how it works
--replaces "^%s*(.-)%s*$" with "%1" in str
--^%s*(.-)%s*$ matches:
--^:beginning of string
--%s*:any number of spaces
--(.-):any number of any character, minimum possible, saved to %1
--%s* again
--$: end of string
--%1 inserts the content of the parentheses
--pretty simple if you know the meanings
--User:Giga Martin
function p.trim(str)
	return (str:gsub("^%s*(.-)%s*$", "%1"))
end
 
-- generic function that checks to see if a key exists in a given nested table
-- added by User:Cephalon Scientia
-- pre : table is a nested table
--       key is a string that represents a key name
--       length is a integer that represents the size of outer table; 
--       if omitted, length is set to size of outer table
-- post: returns a boolean; true if key exists in table, false otherwise or
--       if key contains a nil value
function p.hasKey(table, key, length)
	if (length == nil) then
		length = p.tableCount(table)
	end

	-- iterating through outer table
	for i = 1, length, 1 do
		local elem = table[i]   -- storing one of inner tables into a variable
		if (elem[key] ~= nil) then
			return true
		end
	end
	return false
end

-- copies the contents of a variable; handy for when you might want to modify an object taken from a data file
-- or any other read-only variable
-- Stolen from https://gist.github.com/tylerneylon/81333721109155b2d244
function p.clone(obj)
	if type(obj) ~= 'table' then return obj end
	local res = {}
	for k, v in pairs(obj) do res[p.clone(k)] = p.clone(v) end
	return res
end

-- Shallow clone, desirable when operations such as sorting are to be performed
-- on a table where it is not necessary to perform a deep clone of all data within
-- the table's elements
function p.shallowClone(obj)
	if type(obj) ~= 'table' then return obj end
	local res = {}
	for k, v in pairs(obj) do
		res[k] = v
	end
	return res
end

-- Euclidean Greatest Common Divisor algorithm
function p.gcd(a, b)
	if b ~= 0 then
		return p.gcd(b, a % b)
	else
		return math.abs(a)
	end
end

--Formats a pair of numbers as a reduced fraction
function p.fraction(n, d)
	local gcd = p.gcd(n, d)
	return p.formatnum(n/gcd)..'/'..p.formatnum(d/gcd)
end

--Similar to p.fraction but returns the simplified numerator and denomerator separately without formatting
function p.fractionpair(n, d)
	local gcd = p.gcd(n, d)
	return n / gcd, d / gcd
end

function p.timeString(timeInSeconds, shorten)
	local remain = timeInSeconds
	local days, hours, minutes = 0, 0, 0
	local isShort = shorten

	local pieces = {}

	if remain >= 86400 then
		days = math.floor(remain / 86400)
		remain = remain - days * 86400
		if isShort then
			table.insert(pieces, days..'d')
		elseif days > 1 then
			table.insert(pieces, days..' days')
		else
			table.insert(pieces, days..' day')
		end
	end
	if remain >= 3600 then
		hours = math.floor(remain / 3600)
		remain = remain - hours * 3600
		if isShort then
			table.insert(pieces, hours..'h')
		elseif hours > 1 then
			table.insert(pieces, hours..' hours')
		else
			table.insert(pieces, hours..' hour')
		end
	end
	if remain >= 60 then
		minutes = math.floor(remain / 60)
		remain = remain - minutes * 60
		if isShort then
			table.insert(pieces, minutes..'m')
		elseif minutes > 1 then
			table.insert(pieces, minutes..' minutes')
		else
			table.insert(pieces, minutes..' minutes')
		end
	end
	if remain > 0 then
		if isShort then
			table.insert(pieces, remain..'s')
		elseif remain > 1 then
			table.insert(pieces, remain..' seconds')
		else
			table.insert(pieces, remain..' second')
		end
	end
	return table.concat(pieces, ', ')
end

function p.fixPagename(pageName)
	local result = pageName
	result = string.gsub(result, "%%27", "'")
	result = string.gsub(result, "&#39;", "'")
	result = string.gsub(result, "&#38;", "&")
	return result
end

--Checks if two tables contain the same value with the same indices
function p.tablesEqual(t1, t2)
	if p.tableCount(t1) ~= p.tableCount(t2) then return false end
	for i, val in p.skpairs(t1) do
		if type(val) ~= type(t2[i]) then
			return false
		elseif type(val) == 'table' then
			if not p.tablesEqual(val, t2[i]) then return false end
		elseif t2[i] ~= val then 
			return false 
		end
	end
	return true
end

--Returns a number including the sign, even if positive
function p.numStrWithSign(number)
	if number >= 0 then
		return '+'..p.formatnum(number)
	else
		return p.formatnum(number)
	end
end

-- Applies formatting to an error message for display on wiki pages.
-- Also appends a category such that errors can be easily located
function p.printError(message)
	-- Prevent message being interpreted as wikitext and handle non-string messages
	local messageString = mw.text.nowiki(type(message) == 'string' and message or mw.dumpObject(message))
	return '[[Category:Pages with script errors]]<div class="text-negative">ERROR: ' .. messageString .. '</div>'
end

-- Takes a description template & template data, returning a description with variables populated
function p.applyTemplateData(descTemplate, templateData)
    local resultDesc = descTemplate
    for k, v in pairs(templateData) do
        local val = v
        if type(v) == 'number' then
            val = p.formatnum(val)
        end
        resultDesc = string.gsub(resultDesc, '${' .. k .. '}', val)
    end
    return resultDesc
end

-- Given a namespace & local ID, returns a namespaced ID
function p.getNamespacedID(namespace, localID)
	if string.find(localID, ':') == nil then
		return namespace .. ':' .. localID
	else
		-- ID already appears to be namespaced
		return localID
	end
end

-- Given a namespaced ID, returns both the namespace & local ID
function p.getLocalID(ID)
	local namespace, localID = nil, nil
	local sepIdx = string.find(ID, ':')
	if sepIdx == nil then
		-- Provided ID doesn't appear to be namespaced
		localID = ID
	else
		namespace = string.sub(ID, 1, sepIdx - 1)
		localID = string.sub(ID, sepIdx + 1, string.len(ID))
	end
	return namespace, localID
end

-- Converts a string to a numeric value, or the default value if the
-- string isn't a number.
function p.toNumberOrDefault(str, def)
	local num = tonumber(str)
	if num then
		return num
	else
		return def
	end
end

-- Attempts to convert a string to a numeric value.
-- Throws an error if the value isn't a number.
function p.toNumberOrError(str, errorMsg)
	local num = tonumber(str)
	local msg = errorMsg
	
	if msg == nil then
		msg = "NaN"
	end
	
	if num then
		return num
	else
		error(msg)
	end
end

return p