Module:Shared: Difference between revisions

From Melvor Idle
(Fixed specific edge case of rounding sometimes leading to too few digits being shown.)
No edit summary
 
(48 intermediate revisions by 4 users not shown)
Line 1: Line 1:
local Num = require('Module:Number')
--So there are a handful of functions that I'm using in a lot of places
--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
--So rather than continue to copy across the same handful of functions to every single new module
Line 13: Line 14:


local p = {}
local p = {}
 
-- iterator sorted by keys
-- iterator sorted by keys
-- For example, if you had a table that looked something like
-- For example, if you had a table that looked something like
Line 25: Line 26:
--Originally snagged this from Module:VoidByReward written by User:NoBrainz
--Originally snagged this from Module:VoidByReward written by User:NoBrainz
function p.skpairs(t, revert)
function p.skpairs(t, revert)
    local keys = {}
local keys = {}
    for k in pairs(t) do keys[#keys + 1] = k end
for k in pairs(t) do keys[#keys + 1] = k end
    if revert ~= nil then
if revert ~= nil then
        table.sort(keys, function(a, b) return a > b end)
table.sort(keys, function(a, b) return a > b end)
    else
else
        table.sort(keys)
table.sort(keys)
    end
end
 
    local i = 0
local i = 0
    local iterator = function()
local iterator = function()
        i = i + 1
i = i + 1
        local key = keys[i]
local key = keys[i]
        if key then
if key then
            return key, t[key]
return key, t[key]
        else
else
            return nil
return nil
         end
end
end
return iterator
end
 
-- Function to sort a dictionary-like structure where items are added like tbl['key'] = value
-- We need to turn this structure into a table first, in order to sort it.
function p.sortDictionary(dict, comparer, factory)
local sortedTable = {}
    for k, v in pairs(dict) do
    local newValue = nil
    if factory then
    newValue = factory(k, v)
    end
    newValue = newValue or {key = k, value = v}
         table.insert(sortedTable, newValue)
     end
     end
     return iterator
   
    table.sort(sortedTable, comparer)
   
     return sortedTable
end
end


--General purpose function for going through a table after sorting based on a custom sort order
--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
--Taken from https://stackoverflow.com/questions/15706270/sort-a-table-in-lua
function p.spairs(t, order)
function p.spairs(t, order)
    -- collect the keys
-- collect the keys
    local keys = {}
local keys = {}
    for k in pairs(t) do keys[#keys+1] = k end
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,
-- if order function given, sort by it by passing the table and keys a, b,
    -- otherwise just sort the keys  
-- otherwise just sort the keys  
    if order then
if order then
        table.sort(keys, function(a,b) return order(t, a, b) end)
table.sort(keys, function(a,b) return order(t, a, b) end)
    else
else
        table.sort(keys)
table.sort(keys)
    end
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


    -- return the iterator function
-- Makes a deep copy of a table or otherwise object.
     local i = 0
-- Yoinked from http://lua-users.org/wiki/CopyTable
     return function()
function p.deepcopy(orig)
         i = i + 1
     local copy
         if keys[i] then
     if type(orig) == 'table' then
             return keys[i], t[keys[i]]
         copy = {}
         for orig_key, orig_value in next, orig, nil do
             copy[deepcopy(orig_key)] = deepcopy(orig_value)
         end
         end
        setmetatable(copy, deepcopy(getmetatable(orig)))
    else -- number, string, boolean, etc
        copy = orig
     end
     end
    return copy
end
end
 
-- conveniently shifts BLAH to Blah
-- Takes an input string and returns the same string with title case-like
-- Handy when formatting data in ALL CAPS or all lower case
-- formatting (that is, the first letter of every word becomes uppercase,
-- while the remainder becomes lowercase)
-- Examples:
-- titleCase('ALL CAPS') = 'All Caps'
-- titleCase('all lowercase') = 'All Lowercase'
-- titleCase('A MiXTUre') = 'A Mixture'
-- Note that non-alphanumeric characters are treated as a word boundary, so:
-- titleCase('a!b(c)d') = 'A!B(C)D' (not 'A!b(c)d')
--Originally snagged this from Module:VoidByReward written by User:NoBrainz
--Originally snagged this from Module:VoidByReward written by User:NoBrainz
function p.titleCase(head, tail)
function p.titleCase(head, tail)
    if tail == nil then
if tail == nil then
        --Split into two lines because don't want the other return from gsub
--Split into two lines because don't want the other return from gsub
        local result = string.gsub(head, "(%a)([%w_']*)", p.titleCase)
local result = string.gsub(head, "(%a)([%w_']*)", p.titleCase)
        return result
return result
    else
else
        return string.upper(head) .. string.lower(tail)
return string.upper(head) .. string.lower(tail)
end
end
 
-- Converts an input string into TitleCase.
-- Every first letter of every word becomes uppercase
-- With the exception of 'of', 'the', 'and', but only if these
-- appear anywhere in the sentence besides the first occurence.
-- Examples:
-- specialTitleCase('ALL CAPS') = 'All Caps'
-- specialTitleCase('all lowercase') = 'All Lowercase'
-- specialTitleCase('A MiXTUre') = 'A Mixture'
-- specialTitleCase('the bones') = 'The Bones
-- specialTitleCase('of the world') = 'Of the World'
-- specialTitleCase('amulet Of Fishing') = 'Amulet of Fishing'
function p.specialTitleCase(sentence)
if sentence == nil or sentence:match("^%s*$") ~= nil then return nil end
-- List of words that are excluded from TitleCase
    local excludedWords = {
        ["of"] = true,
        ["and"] = true,
        ["the"] = true
    }
 
-- Split all words and add them as lower case to the table.
    local words = {}
    for word in sentence:gmatch("%S+") do
        table.insert(words, word:lower())
    end
 
    -- Capitalize the first word
    words[1] = words[1]:gsub("^%l", string.upper)
 
    -- Title-case the remaining words, excluding certain words based on position
    for i = 2, #words do
    local curWord = words[i]
        if excludedWords[curWord] == true then
        else
            words[i] = curWord:gsub("^%l", string.upper)
        end
     end
     end
    return table.concat(words, " ")
end
end
 
-- Returns the number of rows in a table
-- Returns the number of rows in a table
-- Originally snagged this from Module:VoidByReward written by User:NoBrainz
-- Originally snagged this from Module:VoidByReward written by User:NoBrainz
Line 97: Line 181:
--      if table is not of type 'table' then return nil
--      if table is not of type 'table' then return nil
function p.tableCount(table)
function p.tableCount(table)
    if (type(table) == 'table') then
if (type(table) == 'table') then
        local count = 0
local count = 0
        for _ in pairs(table) do count = count + 1 end
for _ in pairs(table) do count = count + 1 end
        return count
return count
    else
else
        return nil
return nil
    end
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
end
 
-- Returns the number of indexed elements in a table
-- Returns the number of indexed elements in a table
-- pre : table is a table with no explicit nil values
-- pre : table is a table with no explicit nil values
Line 111: Line 207:
--      if table is not of type 'table' then return nil
--      if table is not of type 'table' then return nil
function p.indexCount(table)
function p.indexCount(table)
    if (type(table) == 'table') then
if (type(table) == 'table') then
        local count = 0
local count = 0
        for _ in ipairs(table) do count = count + 1 end
for _ in ipairs(table) do count = count + 1 end
        return count
return count
    else
else
        return nil
return nil
    end
end
end
end
 
--Sorts theTable based on the listed column
--Sorts theTable based on the listed column
function p.tableSort(theTable, sortCol, ascend)
function p.tableSort(theTable, sortCol, ascend)
    local new  function sorter(r1, r2)
local sorter = function(r1, r2)
                    if(ascend) then
if ascend then
                        return r1[sortCol] < r2[sortCol]
return r1[sortCol] < r2[sortCol]
                    else
else
                        return r1[sortCol] > r2[sortCol]
return r1[sortCol] > r2[sortCol]
                    end
end
                end
end
    table.sort(theTable, sorter)
table.sort(theTable, sorter)
end
end
 
--Splits a string based on a sent in separating character
--- Splits a string based on a sent in separating character
--For example calling splitString ("Lith V1 Relic", " ") would return {"Lith", "V1", "Relic"}
--- For example calling splitString ("Lith V1 Relic", " ") would return {"Lith", "V1", "Relic"}
function p.splitString(inputstr, sep)
-- @param inputstr (string) The input to separate.
        if sep == nil then
-- @param sep (string/char) The separation character.
                sep = "%s"
-- @param trim (boolean) TRUE to trim the leading/trailing whitespaces
        end
function p.splitString(inputstr, sep, trim)
        local t={}
if sep == nil then
        for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
sep = "%s"
                table.insert(t, str)
end
        end
local t = {}
        return t
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
if trim == true then
str = str:gsub("^%s*(.-)%s*$", "%1")
end
table.insert(t, str)
end
return t
end
end
 
--Returns 'true' if a string starts with something
--Returns 'true' if a string starts with something
--For example calling startsWith ("Lith V1 Relic", "Lith") would return true
--For example calling startsWith ("Lith V1 Relic", "Lith") would return true
function p.startsWith(string1, start)
function p.startsWith(string1, start)
    return string.sub(string1, 1, string.len(start)) == start
return string.sub(string1, 1, string.len(start)) == start
end
end
 
--Stolen from Stack Overflow
--Adds commas
function p.formatnum(number)
  local i, j, minus, int, fraction = tostring(number):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
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("%."..minDigits.."f", tonumber(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
-- pre : List is a table or a string
--      Item is the element that is being searched
--      Item is the element that is being searched
Line 198: Line 258:
-- post: returns a boolean; true if element exists in List, false otherwise
-- post: returns a boolean; true if element exists in List, false otherwise
function p.contains(List, Item, IgnoreCase)
function p.contains(List, Item, IgnoreCase)
    if (List == nil or Item == nil) then  
if List == nil or Item == nil then  
        return false  
return false  
    end
end
    if(IgnoreCase == nil) then  
if IgnoreCase == nil then  
        IgnoreCase = false  
IgnoreCase = false  
    end
end
 
    if(type(List) == "table") then
if type(List) == "table" then
        for key, value in pairs(List) do
for key, value in pairs(List) do
            if (value == Item) then
if value == Item then
                return true, key
return true, key
            elseif (IgnoreCase and string.upper(value) == string.upper(Item)) then
elseif IgnoreCase and string.upper(value) == string.upper(Item) then
                return true, key
return true, key
            end
end
        end
end
    else
else
        local start = string.find(List, Item)
local start = string.find(List, Item)
        return start ~= nil
return start ~= nil
    end
end
    return false
return false
end
end
   
   
Line 234: Line 294:
--User:Giga Martin
--User:Giga Martin
function p.trim(str)
function p.trim(str)
  return (str:gsub("^%s*(.-)%s*$", "%1"))
return (str:gsub("^%s*(.-)%s*$", "%1"))
end
end
   
   
Line 246: Line 306:
--      if key contains a nil value
--      if key contains a nil value
function p.hasKey(table, key, length)
function p.hasKey(table, key, length)
    if (length == nil) then
if (length == nil) then
        length = p.tableCount(table)
length = p.tableCount(table)
    end
end
 
    -- iterating through outer table
-- iterating through outer table
    for i = 1, length, 1 do
for i = 1, length, 1 do
        local elem = table[i]  -- storing one of inner tables into a variable
local elem = table[i]  -- storing one of inner tables into a variable
        if (elem[key] ~= nil) then
if (elem[key] ~= nil) then
            return true
return true
        end
end
    end
end
    return false
return false
end
end
 
-- copies the contents of a variable; handy for when you might want to modify an object taken from a data file
-- 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
-- or any other read-only variable
-- Stolen from https://gist.github.com/tylerneylon/81333721109155b2d244
-- Stolen from https://gist.github.com/tylerneylon/81333721109155b2d244
function p.clone(obj)
function p.clone(obj)
    if type(obj) ~= 'table' then return obj end
if type(obj) ~= 'table' then return obj end
    local res = {}
local res = {}
    for k, v in pairs(obj) do res[p.clone(k)] = p.clone(v) end
for k, v in pairs(obj) do res[p.clone(k)] = p.clone(v) end
    return res
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
 
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
 
-- 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
end


-- Euclid's Greatest Common Divisor algorithm
-- Takes a description template & template data, returning a description with variables populated
function p.gcd(a, b)
function p.applyTemplateData(descTemplate, templateData)
  if(a == b) then
    local resultDesc = descTemplate
     return a
     for k, v in pairs(templateData) do
  else
        local val = v
    if(a > b) then
        if type(v) == 'number' then
      if b == 0 then
            val = Num.formatnum(val)
        return a
        end
      else
        resultDesc = string.gsub(resultDesc, '${' .. k .. '}', val)
        return p.gcd(a - b, b)
      end
    else
      if a == 0 then
        return b
      else
        return p.gcd(a, b - a)
      end
     end
     end
  end
    return resultDesc
end
end


--Formats a pair of numbers as a reduced fraction
-- Given a namespace & local ID, returns a namespaced ID
function p.fraction(n, d)
function p.getNamespacedID(namespace, localID)
  local gcd = p.gcd(n, d)
if string.find(localID, ':') == nil then
  return p.formatnum(n/gcd)..'/'..p.formatnum(d/gcd)
return namespace .. ':' .. localID
else
-- ID already appears to be namespaced
return localID
end
end
end


function p.timeString(timeInSeconds, shorten)
-- Given a namespaced ID, returns both the namespace & local ID
  local remain = timeInSeconds
function p.getLocalID(ID)
  local days, hours, minutes = 0, 0, 0
local namespace, localID = nil, nil
  local isShort = shorten
local sepIdx = string.find(ID, ':')
 
if sepIdx == nil then
  local pieces = {}
-- 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
 
-- Compares two strings, optionally ignoring case
function p.compareString(left, right, ignoreCase)
-- Both are nil (equal)
if left == nil and right == nil then return true end
-- Only one is nil (not equal)
if left == nil or right == nil then return false end
-- Convert inputs to strings, just in case
left =  tostring(left)
right = tostring(right)
if ignoreCase == true then
return left:upper() == right:upper()
else
return left == right
end
end
 
function p._replace(str, searchTerm, replacementTerm)
if str == nil then
return str
end
 
    local escapedSearch = searchTerm:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%1")
    local result = str:gsub(escapedSearch, replacementTerm)
   
    return result
end


  if remain >= 86400 then
function p.addOrUpdate(tbl, key, func)
    days = math.floor(remain / 86400)
local val = tbl[key]
    remain = remain - days * 86400
     if val ~= nil then
     if isShort then
        tbl[key] = func(val)
      table.insert(pieces, days..'d')
    elseif days > 1 then
      table.insert(pieces, days..' days')
     else
     else
      table.insert(pieces, days..' day')
        tbl[key] = func(nil)
     end
     end
  end
      
  if remain >= 3600 then
     return tbl[key]
    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
end


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

Latest revision as of 22:53, 29 June 2024

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

local Num = require('Module:Number')
--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

-- Function to sort a dictionary-like structure where items are added like tbl['key'] = value
-- We need to turn this structure into a table first, in order to sort it.
function p.sortDictionary(dict, comparer, factory)
	local sortedTable = {}
	
    for k, v in pairs(dict) do
    	local newValue = nil
    	if factory then 
    		newValue = factory(k, v)
    	end
    	newValue = newValue or {key = k, value = v}
        table.insert(sortedTable, newValue)
    end
    
    table.sort(sortedTable, comparer)
    
    return sortedTable
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

-- Makes a deep copy of a table or otherwise object.
-- Yoinked from http://lua-users.org/wiki/CopyTable
function p.deepcopy(orig)
    local copy
    if type(orig) == 'table' then
        copy = {}
        for orig_key, orig_value in next, orig, nil do
            copy[deepcopy(orig_key)] = deepcopy(orig_value)
        end
        setmetatable(copy, deepcopy(getmetatable(orig)))
    else -- number, string, boolean, etc
        copy = orig
    end
    return copy
end

-- Takes an input string and returns the same string with title case-like
-- formatting (that is, the first letter of every word becomes uppercase,
-- while the remainder becomes lowercase)
-- Examples:
-- titleCase('ALL CAPS') = 'All Caps'
-- titleCase('all lowercase') = 'All Lowercase'
-- titleCase('A MiXTUre') = 'A Mixture'
-- Note that non-alphanumeric characters are treated as a word boundary, so:
-- titleCase('a!b(c)d') = 'A!B(C)D' (not 'A!b(c)d')
--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

-- Converts an input string into TitleCase.
-- Every first letter of every word becomes uppercase
-- With the exception of 'of', 'the', 'and', but only if these
-- appear anywhere in the sentence besides the first occurence.
-- Examples:
-- specialTitleCase('ALL CAPS') = 'All Caps'
-- specialTitleCase('all lowercase') = 'All Lowercase'
-- specialTitleCase('A MiXTUre') = 'A Mixture'
-- specialTitleCase('the bones') = 'The Bones
-- specialTitleCase('of the world') = 'Of the World'
-- specialTitleCase('amulet Of Fishing') = 'Amulet of Fishing'
function p.specialTitleCase(sentence)
	if sentence == nil or sentence:match("^%s*$") ~= nil then return nil end
	
	-- List of words that are excluded from TitleCase
    local excludedWords = {
        ["of"] = true,
        ["and"] = true,
        ["the"] = true
    }

	-- Split all words and add them as lower case to the table.
    local words = {}
    for word in sentence:gmatch("%S+") do
        table.insert(words, word:lower())
    end

    -- Capitalize the first word
    words[1] = words[1]:gsub("^%l", string.upper)

    -- Title-case the remaining words, excluding certain words based on position
    for i = 2, #words do
    	local curWord = words[i]
        if excludedWords[curWord] == true then
        else
            words[i] = curWord:gsub("^%l", string.upper)
        end
    end

    return table.concat(words, " ")
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"}
-- @param inputstr (string) The input to separate.
-- @param sep (string/char) The separation character.
-- @param trim (boolean) TRUE to trim the leading/trailing whitespaces
function p.splitString(inputstr, sep, trim)
	if sep == nil then
		sep = "%s"
	end
	local t = {}
	for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
		if trim == true then
			str = str:gsub("^%s*(.-)%s*$", "%1")
		end
		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

-- 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

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

-- 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 = Num.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

-- Compares two strings, optionally ignoring case
function p.compareString(left, right, ignoreCase)
	-- Both are nil (equal)
	if left == nil and right == nil then return true end
	
	-- Only one is nil (not equal)
	if left == nil or right == nil then return false end
	
	-- Convert inputs to strings, just in case
	left =  tostring(left)
	right = tostring(right)
	if ignoreCase == true then
		return left:upper() == right:upper()
	else
		return left == right	
	end
end

function p._replace(str, searchTerm, replacementTerm)
	if str == nil then 
		return str
	end

    local escapedSearch = searchTerm:gsub("[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%1")
    local result = str:gsub(escapedSearch, replacementTerm)
    
    return result
end

function p.addOrUpdate(tbl, key, func)
	local val = tbl[key]
    if val ~= nil then
        tbl[key] = func(val)
    else
        tbl[key] = func(nil)
    end
    
    return tbl[key]
end

return p