7 minute read
This is an assortment of functions that I've developed over time and like to use. They are all generally built with the following expectations:
--!strict.== checks pass. (Note: This is not a requirement, but is generally an improvement if the check can be done in O(1))All code here is MPL 2.0 licensed.
joinListslocal function joinLists<T>(...: { T }?): { T }
	local final = nil
	local changed = false
	for _, list in { ... } do
		assert(list ~= nil, "Luau")
		if #list == 0 then
			continue
		end
		if final == nil then
			final = list
		else
			assert(final ~= nil, "Luau")
			if not changed then
				final = table.clone(final)
				changed = true
			end
			table.move(list, 1, #list, #final + 1, final)
		end
	end
	return final or {}
endit("should join no arguments into an empty list", function()
	expect(joinLists()).toEqual({})
end)
it("should join multiple lists", function()
	expect(joinLists({ 1, 2, 3 }, { 4, 5, 6 })).toEqual({ 1, 2, 3, 4, 5, 6 })
end)
it("should return the same list if only one list is provided", function()
	local list = { 1, 2, 3 }
	expect(joinLists(list)).toBe(list)
end)
it("should join lists with nils in between", function()
	expect(joinLists({ 1, 2, 3 }, nil, { 4, 5, 6 })).toEqual({ 1, 2, 3, 4, 5, 6 })
end)
it("should return the same list if one of the lists is empty", function()
	local list = { 1, 2, 3 }
	expect(joinLists({}, list, {})).toBe(list)
end)joinLists takes a vararg of immutable lists and combines them together. It accepts nil as a value, which it will skip over. If the final list is equivalent to any given input, it will return that input directly.
filterListlocal function filterList<T>(list: { T }, callback: (T) -> boolean): { T }
	local newList = {}
	for _, item in list do
		if callback(item) then
			table.insert(newList, item)
		end
	end
	if #newList == #list then
		return list
	end
	return newList
endfilterList takes a list and a callback, of which it will return a new table of only the values in the list that pass the check. If all values pass the check, it will return the same list.
sort-- Return types must always be the same, we don't want a function that returns a string or number.
-- Unfortunately, a Luau bug prevents this.
-- type CompareCallback<T> = ((T) -> string) | ((T) -> number) | ((T) -> boolean)
export type CompareCallback<T> = (T) -> string | number | boolean
local function sort<T>(list: { T }, ...: CompareCallback<T>): { T }
	local sorted = table.clone(list)
	local sortCallbacks = { ... }
	table.sort(sorted, function(a, b)
		for _, sortCallback in sortCallbacks do
			local keyA = sortCallback(a)
			local keyB = sortCallback(b)
			if keyA == keyB then
				continue
			end
			if typeof(keyA) == "boolean" then
				assert(typeof(keyB) == "boolean", "keyA == bool, keyB is not")
				return keyB
			else
				assert(typeof(keyA) == typeof(keyB), "typeof(keyA) ~= typeof(keyB)")
				return (keyA :: any) < (keyB :: any)
			end
		end
		return false
	end)
	return sorted
endit("should sort with one key", function()
	expect(sort({ 3, 1, 2 }, function(x: number)
		return x
	end)).toEqual({ 1, 2, 3 })
	expect(sort({ "c", "a", "b" }, function(x: string)
		return x
	end)).toEqual({ "a", "b", "c" })
	expect(sort({ true, false }, function(x: boolean)
		return x
	end)).toEqual({ false, true })
end)
it("should sort with multiple keys", function()
	expect(sort({ 3, 1, 2 }, function(x: number)
		return x == 2
	end, function(x: number)
		return x
	end)).toEqual({ 1, 3, 2 })
end)sort takes a list and a vararg of functions that create keys to sort by. Key functions are called in order until one doesn't match.
I created this because table.sort is frankly a pretty unwieldy interface to use, and this both simplifies the process of sorting a lot while also making it trivial to sort things by multiple keys.
localizeIntlocal LocalizationService = game:GetService("LocalizationService")
local localizationTable
local function localizeInt(number: number): string
	if typeof(number) ~= "number" then
		error(`Not localizing a number: {number}`)
	end
	if localizationTable == nil then
		localizationTable = Instance.new("LocalizationTable")
		localizationTable:SetEntries({
			{
				Key = "translatedNumber",
				Source = "{1:num}",
				Values = {
					[LocalizationService.RobloxLocaleId] = "{1:num}",
				},
			},
		})
	end
	return localizationTable
		:GetTranslator(LocalizationService.RobloxLocaleId)
		:FormatByKey("translatedNumber", { number })
		:sub(1, -4)
endA break from the immutable helpers, this function takes an integer and returns it as a number that is worth displaying. That is to say, it takes 1000 and turns it into "1,000". It'll adjust itself depending on the user's locale, such as creating "1.000" if the user is in, say, a European country that uses decimal points.
shallowEquallocal function shallowEqual<T>(x: T, y: T): boolean
	if x == y then
		return true
	end
	if typeof(x) ~= typeof(y) then
		return false
	end
	if typeof(x) == "table" then
		assert(typeof(y) == "table", "Luau")
		if #x ~= #y then
			return false
		end
		for key in x do
			if x[key] ~= y[key] then
				return false
			end
		end
		for key in y do
			if x[key] ~= y[key] then
				return false
			end
		end
	end
	return true
endit("should pass for identical lists", function()
	local x = { 1 }
	expect(shallowEqual(x, x)).toEqual(true)
end)
it("should pass for equal lists", function()
	expect(shallowEqual({ 1, 2, 3 }, { 1, 2, 3 })).toEqual(true)
end)
it("should fail for inequal, same length lists", function()
	expect(shallowEqual({ 1, 2, 3 }, { 1, 2, 4 })).toEqual(false)
end)
it("should fail for lists of different length", function()
	expect(shallowEqual({ 1, 2, 3 }, { 1, 2 })).toEqual(false)
end)shallowEqual checks if two tables share the same keys and values. As a shallow check, it doesn't look inside nested tables, but I find this is extremely rarely necessary in immutable codebases. But if it is...
deepEquallocal function deepEqual<T>(x: T, y: T): boolean
	if typeof(x) == "table" and typeof(y) == "table" then
		for key, value in x :: any do
			if not deepEqual(value, y[key]) then
				return false
			end
		end
		for key in y :: any do
			if x[key] == nil then
				return false
			end
		end
		return true
	end
	return x == y
endit("should pass for identical lists", function()
	local x = { 1 }
	expect(deepEqual(x, x)).toEqual(true)
end)
it("should pass for equal lists", function()
	expect(deepEqual({ 1, 2, 3 }, { 1, 2, 3 })).toEqual(true)
end)
it("should fail for inequal, same length lists", function()
	expect(deepEqual({ 1, 2, 3 }, { 1, 2, 4 })).toEqual(false)
end)
it("should fail for lists of different length", function()
	expect(deepEqual({ 1, 2, 3 }, { 1, 2 })).toEqual(false)
end)
it("should pass for nested tables", function()
	expect(deepEqual({
		x = {
			1,
			2,
		},
		y = 3,
	}, {
		x = {
			1,
			2,
		},
		y = 3,
	})).toEqual(true)
end)deepEqual checks completely through two tables to make sure every key and value match exactly, even if memory addresses differ. I don't remember what I made this for, since it looks like it's used 0 times in my entire codebase, while shallowEqual is used in 4 places. So I stand pretty firm that it just isn't useful.
flattenlocal function flatten<T>(lists: { { T } }): { T }
	local flattened: { T } = {}
	for _, list in lists do
		table.move(list, 1, #list, #flattened + 1, flattened)
	end
	return flattened
endit("should flatten lists", function()
	expect(flatten({
		{ 1, 2, 3 },
		{ 4, 5, 6 },
		{ 7, 8, 9 },
	})).toEqual({ 1, 2, 3, 4, 5, 6, 7, 8, 9 })
end)flatten takes a list of lists and flattens them down into one list.
diffArray-- Compares two arrays.
-- Returns values only in A, values only in B, and values in both.
local function diffArray<T>(arrayA: { T }, arrayB: { T }): ({ T }, { T }, { T })
	local onlyInA = {}
	local onlyInB = {}
	local both = {}
	local mappedA = {}
	for _, itemA in arrayA do
		mappedA[itemA] = (mappedA[itemA] or 0) + 1
	end
	for _, itemB in arrayB do
		local count = mappedA[itemB]
		if count == nil or count == 0 then
			table.insert(onlyInB, itemB)
		else
			mappedA[itemB] -= 1
			table.insert(both, itemB)
		end
	end
	for itemA, countLeft in mappedA do
		for _ = 1, countLeft do
			table.insert(onlyInA, itemA)
		end
	end
	return onlyInA, onlyInB, both
endit("should give the difference between two arrays", function()
	-- stylua: ignore
	local onlyInA, onlyInB, both = diffArray(
		{ "a", "b", "b", "C", "d" },
		{ "A", "b", "c", "a", "a", "e" }
	)
	table.sort(onlyInA)
	table.sort(onlyInB)
	table.sort(both)
	expect(onlyInA).toEqual({ "C", "b", "d" })
	expect(onlyInB).toEqual({ "A", "a", "c", "e" })
	expect(both).toEqual({ "a", "b" })
end)diffArray takes two arrays and returns three lists: the values only in the first list, the values only in the second list, and the values in both. Position doesn't matter.