Module:Sandbox/User:Oo00oo00oO/utils

From Illerai

This is an old revision of this page, as edited by illerai>Oo00oo00oO at 13:33, 19 April 2024. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
Module documentation
This documentation is transcluded from Module:Sandbox/User:Oo00oo00oO/utils/doc. [edit] [history] [purge]

Provides default documentation for sandboxes in the module namespace.

Lua error in package.lua at line 80: module 'Module:Debug' not found.


local p = {}
p.tests = {}
p.__THIS_MODULE_PATH = mw.title.getCurrentTitle().fullText -- "Module:Sandbox/User:Oo00oo00oO/utils";
local lang = mw.language.getContentLanguage()
-------------------------------------
-- Game specific info --

function p.getGameTaxRate()
	if mw.site.server:lower():find("oldschool") then
		return {thresh = 100, rate = 0.01}
	else
		--assume rs3
		return {thresh = 100, rate = 0.02}
	end
end

-------‐----------------------------- 
-- QUEUE/STACK list type --
p.List = {}
function p.List.new ()
  return {first = 0, last = -1, data={}}
end
function p.List.pushleft (list, value)
  local first = list.first - 1
  list.first = first
  list.data[first] = value
end
function p.List.pushright (list, value)
  local last = list.last + 1
  list.last = last
  list.data[last] = value
end
function p.List.popleft (list)
  local first = list.first
  if first > list.last then error("list is empty") end
  local value = list.data[first]
  list.data[first] = nil        -- to allow garbage collection
  list.first = first + 1
  return value
end
function p.List.popright (list)
  local last = list.last
  if list.first > last then error("list is empty") end
  local value = list.data[last]
  list.data[last] = nil         -- to allow garbage collection
  list.last = last - 1
  return value
end
function p.List.peekleft(list) 
	local first = list.first
  if first > list.last then error("list is empty") end
  return list.data[first]
end
function p.List.peekright(list)
	local last = list.last
  if list.first > last then error("list is empty") end
  return list.data[last]
end
function p.List.size(list)
	return list.last - list.first + 1
end
function p.List.contains(list, val)
	for i=list.first, list.last, 1 do
		if list.data[i] == val then
			return true
		end
	end
	return false
end
function p.List.foreach(list, func) 
	for i=list.first, list.last, 1 do
		func(list.data[i], i-list.first+1)
	end
end
function p.List.join(list, str)
	local ret = ""
	local i = list.first
	while i < list.last do
		ret = ret .. tostring(list.data[i]) .. str
		i = i+1
	end
	if i==list.last and list.last ~= list.first then
		ret = ret .. tostring(list.data[i])
	end
	return ret
end
-------‐-----------------------------
-- Data conversion --
--- converts lua data to a an html <pre> element containing the jsonified data
function p.convertToJson(data, asPreBlock)
	asPreBlock = asPreBlock == nil and true or asPreBlock
	local jsonified = mw.text.jsonEncode( data, mw.text.JSON_PRESERVE_KEYS + mw.text.JSON_PRETTY)
	if not asPreBlock then
		return jsonified
	end
	local html = mw.html.create('pre')
	html:newline()
	html:wikitext(jsonified)
	html:newline()
	html:done():allDone()
	return html
end
-------‐-----------------------------
-- Test execution --
-- @param obj the table containing the test functions to be run
-- @param format will make this return the results of the tests as an html table with results
function p.runTests(obj, format, objectsToProfile)
	if objectsToProfile == nil then
		if obj["tests"] and type(obj["tests"]) == "table" then 
			objectsToProfile = obj
			obj = obj["tests"]
		else
			objectsToProfile = objectsToProfile or {}
		end
	end
	-- Module:Debug DNE in osrs wiki...
	local _debug = require(mw.site.server:find("oldschool") and "Module:Sandbox/User:Oo00oo00oO/RS3/Module:Debug" or "Module:Debug")
	local profiles = {
		["TIME"] = _debug:newProfiler("time"),
		["CALLSTACK"] = _debug:newProfiler("callstack"),
	}
	local testCoverageProfiler = _debug:newProfiler("time")
	format = tostring(format):lower()
	format = format and format
	local currFuncName = debug.traceback():match("in function (['<][^'>]+['>])")
	local count = {
		[true]=0,
		[false]=0,
	}
	profiles["TIME"]:setHooks{{table = obj, tname = "obj"}}
	profiles["CALLSTACK"]:setHooks{{table = obj, tname = "obj"}}
	testCoverageProfiler:setHooks{{table=objectsToProfile, R=true, tname="•"}}
	local subgroups = {}
	local ret = p.List.new()
	-- run tests and store result reports
	for k,v in pairs(obj) do
		if type(v) == "table" then
			subgroups[tostring(k)] = p.runTests(v, "subgroup")
		end
		if type(v) == "function" then
			-- call with try/catch and try to get full traceback
			local status, result =  xpcall(function(...) return v(arg) end, debug.traceback)
			if status == false then
				-- on error, remove this function from the bottom of the stack
				result = "<pre>" .. result:gsub("%[C%]: in function 'xpcall'\n\t" .. p.__THIS_MODULE_PATH .. ":%d+: in function " .. currFuncName .. ".*", "") .. "</pre>"
			end
			local f = status and p.List.pushleft or p.List.pushright
			f(ret, {["status"]=status, ["name"] = k, ["result"] = result})
			count[status] = count[status] + 1
		end
	end
	mw.logObject(subgroups,"subgroups")
	local funcExecStatus = nil
	if objectsToProfile then
		-- find all missing functions from the executed ones relative to the objectsToProfile
		local function extractAllFuncs(inp, out, path)
			path = path or ""
			for k,v in pairs(inp) do
				-- go to children unless its a test function.
				if type(v) == "table" and v ~= obj then
					extractAllFuncs(v, out, path .. (#path > 0 and "." or "") .. k)
				elseif type(v) == "function" then
					out[path .. (#path > 0 and "." or "") .. k] = false
				end
			end
		end
		funcExecStatus = {}
		extractAllFuncs(objectsToProfile, funcExecStatus)
		
		local funcNamePattern = "^•%.*(.*)$"
		local txt = testCoverageProfiler:report()
		local l = p.ListOfLines(txt)
		p.List.foreach(l, function(e,i)
			local m = e:match(funcNamePattern)
			if m then
				funcExecStatus[m] = true
			end
		end)
	end
	
	if format then
		local printable = mw.html.create("table")
		printable:addClass('wikitable mw-collapsible sortable sticky-header align-center-1 align-left-2 align-left-3'):tag("caption"):wikitext("TEST SUMMARY: " .. count[true] .. " PASS | " .. count[false] .. " FAIL"):done()
		-- ADD HEADERS
		printable:tag("tr"):tag("th"):wikitext("STATUS"):done():tag("th"):wikitext("TEST"):done():tag("th"):wikitext(""):done():done()
		-- ADD RESULTS
		p.List.foreach(ret, function(e, i) 
			printable
				:tag("tr")
					:tag("td")
						:wikitext(e["status"]==true and "✓" or "☓")
						:attr('data-sort-value', e["status"] == true and 1 or 0)
						:done()
					:tag("td")
						:wikitext(tostring(e["name"]))
						:attr('data-sort-value', e["name"])
						:done()
					:tag("td")
						:wikitext(tostring(e["result"]))
						:done()
					:done()
			end)
		printable:allDone()
		local profileNode = mw.html.create("table")
		profileNode:addClass('mw-collapsible mw-collapsed align-left-1 align-left-2')
		profileNode
			:tag("tr")
				:tag("th")
					:wikitext("Type")
					:css("display","inline-block")
					:css("vertical-align","top")
					:done()
				:tag("th")
					:wikitext("Profile info")
					:done()
				:done()
		for name, prof in pairs(profiles) do
			local tr = mw.html.create("tr")
			tr
				:tag("td")
					:wikitext(name)
					:done()
				:tag("td")
					:tag("pre")
						:wikitext(prof:report())
						:done()
					:done()
				:done()
			profileNode:node(tr)
		end
		-- if we have code coverage, add it
		if funcExecStatus then
			local tr = mw.html.create("tr")
			local td2pre = mw.html.create("table")
			td2pre:addClass("wikitable sortable lighttable mw-collapsible mw-collapsed align-left-1 align-center-2")
			td2pre:tag("tr"):tag("th"):wikitext("function"):done():tag("th"):wikitext("Covered?"):done():done()
			for n, s in pairs(funcExecStatus) do
				td2pre:tag("tr"):tag("td"):wikitext(n):done():tag("td"):wikitext((not not s) and "☑" or "⁉️"):done():done()
			end
			tr
				:tag("td")
					:wikitext(n)
					:done()
				:tag("td")
					:node(td2pre)
					:done()
				:done()
			profileNode:node(tr)
		end
		
		profileNode:done()
		printable:node(profileNode)
		return printable
	else
		for name, prof in pairs(profiles) do
			ret.data["PROFILE: " .. name] = prof:report()
		end
		return ret.data
	end
end

-------‐-----------------------------
-- Tables --
--- transposes the rows and columns indexable by ipairs. 
--@param tbl (table) a rectangular table to transpose (sequentially indexed starting from 1, aka passable by ipairs)
--@returns (table) the transposed table
function p.transpose(tbl)
   local transposed = {}
   for c, m_1_c in ipairs(tbl[1]) do
      local col = {m_1_c}
      for r = 2, #tbl do
         col[r] = tbl[r][c]
      end
      table.insert(transposed, col)
   end
   return transposed
end


--- will not work as expected on tables that are not perfectly rectangular or that do not meet the expected structure 
-- @param tbl (mw.html) a <table> tag of data. must have #table.tr.th for header cell and #table.tr.td for data cell. other html tags will be ignored. table data rows or columns nested any more than expected are not read. 
-- @param transposed (boolean) if true, will return the data extracted as transposed so that its by column first instead of by row.
-- @returns (table) where keys are row indexes, values are sub-tables. the table's row index sub-tables have keys that are column indexes and values are sub-tables. the column index sub-tables have keys {
-- 		["data"] = the raw data if it was extracted, (data key will be missing if failed to extract or cell was empty)
-- 		["pos"] = {true row index, true column index},
-- 	 	["src"] = reference to the source <td> cell
--		}
-- EXAMPLE:
--[==[ 
{
	Example: 
	 From a table that looks like the following:
	    +------+------+------+
	    | R1C1 | R1C2 | R1C3 |
	    |  H1  |  H2  |  H3  |
	    +------+------+------+
	    | R2C1 | R2C2 | R2C3 |
	    +------+------+------+
	    | R3C1 |      | R3C3 |  (R3C2 intentionally left blank)
	    +------+------+------+
	    | R4C1 | R4C2 | R4C3 |
	    +------+------+------+
	
	## raw table:
		local myTable = mw.html.create("table")											 |<table>
			:tag('tr')																	 |	<tr>
				:tag('th'):wikitext('R1C1'):tag('br'):done():wikitext('H1'):done()		 |		<th>R1C1<br>H1</th>
				:tag('th'):wikitext('R1C2'):tag('br'):done():wikitext('H2'):done()		 |		<th>R1C2<br>H2</th>
				:tag('th'):wikitext('R1C3'):tag('br'):done():wikitext('H3'):done()		 |		<th>R1C3<br>H3</th>
			:done()																		 |	</tr>
			:tag('tr')																	 |	<tr>
				:tag('td'):wikitext('R2C1'):done()										 |		<td>R2C1</td>
				:tag('td'):wikitext('R2C2'):done()										 |		<td>R2C2</td>
				:tag('td'):wikitext('R2C3'):done()										 |		<td>R2C3</td>
			:done()																		 |	</tr>
			:tag('tr')																	 |	<tr>
				:tag('td'):wikitext('R3C1'):done()										 |		<td>R3C1</td>
				:tag('td') --[===[this cell was intentionally left empty]===] :done()	 |		<td></td>
				:tag('td'):wikitext('R3C3'):done()										 |		<td>R3C3</td>
			:done()																		 |	</tr>
			:tag('tr')																	 |	<tr>
				:tag('td'):wikitext('R4C1'):done()										 |		<td>R4C1</td>
				:tag('td'):wikitext('R4C2'):done()										 |		<td>R4C2</td>
				:tag('td'):wikitext('R4C3'):done()										 |		<td>R4C3</td>
			:done()																		 |	</tr>
		:allDone()																		 |</table>
	
	## mw.html.indexableTable(myTable, false) returns: 
		-- (...["src"]'s are references to the actual mw.html element object)
		{
			{
				{
					["data"] = "R2C1",
					["pos"] = {
						2,
						1,
					},
					["src"] = <td>R2C1</td>,
				},
				{
					["data"] = "R2C2",
					["pos"] = {
						2,
						2,
					},
					["src"] = <td>R2C2</td>,
				},
				{
					["data"] = "R2C3",
					["pos"] = {
						2,
						3,
					},
					["src"] = <td>R2C3</td>,
				},
			},
			{
				{
					["data"] = "R3C1",
					["pos"] = {
						3,
						1,
					},
					["src"] = <td>R3C1</td>,
				},
				{
					["pos"] = {
						3,
						2,
					},
					["src"] = <td></td>,
				},
				{
					["data"] = "R3C3",
					["pos"] = {
						3,
						3,
					},
					["src"] = <td>R3C3</td>,
				},
			},
			{
				{
					["data"] = "R4C1",
					["pos"] = {
						4,
						1,
					},
					["src"] = <td>R4C1</td>,
				},
				{
					["data"] = "R4C2",
					["pos"] = {
						4,
						2,
					},
					["src"] = <td>R4C2</td>,
				},
				{
					["data"] = "R4C3",
					["pos"] = {
						4,
						3,
					},
					["src"] = <td>R4C3</td>,
				},
			},
		}
  } 
]==]
function p.htmlToIndexableTable(tbl, transposed)
	-- check args
	if type(tbl) ~= "table" or tbl.tagName ~= "table" then
		error("bad argument #1 <tbl> to 'extractColumnData' (expected to be a mw.html with tag of <table>)")
	end
	
	local dataTable = {}
	local headerIdxs = {} 
	for ir, row in pairs(tbl.nodes) do
		if type(row) == "table" and row.tagName:lower() == "tr" then 
			-- real row, get data column.
			local dataRow = {}
			for columnIdx, colNode in ipairs(row.nodes) do
				if type(colNode) == "table" and colNode.tagName:lower() == "th" then
					headerIdxs[columnIdx] = tonumber(colNode.nodes[1]) or tostring(colNode.nodes[1])
				end
				if type(colNode) == "table" and colNode.tagName:lower() == "td" then
					local dataNode = {
						-- pointer to the source mw.html <td> element object
						["src"] = colNode,
						-- {1=rowIndex, 2=columnIndex} index position from the original table (indexes start at 1)
						["pos"] = {ir, columnIdx}
					}
					-- get data from node
					-- first see if theres an attribute data-sort-value=<value>
					for i, t in pairs(colNode.attributes) do
						if type(t) == "table" then 
							if t["name"] and t["name"]:lower() == "data-sort-value" then
								-- this is the attribute that holds the data-sort-value
								dataNode.data = tonumber(t["val"])
								break
							end
						end
					end
					if dataNode.data == nil then
						-- no value got from sort-data-value attribute, so get raw value
						-- NOTE this does not work properly for comma separated numbers as strings
						dataNode.data = p.tonumber(tostring(colNode.nodes[1])) or tonumber(tostring(colNode.nodes[1])) or tostring(colNode.nodes[1])
					end
					table.insert(dataRow, dataNode)
				end
			end
			if #dataRow > 0 then 
				table.insert(dataTable, dataRow)
			end
		end
	end
	if transposed then 
		dataTable = p.transpose(dataTable)
	end
	dataTable["headerIdxs"] = headerIdxs
	return dataTable
end

--- Breadth first search on a table.
-- @param conditionFunc -> function that takes in 2 args: (key, value) and returns boolean for if that key value pair is what you are looking for
-- @returns the first node that returns true when passed to the conditionFunc:
-- {["k"]=<string>,["v"]=<any>}
function p.BSF(node, conditionFunc)
	mw.logObject(mw.dumpObject(node))
	local q = p.List.new()
	local explored = p.List.new()
	if type(node) == "table" then
		
	p.List.pushleft(q, {["k"]="",["v"]=node})
	
	repeat
		local curr = p.List.popright(q)
		if conditionFunc(curr["k"], curr["v"]) == true then
			return curr
		end
		if type(curr["v"]) == "table" then
			for k, v in pairs(curr["v"]) do
				p.List.pushleft(q, {["k"]=k, ["v"]=v})
			end
		end
	until p.List.size(q) == 0 end
	return nil
end

--- Extracts the "args" key from a given table or frame object. if none exists returns what it got.
function p.extractArgs(frame)
	if frame and type(frame["getParent"]) == "function" then
		frame.getParent = frame:getParent()
	end
	local temp = p.BSF(frame, function(k,v) return k == "args" end)
	args = (temp and temp["v"]) or frame
	return args
end

-------‐-----------------------------
-- Strings --
--- Returns the Levenshtein distance between the two given strings
-- @param str1 string 1
-- @param str2 string 2
-- @returns (number)
function p.levenshteinDistance(str1, str2)
	str1 = tostring(str1)
	str2 = tostring(str2)
	local len1 = string.len(str1)
	local len2 = string.len(str2)
	local matrix = {}
	local cost = 0
	-- quick cut-offs to save time
	if (len1 == 0) then
		return len2
	elseif (len2 == 0) then
		return len1
	elseif (str1 == str2) then
		return 0
	end
	
	-- initialise the base matrix values
	for i = 0, len1, 1 do
		matrix[i] = {}
		matrix[i][0] = i
	end
	for j = 0, len2, 1 do
		matrix[0][j] = j
	end
	
	-- actual Levenshtein algorithm
	for i = 1, len1, 1 do
		for j = 1, len2, 1 do
			if (str1:byte(i) == str2:byte(j)) then
				cost = 0
			else
				cost = 1
			end
			
			matrix[i][j] = math.min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost)
		end
	end
	
	-- return the last value - this is the Levenshtein distance
	return matrix[len1][len2]
end

--- Splits a string to a List by lines
-- @param inputstr (string) the input string to be split
-- @returns (List) a QUEUE/STACK List where each element is a line (the end of line \n not included in each element)
function p.ListOfLines(inputstr)
	local t=p.List.new()
	for str in string.gmatch(inputstr, "([^\n]+)") do
		p.List.pushright(t, str)
	end
	return t
end


--- Trims a string (both ends). Removes chars that match %s
--- Dec		Chr		Name
--- ---		----	---------------------------
---   9		'\t'	TAB (horizontal tab)
---  10		'\n'	LF (NL line feed, new line)
---  11		'\v'	VT (vertical tab)
---  12		'\f'	FF (NP form feed, new page)
---  13		'\r'	CR (carriage return)
---  32		' '		SPACE
-------------------------------------------
-- @param inputstr (string) the string to trim
-- @returns (string) the trimmed input string
function p.trim(inputStr)
	return inputStr:match("^%s*(.*)"):match("(.-)%s*$")
end


--- String startsWith
-- @param inputStr (string) the string to search
-- @param prefix (string) the prefix to match
-- @param ignoreCase (boolean) [Optional] when true, will ignore case when matching
-- @returns (boolean) if the inputStr starts with the given prefix (ignoring case if ignoreCase is truthy)
function p.startsWith(inputStr, prefix, ignoreCase)
	if ignoreCase then
		return inputStr:sub(1,#prefix):lower() == prefix:lower()
	else
		return inputStr:sub(1,#prefix) == prefix
	end
end

--- String endswith
-- @param inputStr (string) the string to search
-- @param suffix (string) the suffix to match
-- @param ignoreCase (boolean) [Optional] when true, will ignore case when matching
-- @returns (boolean) if the inputStr ends with the given suffix (ignoring case if ignoreCase is truthy)
function p.endsWith(inputStr, suffix, ignoreCase)
	if ignoreCase then
		return inputStr:sub(#inputStr - #suffix + 1, #inputStr):lower() == suffix:lower()
	else
		return inputStr:sub(#inputStr - #suffix + 1, #inputStr) == suffix
	end
end

-------‐-----------------------------
-- Numbers --
--- tonumber: Converts stringified numerical values to their number form. Can simplify basic numerical expressions as well.
-- Works with: Number, Decimal, Fractions, Scientific notation, Floating points, Numerical expressions
-- @param value any: The value to extract the number from.
-- @return number | nil: the number if it was able to be parsed, otherwise nil for invalid.
function p.tonumber(value)
	if type(value) == "number" then 
		return value
	end 
	local num = tonumber(value)
	if num ~= nil then
		return num
	end 
	local status, num = pcall(mw.ext.ParserFunctions.expr, value)
	num = status == true and num ~= "" and tonumber(num) or nil
	if num == nil and type(value) == "string" and value:match("^-?[0-9,]+%.?%d*$") then
		num = tonumber(value:gsub(",",""),10)
	end
	return num
end

--- isfinite: tests if value is numeric finite (aka not nan and not ±inf)
-- @param value (number) value to test
-- @return (boolean) true if number is finite, otherwise false. 
function p.isfinite(value) 
	return type(value) == "number" and value ~= math.huge and value ~= -math.huge and (value == value)
end

--- fnum: formats the given number to human readable text. formatted with commas and decimal points if needed. 
-- @param x (number): the value to process
-- @return (string): the stringified number.
function p.fnum(x)
	return lang:formatNum(p.tonumber(x) or 0)
end
-------‐-----------------------------
-- DEBUGGING TOOLS --
p.debug = {}

--[[ LOG_LEVEL ENUM : We print when the the given func's corresponding log level is less than or equal to the current p.debug.config.LOG.LEVEL]]--
p.debug.LOG_LEVEL = {
	FATAL    = -math.huge,
	ERR      = 0,
	ERROR    = 0,
	WARN     = 1,
	WARNING  = 1,
	INFO     = 2,
	DEBUG    = 3,
	VERBOSE  = 4,
	TRACE    = 4,
	ALL      = math.huge,
	LOG      = math.huge,

}

--[[ CONFIG: currently holds default values ]]--
p.debug.config = {
	LOG = {
		-- LOGGING LEVEL --
		LEVEL = p.debug.LOG_LEVEL.INFO,	--[[ (number): print at or below this level ]]
		-- LOG MESSAGE WITH TIMESTAMP --
		TIMESTAMP = true, --[[ (boolean): if we want to include a timestamp (Note: customizable format with config.LOG.TIMESTAMP_FORMAT) ]]
		TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S.%s", --[[ (string): time format (Note: not validated) ]]
		LOCAL_TIMEZONE_OFFSET_FROM_UTC_IN_SECONDS = 0, --[[ (number): number of seconds local timezone is offset when compared to UTC. (Note: not validated). EXAMPLE: USA EAST (+05:00) = -60*60*5]]
		-- LOG MESSAGE WITH STACK INFO --
		MINIFIED_STACK = true, --[[ (boolean): if we want to include shorthand information about the current stack ]]
		FULL_STACK = false, --[[ (boolean): if we want to include a p.LIST of the current stack (Note: see p.) ]]
	}
}

--- Updates log config settings. NOTE: Does not fully validate new value.
--- @param key any the p.debug.config.LOG key to be updated (case insensitive)
--- @param value any the value to set.
function p.debug.updateLogConfigSetting(key, value)
	key = p.trim(key):upper()
	if p.debug.config.LOG[key] == nil then 
		error("Invalid debug config key: " .. key)
	end
	if type(p.debug.config.LOG[key]) ~= type(value) then 
		error("Invalid debug config value of (<".. type(value) .. "> " .. mw.dumpObject(value) .. ") for key (" .. key .. "). Valid value type is (<" .. type(p.debug.config.LOG[key]) .. ">)!")
	end
	p.log("Updated debug logging config [" .. key .. "] = " .. mw.dumpObject(p.debug.config.LOG[key]) .. " --> ".. mw.dumpObject(value))
	p.debug.config.LOG[key] = value
end

---Generates a function that prints to STD.OUT depending on the configured log values.
---@param levelValue number the numerical value for this log function to determine if it should print or not
---@param levelName string the name/level/header to be used when printing to std.out
---@return function dynamically generated function that prints custom info to std.out. to be used the same way as mw.logObject(...)
function generateDebugLogLevelFunction(levelValue, levelName)
	-- dynamically generated function --
	---If the current configured debug.config.LOG.LOG_LEVEL allows this function to print, 
	---then this will print the given data to std.out
	---@param arg any[] each item in the argument will be printed on its own line.
	---@returns (boolean) for if a value was logged or the log was suppressed
	return function (...)
		-- We print when the the given func's corresponding log level value is less than or equal to the current p.debug.config.LOG.LEVEL value
		-- if the corresponding log level value is greater than the current p.debug.config.LOG.LEVEL value then we do not print.
		-- if we are given no data, then we do not print.
		if (arg['n'] == 0) or (levelValue > p.debug.config.LOG.LEVEL) then
			-- nothing to log
			return false
		end
		local toLog = {}
		-- log level name --
		table.insert(toLog, levelName .. " (" .. tostring(levelValue) .. ")")

		-- timestamp -- 
		if p.debug.config.LOG.TIMESTAMP == true then
			table.insert(toLog, os.date(p.debug.config.LOG.TIMESTAMP_FORMAT, os.time() + p.debug.config.LOG.LOCAL_TIMEZONE_OFFSET_FROM_UTC_IN_SECONDS))
		end

		-- stack info --
		if p.debug.config.LOG.MINIFIED_STACK == true then
			local s = ""
			local stack = p.debug.toStackTraceList()
			for i=#stack.stack, 2, -1 do
				local n = stack.stack[i]['function'] and stack.stack[i]['function']['name']
				if n then
					s = s .. (#s > 0 and "." or "") .. n
					if stack.stack[i]['location'] and stack.stack[i]['location']['lineno'] then
						s = s .. ":" .. stack.stack[i]['location']['lineno']
					elseif stack.stack[i]['function']['lineno'] then
						s = s .. ":" .. stack.stack[i]['function']['lineno']
					end
				end
			end
			table.insert(toLog, s)
		end
		
		-- data --
		table.insert(toLog, arg[1])

		-- key --
		if arg[2] then
			table.insert(toLog, arg[2])
		end

		-- where to output --
		-- STD.OUT -- 
		mw.logObject(toLog, "LOG>")
		return true
	end
end

function attachAllDebugLogFunctions()
	-- for each of the p.debug.LOG_LEVEL enums, dynamically generate a logging function with that enum name
	for enum, log_level in pairs(p.debug.LOG_LEVEL) do
		p.debug[enum:lower()] = generateDebugLogLevelFunction(log_level, enum)
	end
	p.log = generateDebugLogLevelFunction(math.huge, "log")
end

--[[ ENABLE LOGGING FUNCTIONS BY EXPLICIT LEVEL FUNCTION CALLS ]]--
attachAllDebugLogFunctions()

---converts a stack trace string to a stack like object
---@param stacktraceStr string a debug.traceback() string
--@return table with the following kvp
--[[
			message = (string): data before traceback, 
			stack = (table): where each indexed item is a (table) {
					["location"] = {
							["lineno"] = (number), -- the line number from current stack frame
							["name"] = (string), -- the line's location
					},
					["function"] = {
						["lineno"] = (number), -- the line number of the encapsulating function for the current stack frame
						["name"] = (string) -- the function's location or name,
					},
					["raw"] = (string), -- the raw value of the stack frame
]]-- 
function p.debug.toStackTraceList(stacktraceStr)
	local removeSelfFromStack = stacktraceStr == nil
	local stacktraceStr = stacktraceStr or debug.traceback()
	local message = stacktraceStr:match("(.*)stack traceback:\n")
	stacktraceStr = stacktraceStr:gsub(".*stack traceback:\n", "")
	local stacktrace = p.ListOfLines(stacktraceStr)
	if removeSelfFromStack == true then
		p.List.popleft(stacktrace) -- remove this function from stack
	end
	local stack = {
		message = message,
		stack = {}
	}
	-- parse stack string
	for i = 1, p.List.size(stacktrace), 1 do
		local lineInfo = {
			raw = p.List.popleft(stacktrace)
		}
		lineInfo.str = lineInfo.raw:gsub("^([%c%s]*)", "")	-- remove start of line whitespace
		lineInfo.str = lineInfo.str:gsub("^%p+%P+%p+: %?$", "") -- remove useless '[C]: ?'/'(tail call): ?' lines
		lineInfo['location'] = {}
		lineInfo['location']['name'], lineInfo['location']['lineno'] = lineInfo.str:match("^(.+):(%d+): ")
		if lineInfo['location']['name'] or lineInfo['location']['lineno'] then
			lineInfo.str = lineInfo.str:gsub("^(.+):(%d+): ", "")
			lineInfo['location']['lineno'] = tonumber(lineInfo['location']['lineno'])
		end
		lineInfo.str = lineInfo.str:gsub("^.*in function ", "") -- remove standard seperator
		lineInfo['function'] = {}
		lineInfo['function']['name'] = lineInfo.str:match("^[<'](.+)['>]$")
		if lineInfo['function']['name'] then
			lineInfo['function']['lineno'] = lineInfo['function']['name']:match(":(%d+)$")
			if lineInfo['function']['lineno'] then
				lineInfo['function']['lineno'] = tonumber(lineInfo['function']['lineno'])
				lineInfo['function']['name'] = lineInfo['function']['name']:gsub(":(%d+)$","")
			end
			lineInfo.str = lineInfo.str:gsub("^[<'](.+)['>]$", "")
		end
		if #lineInfo.str == 0 then
			lineInfo.str = nil
		end
		table.insert(stack.stack, lineInfo)
	end
	return stack
end

-------‐-----------------------------
--------------  TESTS  --------------
-------‐-----------------------------
function p._runUtilsTests(frame)
	-- to actually run these tests from an #invoke
	return p.runTests(p.tests, frame and true, p)
end

function p.tests.tonumber() 
	-- test NAN
	local NaN = 0/0
	local actual = p.tonumber(NaN)
	if not (actual and tonumber(NaN) ~= NaN) then
		error('Failed to catch nan case 1')
	end
	local actual = p.tonumber("0/0")
	if actual then
		error('Failed to catch nan case 2')
	end

	-- test invalid data
	local invalidDataTests = {
		'     1 2 3 4 . 5 ',
		'0/0',
		'ASD',
		'ffffff',
		'infinite',
		'',
	}
	for i, testVal in ipairs(invalidDataTests) do
		local actual = p.tonumber(testVal)
		if actual then
			error("In invalid data test #" .. i .. ": Expected (nil), got (" .. mw.dumpObject(testVal) .. ").")
		end
	end
	local validDataTests = {
	--[[{ TEST CASE								, EXPECTED RETURN						 } 		-- NOTE ]]
		{ 0										, 0										 }, 
		{ 1										, 1										 },
		{ 9876543210							, 9876543210							 },
		{ 1.7976931348623E+308					, 1.7976931348623E+308					 }, 	-- lua max float
		{ -1.7976931348622e+308					, -1.7976931348622E+308					 }, 	-- lua min float
		{ -9.981312916825e-324					, -9.8813129168249e-324					 }, 	-- Smaller than the smallest magnitude lua can handle reverts to smallest magnitude
		{ -math.huge							, -math.huge							 }, 	-- Signed infinite literal
		{ 1.79769313486234E+308					, math.huge								 }, 	-- over max float
		{ 2^0									, 2^0									 }, 	-- regular number we would encounter
		{ 2^1									, 2^1									 }, 	-- regular number we would encounter
		{ 2^2									, 2^2									 }, 	-- regular number we would encounter
		{ 2^3									, 2^3									 }, 	-- regular number we would encounter
		{ 2^4									, 2^4									 }, 	-- regular number we would encounter
		{ 2^5									, 2^5									 }, 	-- regular number we would encounter
		{ 2^6									, 2^6									 }, 	-- regular number we would encounter
		{ 2^7									, 2^7									 }, 	-- regular number we would encounter
		{ 2^8									, 2^8									 }, 	-- regular number we would encounter
		{ 9.881312916825e-324					, 9.881312916825e-324					 }, 	-- smallest magnitude of value lua can handle before zero
		{ 9.881312916825e-34					, 9.881312916825e-34					 },
		{ math.huge								, math.huge								 }, 	-- infinite literal
		{ '  000001000.010000   '				, 1000.01								 }, 	-- leading and trailing spaces and zeroes
		{ '  30303  '							, 30303									 }, 	-- leading and trailing spaces
		{ '-0'									, 0										 }, 	-- negative zero
		{ '-1000'								, -1000									 },
		{ '-inf'								, -math.huge							 }, 	-- Signed infinite literal
		{ '000001000.010000'					, 1000.01								 }, 	-- leading and trailing zeroes
		{ '030303'								, 30303									 }, 	-- leading and trailing zeroes
		{ '1,000,000'							, 1000000								 }, 	-- comma formatted integer that might be mistaken for hex
		{ '1,000.245'							, 1000.245								 }, 	-- comma formatted decimal
		{ '1.79769313486230E + 308+1.5e294'		, 1.7976931348623E+308					 }, 	-- addition right before max float
		{ '1.8E+308-1.4e+308'					, math.huge								 }, 	-- addition over max float
		{ '1.2E+13-1.4e12'						, 10600000000000						 }, 	-- lots of + and - with e
		{ '1/1'									, 1/(2^0)								 }, 	-- fraction/division
		{ '1/2'									, 1/(2^1)								 }, 	-- fraction/division
		{ '1/4'									, 1/(2^2)								 }, 	-- fraction/division
		{ '1/8'									, 1/(2^3)								 }, 	-- fraction/division
		{ '1/16'								, 1/(2^4)								 }, 	-- fraction/division
		{ '1/32'								, 1/(2^5)								 }, 	-- fraction/division
		{ '1/64'								, 1/(2^6)								 }, 	-- fraction/division
		{ '1/128'								, 1/(2^7)								 }, 	-- fraction/division
		{ '1/256'								, 1/(2^8)								 }, 	-- fraction/division
		{ '1/512'								, 1/(2^9)								 }, 	-- fraction/division
		{ '2E.2'								, 3.1697863849222						 }, 	-- wacky number with E actually converts to 2*(10^0.2)
		{ '2*E*.2'								, 1.0873127313836						 }, 	-- wacky expression with eulers E
		{ '3+3'									, 6										 }, 	-- addition
		{ '3-3'									, 0										 }, 	-- subtraction
		{ '32/64'								, 32/64									 },		--
		{ '35*0'								, 0										 }, 	-- multiplication
		{ '9.3556*10^33'						, 9.3556*10^33							 },
		{ '9.3556*10^33/9.881312916825e-34'		, 9.4679726052093e+66					 }, 	-- arbitrary multi step float expression
		{ 'inf'									, math.huge								 }, 	-- infinite literal
		{ '\t\n\n-\t0.25e3\t\n\n'				, -0.25e3								 }, 	-- funny whitespace
		{ 'E'									, 2.718281828459						 },		-- known constants are available, eulers
		{ 'PI'									, 3.1415926535898						 }, 	-- known constants are available, pi
		{ '1,234,567,634,234,234,356.98769876'	, 1.2345676342342e+18 + 34356.98769876	 }, 	-- big number comma sperated
		{ '-1,234,567,634,234,234,356.98769876'	, -1.2345676342342E+018 - 34356.98769876 }, 	-- large comma formatted decimal
	}
	for i, testVal in ipairs(validDataTests) do
		local actual = p.tonumber(testVal[1])
		local expected = testVal[2]
		if actual ~= testVal[2] then
			msg = "In valid data test #" .. i .. ": Expected " .. mw.dumpObject(testVal[1]) .. "<" .. type(testVal[1]) .. "> to result in (" ..  mw.dumpObject(expected) .. " <number>), but got (" .. mw.dumpObject(actual) .. " <" .. type(actual) .. ">)."
			if type(actual) == number then
				msg = msg + " difference of " .. tostring(actual - testVal[2])	
			end
			error(msg)
		end
	end
	
	local knownBuggyTests = {
		{ '2E.2'								, 2*10^0.2								}, 	-- it looses accuracy 
	}
	for i, testVal in ipairs(knownBuggyTests) do
		local actual = p.tonumber(testVal[1])
		local expected = testVal[2]
		if actual == testVal[2] then
			error("VALUE NO LONGER BUGGY: test #" .. i .. ": Expected (" ..  mw.dumpObject(expected) .. " <number>), got (" .. mw.dumpObject(actual) .. " <" .. type(actual) .. ">).")
		else 
			mw.logObject("In test tonumber: VALUE DOES NOT WORK AS EXPECTED: test #" .. i .. ": Expected (" ..  mw.dumpObject(expected) .. " <number>), got (" .. mw.dumpObject(actual) .. " <" .. type(actual) .. ">).")	
		end
		
	end
	
end

function p.tests.isfinite() 
	local expectedFalse = {
		math.huge, 
		-math.huge,
		0/0,
		1/0,
		"non number",
		false,
		nil,
		"2244",
	}
	for i,v in pairs(expectedFalse) do
		local s,r = pcall(p.isfinite, v)
		assert(s==true and r==false, 'got ' .. tostring(r) .. ', expected false when testing value ' .. tostring(v) .. ' ar index #' .. tostring(i))
	end
	local expectedTrue = {
		0,
		1,
		2,
		9*10^300,
		math.pi, 
		-0,
		-math.pi,
		2^0.2,
	}
	for i,v in pairs(expectedTrue) do
		local s,r = pcall(p.isfinite, v)
		assert(s==true and r==true, 'got ' .. tostring(r) .. ', expected true when testing value ' .. tostring(v) .. ' ar index #' .. tostring(i))
	end
end

function p.tests.trim() 
	local expectedToBeTrimmed = {
		[9] = string.char(9),
		[10] = string.char(10),
		[11] = string.char(11),
		[12] = string.char(12),
		[13] = string.char(13),
		[32] = string.char(32),
	}
	for i=0, 255, 1 do
		local a = string.char(i)
		local status, b = pcall(p.trim, a)
		if status == false or #a ~= #b then
			if expectedToBeTrimmed[i] == nil then
				error("Unexpected char (code=" .. i .. ") was trimmed out of the string!")
			end
		end
		local r = math.ceil(math.random()*10)
		local prefix = string.char(i):rep(r)
		local suffix = string.char(i):rep(r*2)
		local a = prefix .. 'my string' .. suffix
		local b = p.trim(a)
		if expectedToBeTrimmed[i] == nil and #a ~= #b then
			error("Trim trimmed too much from: " .. mw.dumpObject(a))
		elseif expectedToBeTrimmed[i] ~= nil then
			local err = ""
			local startsWith = b:sub(1,#prefix)
			local endsWith = b:sub(#b - #suffix + 1, #b)
			if startsWith == prefix then
				err = err .. "Did not trim the start of the string: " .. mw.dumpObject(startsWith) .. "; "
			end
			if endsWith == suffix then
				err = err .. "Did not trim the end of the string: " .. mw.dumpObject(endsWith) .. "; "
			end
			if #err > 0 then
				error("Failed to trim the string: " .. mw.dumpObject(a) .. "; Failure: " .. err)
			end
		end
	end
	
	local toBeTrimmed = ""
	for a, chr in pairs(expectedToBeTrimmed) do
		toBeTrimmed = toBeTrimmed .. chr .. toBeTrimmed .. chr
	end
	local testVal = ""
	local status, actual = pcall(p.trim, testVal)
	if actual ~= testVal then
		error("Failed to trim empty string")
	end
	testVal = "!"
	for i=0, 255, 1 do
		testVal = testVal .. string.char(i)
	end
	testVal = testVal .. "~"
	local status, actual = pcall(p.trim, testVal)
	if actual ~= testVal then
		error("Failed to trim string with nothing to trim!")
	end
	local status, actual = pcall(p.trim, testVal .. toBeTrimmed .. testVal)
	if actual ~= testVal .. toBeTrimmed .. testVal then
		error("Failed to trim suffix of string: " .. mw.dumpObject(actual))
	end
	local status, actual = pcall(p.trim, toBeTrimmed .. testVal)
	if actual ~= testVal then
		error("Failed to trim prefix of string: " .. mw.dumpObject(actual))
	end
	local status, actual = pcall(p.trim, testVal .. toBeTrimmed)
	if actual ~= testVal then
		error("Failed to trim suffix of string: " .. mw.dumpObject(actual))
	end
	local status, actual = pcall(p.trim, toBeTrimmed .. testVal .. toBeTrimmed .. testVal .. toBeTrimmed)
	if actual ~= testVal .. toBeTrimmed .. testVal then
		error("Failed to trim string: " .. mw.dumpObject(actual))
	end
end

function p.tests.startsWith() 
	local testVal = ""
	local toFind = ""
	local actual = p.startsWith(testVal, toFind)
	assert(actual == true, 'failed empty string starts with empty string')
	testVal = "asd"
	local actual = p.startsWith(testVal, toFind)
	assert(actual == true, 'failed value string starts with empty string')
	toFind = "s"
	local actual = p.startsWith(testVal, toFind)
	assert(actual == false, 'startsWith reported that ' .. mw.dumpObject(testVal) .. ' started with ' .. mw.dumpObject(toFind))
	toFind = "asd"
	local actual = p.startsWith(testVal, toFind)
	assert(actual == true, 'startsWith reported that ' .. mw.dumpObject(testVal) .. ' does not start with ' .. mw.dumpObject(toFind))
	toFind = "ASD"
	local actual = p.startsWith(testVal, toFind)
	assert(actual == false, 'startsWith reported that ' .. mw.dumpObject(testVal) .. ' started with ' .. mw.dumpObject(toFind))
	local actual = p.startsWith(testVal, toFind, true)
	assert(actual == true, 'startsWith reported that ' .. mw.dumpObject(testVal) .. ' does not start with ' .. mw.dumpObject(toFind) .. ' when we were supposed to ignore case!')
end

function p.tests.endsWith() 
	local testVal = ""
	local toFind = ""
	local actual = p.endsWith(testVal, toFind)
	assert(actual == true, 'failed empty string ends with empty string')
	testVal = "asd"
	local actual = p.endsWith(testVal, toFind)
	assert(actual == true, 'failed value string ends with empty string')
	toFind = "s"
	local actual = p.endsWith(testVal, toFind)
	assert(actual == false, 'endsWith reported that ' .. mw.dumpObject(testVal) .. ' ends with ' .. mw.dumpObject(toFind))
	toFind = "asd"
	local actual = p.endsWith(testVal, toFind)
	assert(actual == true, 'endsWith reported that ' .. mw.dumpObject(testVal) .. ' does not end with ' .. mw.dumpObject(toFind))
	toFind = "ASD"
	local actual = p.endsWith(testVal, toFind)
	assert(actual == false, 'endsWith reported that ' .. mw.dumpObject(testVal) .. ' ends with ' .. mw.dumpObject(toFind))
	local actual = p.endsWith(testVal, toFind, true)
	assert(actual == true, 'endsWith reported that ' .. mw.dumpObject(testVal) .. ' does not end with ' .. mw.dumpObject(toFind) .. ' when we were supposed to ignore case!')
end

function p.tests.ListJoin()
	local tbl = p.List.new()
	for i=1,5,1 do 
		p.List.pushright(tbl, i)
	end
	local actual = p.List.join(tbl, "_")
	local expected = "1_2_3_4_5"
	assert(actual == expected, "Actual " .. actual .. " did not match expected " .. expected)
end

function p.tests.updatingLogLevelIsReflectedInLoggingFunctions(preventMock)
	local doMock = not preventMock
	local currconf = p.debug.config
	-- mock function so we dont spam logs?
	local mockedLog = mw.logObject
	local mockedLogCalls = {}
	local mockedLogImplementation = function(msg, key) 
			local st =  p.debug.toStackTraceList(debug.traceback())
			table.insert(mockedLogCalls, {msg=msg, key=key, trace=st})
		end
	if doMock then
		mw.logObject = mockedLogImplementation
	end
	local errors = p.List.new()
	for configLevelEnum, configLevelValue in pairs(p.debug.LOG_LEVEL) do
		p.debug.updateLogConfigSetting('LEVEL', configLevelValue)
		for loggingFuncEnum, loggingFuncCorrespondingValue in pairs(p.debug.LOG_LEVEL) do
			if p.debug.config.LOG.LEVEL ~= configLevelEnum then
				p.List.pushright(errors, 'the config log level failed to persist!')
			end
			if p.debug[configLevelEnum:lower()] == nil then
				p.List.pushright(errors, 'The p.debug.' .. configLevelEnum:lower() .. ' logging function is missing! A')
			end
			if p.debug[loggingFuncEnum:lower()] == nil then
				p.List.pushright(errors, 'The p.debug.' .. loggingFuncEnum:lower() .. ' logging function is missing! B')
			end
			-- We print when the the given func's corresponding log level value is less than or equal to the current p.debug.config.LOG.LEVEL value
			-- if the corresponding log level value is greater than the current p.debug.config.LOG.LEVEL value then we do not print.
			local doWeExpectToLog = loggingFuncCorrespondingValue <= configLevelValue
			local testMessage = configLevelEnum .. ", " .. configLevelValue .. ", " .. loggingFuncEnum .. ", " .. loggingFuncCorrespondingValue .. ", " .. tostring(doWeExpectToLog)
			local status, didWeActuallyLog = pcall(p.debug[loggingFuncEnum:lower()], testMessage)
			if status == false then
				p.List.pushright(errors, "failed to execute " .. loggingFuncEnum:lower() .. " with : " .. testMessage)
			elseif didWeActuallyLog ~= doWeExpectToLog then
				p.List.pushright(errors, 'The following message was ' .. (didWeActuallyLog and '' or 'NOT ') .. ' logged when it was ' .. (doWeExpectToLog and '' or 'NOT ') .. 'supposed to be logged: ' .. testMessage)
			end
			if doWeExpectToLog == true and doMock then
				if mockedLogCalls[#mockedLogCalls]['msg'][4] ~= testMessage then
					p.List.pushright(errors, 'Last message to get logged: ' .. mw.dumpObject(mockedLogCalls[#mockedLogCalls].msg) .. ' does not match what we expected it to be ' .. mw.dumpObject(testMessage))
				end
			end
		end
	end
	p.debug.updateLogConfigSetting('LEVEL', p.debug.LOG_LEVEL.FATAL)
	-- We print when the the given func's corresponding log level value is less than or equal to the current p.debug.config.LOG.LEVEL value
	-- if the corresponding log level value is greater than the current p.debug.config.LOG.LEVEL value then we do not print.
	local doWeExpectToLog = p.debug.LOG_LEVEL.INFO <= p.debug.LOG_LEVEL.FATAL
	local testMessage = 'TEST MESSAGE'
	local status, didWeActuallyLog = pcall(p.debug.info, testMessage)
	if status == false then
		p.List.pushright(errors, "failed to execute fatal with : " .. testMessage)
	elseif didWeActuallyLog ~= doWeExpectToLog then
		p.List.pushright(errors, 'The following message was ' .. (didWeActuallyLog and '' or 'NOT ') .. ' logged when it was ' .. (doWeExpectToLog and '' or 'NOT ') .. 'supposed to be logged: ' .. testMessage)
	end
	-- restore mocks
	if doMock then
		mw.logObject = mockedLog
		-- test we restored properly
		mw.logObject('restored mock of mw.logObject')
		
	end
	if #errors > 0 then
		error(p.List.join(errors, "; "))
	end
end


return p


--[==[

tests:
tonumber()
trim()
startsWith()
endsWith()
ListJoin()
updatingLogLevelIsReflectedInLoggingFunctions(preventMock)
]==]