Comparison of two tables - Table Utility Feedback and/or improvement suggestions

Hello, I have made this table compare-or to assert weather two tables are identical or similar.
The issue is that this will be ran a lot and I know this will hinder performance.

Just to note, I have already attempted optimization and it dose work correctly.

Anyway, How can I further optimize the following code?

function CompareTables(tabA, tabB)
	local Same = true
	local Similar = true
	local keysA = {}
	local keysB = {}
	local function getResult()
		if Same then return "Equal" end
		if Similar then return "Similar" end
		return "Different"
	end
	for k, v in pairs(tabA) do
		keysA[k] = v
	end
	for k, v in pairs(tabB) do
		keysB[k] = v
	end
	for  k, v in pairs(keysA) do
		if keysB[k] ==  nil then
			Similar = false
			local result = getResult()
			return result
		elseif keysB[k] ~= v then
			Same = false
		end
	end
	for  k, v in pairs(keysB) do
		if keysA[k] ==  nil then
			Similar = false
			local result = getResult()
			return result
		elseif keysA[k] ~= v then
			Same = false
		end
	end
	local result = getResult()
	return result
end

The Above function has been replaced with a module
below that I have developed based on feedback

local httpService = game:GetService("HttpService")

local TableUtility = {}

TableUtility.ComparisonStates = {
	Equal = 0, --Same keys, same values
	Similar = 1, --same keys, different values
	Different = 2-- lacking a key

}

local function isType(value, ...)
	local args = table.pack(...)
	local valueType = type(value)
	if valueType == args[1] then return true end
	for k, v in pairs(args) do
		if valueType ~= v then return false end
	end
	return true
end

--This will not support mixxed tables

function TableUtility:ExtractKeys(table1)
	local keys = {}
	for k in pairs(table1) do
		table.insert(keys, k)
	end
	return keys
end

function TableUtility.TablesHaveSameKeys(table1, table2)
	local keys1 = TableUtility:ExtractKeys(table1)
	local keys2 = TableUtility:ExtractKeys(table2)
	return (httpService:JSONEncode(keys1) == httpService:JSONEncode(keys2))
end
	
function TableUtility.AreValuesEqual(value1, value2)
	if value1 == value2 then return true end
	
	local areValuesTables = (
		isType(value1, "table")
		and isType(value2, "table")
	)
	
	if not areValuesTables then return false end
	
	local areTablesEqual = TableUtility.DoTablesMatch(value1, value2)
	if areTablesEqual then 
		return true 
	else
		return false
	end
end

function TableUtility.DoTablesMatch(table1, table2)
	local baseEquivalence = (table1 == table2)
	if baseEquivalence then return true end
	for k, v in pairs(table1) do
		local matches = (
			TableUtility.AreValuesEqual(table2[k], v)
		)
		if not matches then
			print(matches)
			return false
		end
	end
	for k, v in pairs(table2) do
		local matches = (
			TableUtility.AreValuesEqual(table1[k], v)
		)
		if not matches then
			print(matches)
			return false
		end
	end
	return true
end


function TableUtility:CompareTables(table1, table2)
	local areTablesEqual = self.DoTablesMatch(table1, table2)
	if areTablesEqual then
		return self.ComparisonStates.Equal
	end
	local areSimilar = self.TablesHaveSameKeys(table1, table2)
	if areSimilar then
		return self.ComparisonStates.Similar
	else
		return self.ComparisonStates.Different
	end
end

function TableUtility:DoQuickTableComparison(table1, table2, comparisonState)
	if comparisonState == self.ComparisonStates.Equal then
		return (self.DoTablesMatch(table1, table2))
	elseif comparisonState == self.ComparisonStates.Similar then
		return (self.TablesHaveSameKeys(table1, table2))
	elseif comparisonState == self.ComparisonStates.Different then
		return (not self.TablesHaveSameKeys(table1, table2))
	end
end

return TableUtility
1 Like

You only need 2 loops e.g.

function compareTables(tabA, tabB)
    local Similar, Same = true, true
    local getResult = (function ()
        return Same and "Equal" or (Similar and "Similar" or "Different") 
    end)
	if tabA == tabB then
		return "Equal"
	end
	for i, v in next, tabA do
		if not tabB[i] then
			Similar, Same = false, false
			return getResult()
		elseif tabB[i] ~= v then
			Same = false
		end
	end
	for i, v in next, tabB do
		if not tabA[i] then
			Similar, Same = false, false
			return getResult()
		elseif tabA[i] ~= v then
			Same = false
		end
	end
	return getResult()
end

Although, could I ask your application for this? There may be a better way to do it.

1 Like

This is used for my slot system. I am currently making a rewarding system and I need a function for checking if a slot is similar to a item I’m trying to reward (This is so post to prevent a item from stacking onto a item with some boost attached to it unless those items are, in themselves, similar)

Could you give me an example arcitecture of your slot data? Surely you could just compare one or two of the properties e.g. check if the type of both slots are a sword, and whether they are a “fire” type → stack?

itemSlot = {ItemName = "Usless trash", SlotData = {Stk = 50, etc...}

I’m only comparing SlotData for similarity, of course I will take into account ItemName
I have to check each term of Slot Data as “What if I add an enchantment system or attach a certain boost to a item?” I don’t want that to be ridden

This is the newer version I made based on your example.

function CompareTables(tabA, tabB)
	local same = true
	local similar = true
	
	local function getResult()
		if same then return "Equal" end
		if similar then return "similar" end
		return "Different"
	end
	
	if tabA == tabB then return getResult() end
	
	for  k, v in pairs(tabA) do
		if tabB[k] ==  nil then
			similar = false
			same = false
			return getResult()
		elseif tabB[k] ~= v then
			same = false
		end
	end
	for  k, v in pairs(tabB) do
		if tabA[k] ==  nil then
			similar = false
			same = false
			return getResult()
		elseif tabA[k] ~= v then
			same = false
		end
	end
	return getResult()
end

So, are you trying to see if the two objects are literally the same, they have the exact same keys and values, or just the exact same keys?

I think that’s answered by reading the code.

“Similar” is the same keys but different values. Same or “Equal” is same keys and same values. No check is done if they’re actually pointing to the same memory.

Here is a quick solution to what you might be looking for.

local HttpService = game:GetService("HttpService");

local CompareUtil = {};

--- Returns the keys in the given value.
--- TODO: Refactor into a shared table utility.
--- @param {table|userdata} value
--- @returns {string[]}
local function getKeys(value)
	local keys = {};
	for key in pairs(value) do
		table.insert(keys, key);
	end
	return keys;
end

--- Returns if the two values have the same keys.
--- @param {table|userdata} valueOne
--- @param {table|userdata} valueTwo
--- @returns {boolean}
function CompareUtil.hasSameKeys(valueOne, valueTwo)
	local keysOne = getKeys(valueOne);
	local keysTwo = getKeys(valueTwo);
	return HttpService:JSONEncode(keysOne) == HttpService:JsonEncode(keysTwo);
end

--- Returns if the two values are the same in value.
--- @param {?} valueOne
--- @param {?} valueTwo
--- @returns {boolean}
function CompareUtil.isEqual(valueOne, valueTwo)
	if valueOne == valueTwo then
		return true;
	end
	
	local shouldCompareKeys = (
		typeof(valueOne) == typeof(valueTwo)
		and CompareUtil.isType(valueOne, "table", "userdata")
	);
	
	if shouldCompareKeys then
		return (
			CompareUtil.isMatch(valueOne, valueTwo)
			and CompareUtil.isMatch(valueTwo, valueOne)
		);
	end
	
	return false;
end

--- Returns `true` if `tableToMatch`'s shape is assignable to `template`'s shape.
--- @param {table|userdata} tableToMatch
--- @param {table|userdata} template
--- @returns {boolean}
function CompareUtil.isMatch(tableToMatch, template)
	for key, value in pairs(template) do
		local isEqual = (
			tableToMatch[key] == value
			or (
				typeof(tableToMatch[key]) == typeof(value)
				and CompareUtil.isEqual(tableToMatch[key], value)
			)
		)
		if not isEqual then
			return false;
		end
	end
	return true;
end

--- Returns `true` if `value`'s type is equal to any of the types supplied.
--- @param {?} value
--- @param {string[]} ...
--- @returns {boolean}
function CompareUtil.isType(value, ...)
	local valueType = type(value);
	for _, typeKind in ipairs({...}) do
		if valueType == typeKind then
			return true;
		end
	end
	return false;
end

return CompareUtil;

I highly urge you to not return comparisons in the form of string literals or to have a “god function” which does multiple things seeing that you’ll run into a lot of issues with modularity and unit testing. It becomes a maintaining nightmare very quickly. If you want to send a measure of similarity, delegate that functionality to an enum:

local ComparisonState = {
	Equal= 0,
	Similar = 1,
	Different = 2,
};

--- Returns the "comparison state" of the two values.
--- @param {?} valueOne
--- @param {?} valueTwo
--- @returns {ComparisonState}
local function getComparisonState(valueOne, valueTwo)
	if CompareUtil.isEqual(valueOne, valueTwo) then
		return ComparisonState.Equal;
	elseif (
		typeof(valueOne) == typeof(valueTwo)
		and CompareUtil.isType(valueOne, "table", "userdata")
		and CompareUtil.hasSameKeys(valueOne, valueTwo)
	) then
		return ComparisonState.Similar;
	else
		return ComparisonState.Different;
	end
end

Sorry if I have not yet replied, I am trying to compare tables to see if they are similar, same keys, different values; Equal, same keys and same values; or Different. This will be used for an item system that checks for similarities between a slot and a item being rewarded. This will allow for certain slots with enchantments attached to them to not be overwritten as it will return different if the rewarding item has either one more key or one less key.
I’m currently working on a module based off of yours

HTTP Service dose not support mixed tables,
therefore I will have to make a special “Table to string” Translator.
But that will be for another time…

Here is my new module

local httpService = game:GetService("HttpService")

local TableUtility = {}

TableUtility.ComparisonStates = {
	Equal = 0, --Same keys, same values
	Similar = 1, --same keys, different values
	Different = 2-- lacking a key

}

local function isType(value, ...)
	local args = table.pack(...)
	local valueType = type(value)
	if valueType == args[1] then return true end
	for k, v in pairs(args) do
		if valueType ~= v then return false end
	end
	return true
end

--This will not support mixxed tables

function TableUtility:ExtractKeys(table1)
	local keys = {}
	for k in pairs(table1) do
		table.insert(keys, k)
	end
	return keys
end

function TableUtility.TablesHaveSameKeys(table1, table2)
	local keys1 = TableUtility:ExtractKeys(table1)
	local keys2 = TableUtility:ExtractKeys(table2)
	return (httpService:JSONEncode(keys1) == httpService:JSONEncode(keys2))
end
	
function TableUtility.AreValuesEqual(value1, value2)
	if value1 == value2 then return true end
	
	local areValuesTables = (
		isType(value1, "table")
		and isType(value2, "table")
	)
	
	if not areValuesTables then return false end
	
	local areTablesEqual = TableUtility.DoTablesMatch(value1, value2)
	if areTablesEqual then 
		return true 
	else
		return false
	end
end

function TableUtility.DoTablesMatch(table1, table2)
	local baseEquivalence = (table1 == table2)
	if baseEquivalence then return true end
	for k, v in pairs(table1) do
		local matches = (
			TableUtility.AreValuesEqual(table2[k], v)
		)
		if not matches then
			print(matches)
			return false
		end
	end
	for k, v in pairs(table2) do
		local matches = (
			TableUtility.AreValuesEqual(table1[k], v)
		)
		if not matches then
			print(matches)
			return false
		end
	end
	return true
end


function TableUtility:CompareTables(table1, table2)
	local areTablesEqual = self.DoTablesMatch(table1, table2)
	if areTablesEqual then
		return self.ComparisonStates.Equal
	end
	local areSimilar = self.TablesHaveSameKeys(table1, table2)
	if areSimilar then
		return self.ComparisonStates.Similar
	else
		return self.ComparisonStates.Different
	end
end

function TableUtility:DoQuickTableComparison(table1, table2, comparisonState)
	if comparisonState == self.ComparisonStates.Equal then
		return (self.DoTablesMatch(table1, table2))
	elseif comparisonState == self.ComparisonStates.Similar then
		return (self.TablesHaveSameKeys(table1, table2))
	elseif comparisonState == self.ComparisonStates.Different then
		return (not self.TablesHaveSameKeys(table1, table2))
	end
end

return TableUtility
function Module:CompareTables(Table1, Table2)
	local Properties = {}
	for i,v in next, Table1 do
		Properties[i] = v
	end
	for i,v in next, Properties do
		if not Table2[i] or not Table2[i] == v then
			return false
		end
	end
	return Properties
end

I’ve made this what it does is gets all the properties from the 1st table, then compares the properties between 1st and 2nd table if not matching or not exisitng it returns false. Also I recommend not making nested functions (function within a function) u basically cache a whole new function and run it then it gets removed ( functions arent meant for that, but yeah there are some cases where it do be like dat;) )

Be warned that the Properties object is extraneous and this is only a shallow comparison; it also does not directly compare values properly due to the malformed binary operations. Furthermore, if Table2 has any keys not present in Table1, they are not evaluated, leading to cases that return true even if the two tables aren’t, well, the same in value.

However, TheEdgyDev, I do appreciate your method of comparison of how you check each table, I did make a intentional recursive function to make sure that tables within tables within tables were evaluated if necessary. I also made a DoQuickComparison for when only one comparison state is required; this allows for optimization in certain circumstances.

1 Like

That is true; I could add a function for the equivalence operator.