Table extra functions

Hello everyone! I am making a module where I add extra functions to many default libraries (like math, string, pairs, ipairs, etc…). The code you are going to see is some extra functions I belive could be useful for the table library, I am posting it here to see if there is anything I can optimize or any point I need to make more clear with comments. Thanks a bunch!

A comment before seeing the code is that there is a function called AddRemoveSelf and RemoveSelf, it is used by my module to allow people to call the function however they want, example: table.delete and table:delete

	Utilities:AddRemoveSelf(module);
	
--	Checks if a table is empty.
--	¿Why use this function?
--		Well, to check if an array is empty is easy, just do 0 == #array. This function, however, also checks
--		dictionaries.
	module.isempty = function(...)
		local arguments = module:RemoveSelf(...);
		local t1 = arguments[1];
		assert(type(t1) == "table", "Error at table(isempty). Expected positional argument 1 to be a table\nGot '" .. typeof(t1) .. "' instead.");

		return module.length(t1) == 0;
	end
	
--	Checks if a table is an array.
	module.isarray = function(...)
		local arguments = module:RemoveSelf(...);
		local t1 = arguments[1];
		assert(type(t1) == "table", "Error at table(isarray). Expected positional argument 1 to be a table\nGot '" .. typeof(t1) .. "' instead.");

		return module.length(t1) == #t1;
	end
	
--	Merges two dictionaries together.
	module.merge = function(...)
		local arguments = module:RemoveSelf(...);
		local t1 = arguments[1];
		assert(type(t1) == "table", "Error at table(merge). Expected positional argument 1 to be a table\nGot '" .. typeof(t1) .. "' instead.");
		
		table.remove(arguments, 1);
		
		for i, t in ipairs(arguments) do
			assert(type(t) == "table", "Error at table(merge). value at index '" .. tostring(i+1) .. "' is not a table\nExpected all values to be tables\nGot '" .. typeof(t) .. "' instead.");

			for k, v in pairs(t) do
				t1[k] = v;
			end
		end

		return t1;
	end
	
--	Merges two dictionaries or arrays together. This, however, makes all keys indexes.
	
--	Example:
--		{
--			1: "hello"
--			2: "nice"
--			3: "cool"
--		}
	
--		{
--			1: "a"
--			2: "b"
--			3: "c"
--		}
	
--	Result:
--		{
--			1: "hello"
--			2: "nice"
--			3: "cool"
--			4: "a"
--			5: "b"
--			6: "c"
--		}	
	
	module.imerge = function(...)
		local arguments = module:RemoveSelf(...);
		local t1 = arguments[1];
		assert(type(t1) == "table", "Error at table(imerge). Expected positional argument 1 to be a table\nGot '" .. typeof(t1) .. "' instead.");
		
		table.remove(arguments, 1);
		
		
		for i, t in ipairs(arguments) do
			assert(type(t) == "table", "Error at table(imerge). Expected all indexes of position argument 1 to be tables\nGot '" .. typeof(t) .. "' on index '" .. tostring(i) .. "' instead.");
			
			local f = pairs;
			if(module.isarray(t)) then f = ipairs end; --We need to use ipairs on an array to ensure the
--														indexes are organized
			
			for _, v in f(t) do
				table.insert(t1, v)
			end
		end

		return t1;
	end	
	
--	Replacement for table.remove, adds support to proxies.
	module.remove = function(...)
		local arguments = module:RemoveSelf(...);

		local t, index, skip = arguments[1], arguments[2], arguments[3];
		assert(type(t) == "table", "Error at table(remove). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead.");
		assert(type(index) == "number", "Error at table(remove). Expected positional argument 2 to be a number\nGot '" .. typeof(index) .. "' instead.");
		
		local metatable = getmetatable(t);
		if(not skip and metatable and type(metatable.controller) == "table" and type(metatable.controller.remove) == "function") then
			return metatable.controller.remove(t,index);
		else return table.remove(t, index) end;
	end
	
--	Replacement for table.insert, adds support to proxies.
	module.insert = function(...)
		local arguments = module:RemoveSelf(...);

		local t, index, value, skip = arguments[1], arguments[2], arguments[3], arguments[4];		
		assert(type(t) == "table", "Error at table(insert). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead.");
		assert(type(index) ~= "nil" or type(value) ~= "nil", "Error at table(insert). Expected positional argument 2\nGot '" .. typeof(index) .. "' instead.");		
		
--		Re-organizes the arguments if some of them are missing
		arguments[4] = nil;
		if(not index) then
			table.remove(arguments, 2);
		end
		
		if(type(value) == "nil") then
			value = index;
			index = nil;
		end
--
		
		local metatable = getmetatable(t);
		if(not skip and metatable and type(metatable.controller) == "table" and type(metatable.controller.insert) == "function") then
			return metatable.controller.insert(t,index, value);
		else return table.insert(table.unpack(arguments)) end;
	end
	
--	Removes all the duplicate values in a table, leaving only one of each
	module.unique = function(...)
		local arguments = module:RemoveSelf(...);
		local t1 = arguments[1];
		assert(type(t1) == "table", "Error at unique(collapse). Expected positional argument 1 to be a table\nGot '" .. typeof(t1) .. "' instead.");
		
		local clone = {};
		if(module.isarray(t1)) then
			local found = {};

			for _, v in ipairs(t1) do
				if(not table.find(clone, v)) then
					table.insert(clone, v);
				end
			end
		else
			local found = {};

			for k, v in pairs(t1) do
				if(not table.find(found, v)) then
					table.insert(found, v);
					clone[k] = v;
				end
			end
		end

		return clone;
	end
	
--	Gets the real length of a table. For example, trying to get the length of a dictionary will not return
--	it's real length and will instead return 0.
	module.length = function(...)
		local arguments = module:RemoveSelf(...);
		local t = arguments[1];
		assert(type(t) == "table", "Error at table(length). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead.");
		
		local realLength = 0;
		for k, v in pairs(t) do
			realLength = realLength + 1;
		end

		return realLength;
	end
	
--	Delete the first mention of the given value in an array.
	module.delete = function(...)
		local arguments = module:RemoveSelf(...);
		local t, v, swapRemove = arguments[1], arguments[2], arguments[3];
		assert(type(t) == "table", "Error at table(delete). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead.");
		assert(module.isarray(t), "Error at table(delete). Expected positional argument 1 to be an array.");
		
		local found = false;
		for k, v2 in ipairs(t) do
			if(v == v2) then
				if(swapRemove) then 
					module.swapremove(t,k);
				else module.remove(t, k)	end;
				found = true;
				break
			end
		end

		return found;
	end
	
--	Delete all the mentions of the given value in an array.
	module.deleteall = function(...)
		local arguments = module:RemoveSelf(...);
		local t, v, swapRemove = arguments[1], arguments[2], arguments[3];
		assert(type(t) == "table", "Error at table(deleteall). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead.");
		assert(module.isarray(t), "Error at table(delete). Expected positional argument 1 to be an array.");
		
		local found = 0;
		for k, v2 in ipairs(t) do
			if(v == v2) then
				if(swapRemove) then 
					module.swapremove(t,k);
				else module.remove(t, k)	end;
				found += 1;
			end
		end

		return found;
	end; module.deleteAll = module.deleteall;
	
--	Re-orders all the values in an array.
	module.shuffle = function(...)
		local arguments = module:RemoveSelf(...);
		local t = arguments[1];
		assert(type(t) == "table", "Error at table(shuffle). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead.");

		for i = #t, 2, -1 do
			local j = math.random(i)
			t[i], t[j] = t[j], t[i]
		end
		return t;
	end
	
--	Used by the internals, just an easy way I can add aliases to functions.
--	Examples:
--		MyFunctionName 
--		myFunctionName
--		myfunctionname	

	module.multiindex = function(...)
		local arguments =  module:RemoveSelf(...);

		local t, k, v = arguments[1], arguments[2], arguments[3];
		assert(type(t) == "table", "Error at table(multiindex). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead.");
		assert(type(k) == "string", "Error at table(multiindex). Expected positional argument 2 to be a string\nGot '" .. typeof(k) .. "' instead.");

		k = string.upper(string.sub(k,1,1)) .. string.sub(k,2);
		t[k] = v;
		t[string.lower(string.sub(k,1,1)) .. string.sub(k,2)] = v;
		t[string.lower(k)] = v;

		return t;
	end; module.multiIndex = module.multiindex;
	
--	Allows for a table to be filtered, where only the allowed values can remain.
	
--	NOTE: If you use filter on an array it will create gaps, don't worry though since that is why the
--	collapse function in this library exists	
	module.filter = function(...)
		local arguments = module:RemoveSelf(...);

		local t, c = arguments[1], arguments[2];
		assert(type(t) == "table", "Error at table(filter). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead");
		
		local results = {};
		local removed = {};
		for k,v in pairs(t) do
			if(c(t,k,v)) then
				table.insert(results, {k,v});
			else
				table.insert(removed, {k,v});
				t[k] = nil;
			end
		end

		return results, removed;
	end
	
--	Same as filter, this however will compare every single value with each other and then filter the
--	results.
	module.comparefilter = function(...)
		local arguments = module:RemoveSelf(...);

		local t, c = arguments[1], arguments[2];
		assert(type(t) == "table", "Error at table(comparefilter). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead");
		assert(type(c) == "function", "Error at table(comparefilter). Expected positional argument 2 to be a function\nGot '" .. typeof(t) .. "' instead");
		
		local clone = table.clone(t);
		
		local results = {};
		local removed = {};
		for k,v in pairs(clone) do
			for _,v2 in pairs(clone) do
				if(c(t,v,v2)) then
					table.insert(results, {k,v});
				else
					table.insert(removed, {k,v});
					t[k] = nil;
				end
			end
		end

		return results, removed;
	end; module.compareFilter = module.comparefilter;
	
--	Swapremove, also called as Fastremove is a method often used for large arrays. When you use
--	table.remove, you move all the keys down by one index, this is a problem for large arrays that do not
--	need to be organized since it takes unnecessary time.	To fix this Swapremove replaces the given index
--	with the last index.	
	module.swapremove = function(...)
		local arguments = module:RemoveSelf(module, ...);
		local t, i = arguments[1], arguments[2];
		assert(type(t) == "table", "Error at table(swapremove). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead.");
		
		local size = #t;
		if(size ~= i) then t[i] = t[size] end;
		t[size] = nil;

		return t;
	end; module.swapRemove = module.swapremove; module.fastremove = module.fastremove; module.fastRemove = module.fastremove;
	
--	Transforms every value (or key) in a table to an index, transforming it into an array.
	module.enumarate = function(...)
		local arguments = module:RemoveSelf(...);
		local t, keys = arguments[1], arguments[2];
		assert(type(t) == "table", "Error at table(enumarate). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead.");
		
		local clone = {};
		
		for i, v in ipairs(t) do
			if(not keys) then clone[i] = v;
			else clone[i] = i end;
		end

		for k, v in pairs(t) do
			if(not tonumber(k) or tonumber(k) > #t) then
				if(not keys) then table.insert(clone, v);
				else table.insert(clone, k) end;
			end
		end

		return clone;
	end
	
--	Often times when using functions like 'filter' or 'comparefilter' an array can become hollow (making
--	functions like 'ipairs' not work correctly), to fix this you can collapse an array which will join	
--	the empty indexes and remove the holes.
	module.collapse = function(...)
		local arguments = module:RemoveSelf(...);

		local t = arguments[1];
		assert(type(t) == "table", "Error at table(collapse). Expected positional argument 1 to be a table\nGot '" .. typeof(t) .. "' instead");

		local keys = module.enumarate(t, true);
		local numbers = {};
		for _, k in pairs(keys) do
			if(type(k) == "number") then table.insert(numbers, k) end;
		end;
		
		local currentSmallest;
		local returnValue = {};

		local redundancy;
		redundancy = function()
			for _, k in ipairs(numbers) do
				if(not currentSmallest or k < currentSmallest) then currentSmallest = k end;
			end

			table.insert(returnValue, t[currentSmallest]);
			module.delete(numbers, currentSmallest);
			currentSmallest = nil;
			
			if(not module.isempty(numbers)) then redundancy() end;
		end
		
		redundancy();

		return returnValue;
	end
	
--	Checks if two tables are equal.
--	¿Why use this function?
--		Well, when you do table1 == table2, you are actually checking the memory adress and not the	
--		contents of the table, this function checks the SHALLOW contents of the table	
	module.equals = function(...)
		local arguments = module:RemoveSelf(...);
		local t1 = arguments[1];
		assert(t1, "Error at table(equals). Expected positional argument 1 to be a table\nGot '" .. typeof(t1) .. "' instead.");
		
		table.remove(arguments, 1);
		for k,v in pairs(t1) do
			for _, t in pairs(arguments) do
				if(t[k] ~= v) then
					return false;
				end
			end
		end

		return true;
	end
	
--	Checks if two tables are equal.
--	¿Why use this function?
--		Well, when you do table1 == table2, you are actually checking the memory adress and not the	
--		contents of the table, this function checks the DEEP contents of the table	
	module.requals = 0;
	module.requals = function(...)
		local arguments = module:RemoveSelf(...);
		local t1 = arguments[1];
		assert(t1, "Error at table(equals). Expected positional argument 1 to be a table\nGot '" .. typeof(t1) .. "' instead.");
		
		table.remove(arguments, 1);
		for k,v in pairs(t1) do
			for _, t in pairs(arguments) do
				if(t[k] ~= v) then --Similar to what equals does
					return false;
				elseif(type(v) == "table" and type(t[k]) == "table") then
					if(not module.requals(v, t[k])) then
						return false;
					end
				end
			end
		end

		return true;
	end
2 Likes

It looks like all of your functions are defined using Dot, like module.myFunction, so having AddRemoveSelf and RemoveSelf Are pretty much useless because functions defined by dot dont get the self argument no matter what, its only applicable to something like module:MyFunction.

1 Like

Thank you for your input! Regarding the AddRemoveSelf function, when a Dot function is called with a Colon it adds an extra argument (and when a Colon function is called with a Dot it removes an argument). For this segment of the module it seems kind of useless, however, in other parts of the module there are functions like: EventEmitter.emit or EventEmitter.Emit or EventEmitter:emit or EventEmitter:Emit which I allow to be called both ways for the user’s customizability.

1 Like

The dot and the colon arent supposed to be for style when coding, dots are supposed to be used when your wanting to call a function inside a table without the need of using self, colons are supposed to be used when your wanting to call a function inside a table with the need of using self, plus it doesnt affect performance nor readability.

1 Like

Yes sir, that is precisely why I call :RemoveSelf with a colon since for that specific function self is needed. Regarding styling since this is supposed to be a module I want to allow people to call the functions however they want, and there is no limiter factor since the difference in time is almost negligible, there is not even a type check factor since the module itself is returned within a function (to allow for multiple calls) and is required within a function (Require) which makes the type checker not reach the module (This module has a set of libraries and the one I posted is one of them, so to access it you need to go through multiple function layers and there is no way the type checker could ever reach that far), so naming arguments would be useless since you couldn’t view it when calling the function. So, even though there is no reason to have that feature, there is also no reason to not have it, so I prefer to have it for customizability.

What you’re doing is not helpful, it’s a bad practice. Making the excuse to use the colon operator on functions because people should be able to call them how they want is kinda dumb, especially since you have 4 functions which achieve the same thing.

2 Likes

I guess I’ll remove it, I’m sorry for being dumb. I just thought people would not care since I really didn’t find a negative connotation to it, but if you believe it is not useful then I’ll remove it right away. Thanks!

Anything else that you believe needs changing? And once again, I’m sorry I didn’t realize it sooner.

This method checks if ‘something’ is a table, however it looks like something completely different because it checks the length of the table, arent you supposed to just return typeof(t1) == 'table'?

The length of an array is given when doing #array, it results in 0 when a table is a dictionary. Instead of looping through the entire table and checking if every value is a number I can just check the length of the dict and compare it to the length of the array. If they are the same it’s an array, if not its a dict

Oh i see my bad, i thought it generally checks if something is a table

1 Like

It does not check for a table, instead it checks if a table is an array. And that type/typeof is for debugging.

Yea other than the removeself thingy, i think your code is ready to go, however rename the arguments in the functions to increase readability but thats just optional

Can you once again explain why RemoveSelf is bad practice? Since I can not find any reason of why it’s a bad idea to include it (no performance or type-checking). And once again, thank you for helping improve my code!

It does affect performance-wise by a bit, since you’ll create a table that contains the arguments, and call table.remove(), all of these will take a bit, plus its just really unnecessary, now you can do a test to test both and see the difference:

local extendedTable = require(game.ReplicatedStorage.ExtendedTable)

local withSelfClock = os.clock()

extendedTable.isarrayWithSelf()

local selfDelta = os.clock() - withSelfClock
local clock = os.clock()

extendedTable.isarray()

local delta = os.clock() - clock
print(selfDelta - delta) -- difference in seconds

Also i cant help but notice that your function names are not using camelCase, please use it for better readabillity.

1 Like

It is, you can do module.deleteall and module.deleteAll. (I also have it in isArray and isEmpty, I forgot to include it on the original post).

Also I completely forgot to explain another reason and that is proxies, RemoveSelf is really useful for proxy functions since when you call it with colon you actually get a defective self since it isn’t the real table.

I also tested it myself with this result 5.00003807246685e-07 which is minimal, it’d take 2 million iterations to be one second, you are more likely to get a bigger difference because of noise. In fact, I did some testing:

I looped 10000 times with a function that does not include remove self, comparing it to itself, and the noise difference was bigger than the difference of a function with remove self: 0.000014299992471933365

(I also conducted multiple other tests to ensure that resources overload wasn’t the reason for the delay and in all the answers the noise was bigger).

If the noise difference is bigger than the amount of time it takes to run a function then I do not believe it is worth it. (Running os.clock and comparing it already takes 1/5th of what the difference would be)

There is also no memory issue, I mean the key is aiming at the same place, if you are concerned about memory for a key then at that point you might as well reduce empty lines to save some bytes on the script.

1 Like

Im not quite sure what do you mean by “noise” can you please elaborate? Thanks

1 Like

Sure! There are many definitions of noise, and the definition I am using it for could be considered a bit out of place. In data communication there is a definition which is noise and it is the loss of information when transmitting data, for my case, I am using noise with the meaning ‘interference’. Many things can be noise, the CPU temperature, another process in the computer, the Lua garbage collector… essentially, when you do 1+1 you should always expect the same loading times, but you don’t, and the reason you don’t is because of exterior interference, or the so-called “noise”.

1 Like