BetterTables - Adding on to Lua table functionality

Lua lacks some very useful built in functionality to tables that plenty of other languages are able to take advantage of. The table… table that provides additional functionality to tables is quite limited and really only applies to array-like tables. Also this table is readonly, which means I had to make a whole class for this. Fortunately due to metatables, tables are able to be indexed as normal.

The asset can be found here.
The github can be found here. The github is the same as the asset, but it also has the test file, which I used Nexus unit testing to test the functionality to ensure it works. If you PR something, I advise you to add a unit test to it.

Documentation

Index

Index

Creating a new table

Because the table table is readonly, functionality was extended by creating a new class. You can create a BetterTable in a few ways:

Creating an empty table

If you wish to create an empty table, just call the new function. This will be an empty table, but with the BetterTable functionality on it.

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()

Creating a BetterTable from an existing table

You can convert existing tables (and even existing BetterTables) into BetterTables by passing the table into the new function.

  • NOTE: All tables are deep cloned, so it isn’t recommended to do this if you have a reference to something inside the table from outside the table, they won’t retain the same reference.
local BetterTables = require(path.to.BetterTables)
local tbl = {1,2,3,4,5}
local t = BetterTables.new(tbl)

Creating an array table with a specific size initializer

You can create a table with a size initialization as well. This is useful because resizing tables dynamically can be expensive. Additionally, you can pass an initial value which will be filled into all of the newly created slots

local BetterTables = require(path.to.BetterTables)
local size = 10
local value = "Hello!"
local t = BetterTables.new(size, value)

Indexing the table

BetterTables can be indexed identically to regular tables using [] or ..

local BetterTables = require(path.to.BetterTables)
local size = 10
local value = "Hello!"
local t = BetterTables.new(size, value)
print(t[1]) -- "Hello!"

– OR –

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new({["Hello"]="Goodbye!"})
print(t.Hello) -- "Goodbye!"
print(t["Hello"]) -- "Goodbye!"

Getting the length of the table

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new(10, "a")
print(t:GetLength()) -- 10
  • NOTE: GetLength() works with all types of tables.

Inserting values into a table

Unfortunately because table is readonly, all of the table methods are implemented as functions on the table itself.

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
t:Insert("Hello!")
print(t[1]) -- "Hello!"
  • NOTE: Insert uses table.insert, so the underlying functionality is identical. It supports inserting at specific indices using Insert(pos, value)

Removing values from a table

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
t:Insert("Hello!")
print(t[1]) -- "Hello!"
t:Remove(1)
print(t[1]) -- nil
  • NOTE: Remove uses table.remove so its underlying functionality is identical.

Iterating a table

You can iterate over any type of table, however the type of iteration is different depending on the type. For array tables, ipairs iteration is returned, for mixed and dictionary tables pairs is returned. ipairs essentially maintains the order of iteration so that index 1 is the first value and index #table is the last value. ipairs and pairs are not metamethods that Luau has, unfortunately, so this is implemented as a method.

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
t:Insert("Hello!")
t:Insert("Goodbye!")
for i, v in t:GetIterator() do
    print(i, v) -- "1 Hello!" then  "2 Goodbye!"
end

Sorting array tables

You can sort array tables because the order in which they are retrieved when iterating remains the same, which is unlike dictionaries which can return in any order.

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
t:Insert(2)
t:Insert(1)
t:Sort() -- default sort is ascending
print(t[1]) -- 1
print(t[2]) -- 2
  • NOTE: Attempting to sort a non-array table will result in an error

Finding an element in a table

table.find works a little differently than find functions in other languages. I’ve implemented it to be like those other languages which allow you to pass a predicate function to test values against. This allows for much more control over the result.

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
local v = 7
t:Insert(v)
for i = 1, 100 do
	t:Insert(math.random(0,5))
end
local val = t:Find(function(value, index, tbl)
	if value > 5 then
		return true
	end
	return false
end)
print(val) -- 7

The function you pass must return true or false. The arguments it can accept are value, which is the current iteration’s value, key which is the current iteration’s key (or index), and table which is the BetterTable itself. You don’t need to accept all of these arguments, but they are always passed in the order value, key, table.

  • NOTE: Find works on all types of tables
  • NOTE: If no value passes the function nil is returned

Finding an index/key of a value in a table

Like table.find, you can also find a specific index of a value in a table. FindKey and FindIndex are identical, they only have a name change to respect the fact that array tables’ keys are indices. FindKey (and FindIndex) may be passed a single value, in which case a simple equivalency check is made, or a function that is identical to the one in Find

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
t:Insert(3)
t:Insert(6)
t:Insert(8)
t:Insert(9)
t:Insert(4)
t:Insert(7)
local index = t:FindIndex(8)
print(index) -- 3

– OR –

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
t:Insert(3)
t:Insert(6)
t:Insert(8)
t:Insert(9)
t:Insert(4)
t:Insert(7)
local index = t:FindIndex(function(value, key, tbl)
	if value > 8 then
		return true
	end
	return false
end)
print(index) -- 4
  • NOTE: FindKey and FindIndex are identical and have the exact same functionality and can be used on any type of table.

Creating a copy

  • NOTE: Both copy methods return regular tables and not BetterTables.

Creating a deep copy

Deep copies of tables allow you to get a brand new table with nothing being equivalent to anything in the table it was cloned from. This means tables within tables will not hold the same reference in memory.

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
local x = {5}
t:Insert(x)
local newTable = t:DeepCopy()
print(x == newTable[1]) -- false

Creating a shallow copy

Unlike a deep copy, a shallow copy retains all references in which means tables in tables remain the same in memory.

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
local x = {5}
t:Insert(x)
local newTable = t:ShallowCopy()
print(x == newTable[1]) -- true

Filling a table

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
for i = 1, 5 do
	t:Insert("Yay!")
end
print(t[5]) -- "Yay!"
t:Fill("F", 5, 10)
for i = 5, 10 do
	print(t[i]) -- "F" (x6)
end
  • NOTE: Fill takes 3 arguments: value, fromIndex, and toIndex. Both indices are inclusive. fromIndex is optional, and is defaulted at 0

Testing values in the table

Other languages provide ways to test values in a table and return true or false. Luau has table.foreach, however it is a void function. BetterTables allows you to pass a function to test against values in the table. The arguments it can accept are value, which is the current iteration’s value, key which is the current iteration’s key (or index), and table which is the BetterTable itself. You don’t need to accept all of these arguments, but they are always passed in the order value, key, table

  • NOTE: This works on any type of table

Testing every value

Every tests whether every value in the table passes the test function passed.

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
for i = 1, 100 do
	t:Insert(math.random(6,100))
end
local everyValueGreaterThan5 = t:Every(function(value)
	if value > 5 then return true end
	return false
end)
print(everyValueGreaterThan5) -- true

Testing any value

Some tests whether any value passes the test function passed.

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
for i = 1, 100 do
	t:Insert(math.random(0,5))
end
t:Insert(6)
local someValueGreaterThan5 = t:Some(function(value)
	if value > 5 then return true end
	return false
end)
print(someValueGreaterThan5) -- true

Removing all values that don’t pass a predicate function

  • NOTE: This change happens in place meaning it changes the contents of the table and does not return a new one.
  • NOTE: The tester function’s arguments it can accept are value, which is the current iteration’s value, key which is the current iteration’s key (or index), and table which is the BetterTable itself. You don’t need to accept all of these arguments, but they are always passed in the order value, key, table.
  • NOTE: The tester function should return true if the value should be kept in the table.
local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
for i = 1, 10 do
	t:Insert(5)
end

print(t[1]) -- 5

t:Insert(3)

t:Filter(function(value)
	return value ~= 5
end)

print(t[1]) -- 3

Concatenating two tables together

You can combine two tables using Concat. This function combines the two tables.

  • NOTE: If both of the tables are array-like, new values are added to the end of the table.
  • NOTE: If either table is not array-like, new values will override old values if conflicting keys exist.
  • NOTE: Concat accepts both BetterTables and regular tables.
local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
t:Insert(3)

local t2 = {5}
t:Concat(t2)

print(t[1]) -- 3
print(t[2]) -- 5

Shuffling an array

local BetterTables = require(path.to.BetterTables)
local t = BetterTables.new()
t:Insert(3)
t:Insert(5)
t:Shuffle()
  • NOTE: Shuffle is done in place.
  • NOTE: Value and index cannot be guaranteed.
  • NOTE: Attempting to shuffle a non-array table will result in an error

Feel free to request new methods you think would be useful!

8 Likes

It has come to my attention that people may prefer this as a library of functions rather than an extension. I’ve just made it possible to call all the functions straight from the library.

To do so, just call the function you want like so:

local BetterTables = require(path.to.BetterTables)
local t = {5, 3, 6, 7, 1, 5, 8}
print(BetterTables.FindIndex(t, 7)) -- 4

This works for any function that you can call on a better table (aside from GetTable, GetType, GetLength, Insert, Remove, GetIterator, and Sort, which exist using the table library, which already exists) all you need to do is pass the table explicitly and then use the same arguments you would on the BetterTable. Also, using the functions like this means you need to call them with a ., rather than with a :.

  • NOTE: Using it as a library with some functions may be a little slower. This is because metadata about the table is stored while using a BetterTable directly, but it shouldn’t really be noticable.

I like to think of it as both
you are expanding the library and you are using already existing functions

2 Likes