This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,12 @@
--[[
Package link auto-generated by Rotriever
]]
local PackageIndex = script.Parent.Parent
local package = PackageIndex["roblox_cryo"]["cryo"]
if package.ClassName == "ModuleScript" then
return require(package)
end
return package
@@ -0,0 +1,356 @@
local Root = script.Parent.Parent
local Cryo = require(Root.Parent.Cryo)
local binarySearch = require(Root.binarySearch.binarySearch)
local List = {}
List.__index = List
--[[
Create a new List from a list of values
]]
function List.new(...)
local self = {
values = {},
_immutableDataStructureType = List,
}
setmetatable(self, List)
List._insertInPlace(self, 1, ...)
return self
end
--[[
Internal method that absorbs an existing list-style table as the underlying data.
]]
function List._newCannibalizeTable(tab)
local self = {
values = tab,
_immutableDataStructureType = List,
}
setmetatable(self, List)
return self
end
--[[
Check if a given value is a list.
]]
function List.is(object)
if type(object) ~= "table" then
return false
end
return object._immutableDataStructureType == List
end
--[[
Creates a new List from a list-style table.
]]
function List.newFromListTable(table)
return List.new(unpack(table))
end
--[[
Returns the size of a List.
]]
function List:size()
return #self.values
end
--[[
Creates a (shallow) copy of a list.
]]
function List:copy()
return List.new(unpack(self.values))
end
--[[
Returns the (read only) value at the index.
]]
function List:get(index)
assert(index >= 1 and index <= #self.values, "Index out of bounds!")
return self.values[index]
end
--[[
Creates a new List at which the value at index is changed to value.
]]
function List:set(index, value)
assert(value ~= nil, "Cannot set a value to nil. Use remove() to remove values")
assert(1 <= index and index <= #self.values, "Index out of bounds!")
local new = self:copy()
new.values[index] = value
return new
end
--[[
Expects (any number of) dictionaries of the form { [index] = value }.
Preferred for when multiple sets are needed repeatedly,
as this version does no intermediate copying.
]]
function List:batchSet(...)
local new = self:copy()
local length = select("#", ...)
local values = new.values
for i = 1, length do
local pair = select(i, ...)
for key, value in pairs(pair) do
assert(1 <= key and key <= #values)
values[key] = value
end
end
return new
end
--[[
Creates a new List where the element at index is deleted.
]]
function List:remove(index)
assert(index >= 1 and index <= #self.values, "Index out of bounds!")
local new = self:copy()
table.remove(new.values, index)
return new
end
--[[
Creates a new List where the elements are inserted at index.
]]
function List:insert(index, ...)
assert(index >= 1 and index <= #self.values + 1, "Index out of bounds!")
local new = self:copy()
new:_insertInPlace(index, ...)
return new
end
--[[
Push a value onto the end of the list.
]]
function List:pushBack(value)
return self:insert(#self.values + 1, value)
end
--[[
Push a value onto the front of the list.
]]
function List:pushFront(value)
return self:insert(1, value)
end
--[[
Pop a value from the back of the list.
]]
function List:popBack()
return self:remove(#self.values)
end
--[[
Pop a value from the front of the list.
]]
function List:popFront()
return self:remove(1)
end
--[[
Returns a (shallow) copy of the underlying table.
]]
function List:toTable()
local new = self:copy()
return new.values
end
--[[
Internal method that inserts a number of values in place.
]]
function List:_insertInPlace(index, ...)
local length = select("#", ...)
if length == 0 then
return
end
self:_shift(index, length)
local values = self.values
for i = 0, length - 1 do
values[i + index] = select(i + 1, ...)
end
end
--[[
Internal method that shifts over a number of values to make space for insertion.
]]
function List:_shift(index, numPlacesToShift)
local values = self.values
for i = #self.values, index, -1 do
values[i + numPlacesToShift] = values[i]
values[i] = nil
end
end
--[[
Create a copy of the List with only values for which `callback` returns true.
Calls the callback with (value, index).
]]
function List:filter(callback)
local new = Cryo.List.filter(self.values, callback)
return List._newCannibalizeTable(new)
end
--[[
Create a copy of the List doing a combination filter and map.
If callback returns nil for any item, it is considered filtered from the
list. Any other value is considered the result of the 'map' operation.
]]
function List:filterMap(callback)
local new = Cryo.List.filterMap(self.values, callback)
return List._newCannibalizeTable(new)
end
--[[
Returns the index of the first value found or nil if not found.
]]
function List:find(value)
return Cryo.List.find(self.values, value)
end
--[[
Returns the index of the first value for which predicate(value, index) is truthy, or nil if not found.
]]
function List:findWhere(predicate)
return Cryo.List.findWhere(self.values, predicate)
end
--[[
Performs a left-fold of the List with the given initial value and callback.
]]
function List:foldLeft(callback, initialValue)
return Cryo.List.foldLeft(self.values, callback, initialValue)
end
--[[
Performs a right-fold of the List with the given initial value and callback.
]]
function List:foldRight(callback, initialValue)
return Cryo.List.foldRight(self.values, callback, initialValue)
end
--[[
Returns a new List containing only the elements within the given range.
]]
function List:getRange(startIndex, endIndex)
local new = Cryo.List.getRange(self.values, startIndex, endIndex)
return List._newCannibalizeTable(new)
end
--[[
Create a copy of the List where each value is transformed by `callback`
]]
function List:map(callback)
local new = Cryo.List.map(self.values, callback)
return List._newCannibalizeTable(new)
end
--[[
Joins any number of Lists together into a new List
]]
function List:join(...)
local otherLists = {}
local len = select("#", ...)
for i = 1, len do
otherLists[i] = select(i, ...).values
end
local new = Cryo.List.join(self.values, unpack(otherLists))
return List._newCannibalizeTable(new)
end
--[[
Create a copy of the List with removing the range from the List starting from the index.
]]
function List:removeRange(startIndex, endIndex)
local new = Cryo.List.removeRange(self.values, startIndex, endIndex)
return List._newCannibalizeTable(new)
end
--[[
Creates a new List that has no occurrences of the given value.
]]
function List:removeValue(value)
local new = Cryo.List.removeValue(self.values, value)
return List._newCannibalizeTable(new)
end
--[[
Returns a new List with the reversed order of the given list
]]
function List:reverse()
local new = Cryo.List.reverse(self.values)
return List._newCannibalizeTable(new)
end
--[[
Returns a new List, ordered with the given sort callback.
If no callback is given, the default table.sort will be used.
]]
function List:sort(callback)
local new = Cryo.List.sort(self.values, callback)
return List._newCannibalizeTable(new)
end
--[[
Returns an iterator that traverses the List in a forward direction.
e.g.
for index, value in list:iterator() do
...
end
]]
function List:iterator()
local i = 0
local length = #self.values
return function()
i = i + 1
if i <= length then
return i, self.values[i]
end
end
end
--[[
Returns an iterator that traverses the List in a backward direction.
e.g.
for index, value in list:reverseIterator() do
...
end
]]
function List:reverseIterator()
local i = #self.values + 1
return function()
i = i - 1
if i > 0 then
return i, self.values[i]
end
end
end
--[[
Does a binarySearch in the List, assuming that it is sorted.
Returns the leftmost index of the occurence of value according to the given comaprator, if provided,
otherwise <. If not found, returns nil.
]]
function List:binarySearch(value, comparator)
return binarySearch(self.values, value, comparator)
end
--[[
Potential TODO:
Add O(n) methods for sorted Lists (e.g. setIntersection, setUnion, etc.)
]]
--[[
Specify the behavior of deepJoin for OrderedMap.
]]
List.deepJoin = List.join
return List
@@ -0,0 +1,559 @@
return function()
local List = require(script.Parent.List)
describe("Basic list operations", function()
it("Creates a new empty list", function()
local list = List.new()
expect(list).to.be.ok()
expect(list:size()).to.equal(0)
local tab = list:toTable()
expect(next(tab)).to.never.be.ok()
end)
it("Can tell apart Lists from non-Lists", function()
local nonlist = {}
expect(List.is(nonlist)).to.equal(false)
local number = 5
expect(List.is(number)).to.equal(false)
local list = List.new()
expect(List.is(list)).to.equal(true)
end)
it("Creates a table with some values", function()
local list = List.new(3, 4, 5)
expect(list).to.be.ok()
expect(list:size()).to.equal(3)
expect(list:get(1)).to.equal(3)
expect(list:get(2)).to.equal(4)
expect(list:get(3)).to.equal(5)
end)
it("Creates a table with some values from table", function()
local list = List.newFromListTable({ 3, 4, 5 })
expect(list).to.be.ok()
expect(list:size()).to.equal(3)
expect(list:get(1)).to.equal(3)
expect(list:get(2)).to.equal(4)
expect(list:get(3)).to.equal(5)
end)
it("Can create a copy", function()
local list = List.new(3, 4, 5)
local listCopy = list:copy()
expect(listCopy).never.to.equal(list)
expect(list:get(1)).to.equal(3)
expect(list:get(2)).to.equal(4)
expect(list:get(3)).to.equal(5)
expect(listCopy:size()).to.equal(3)
end)
it("Can be converted immutably to a table", function()
local list = List.new(3, 4, 5)
local tab = list:toTable()
tab[2] = 50
expect(list:get(1)).to.equal(3)
expect(list:get(2)).to.equal(4)
expect(list:get(3)).to.equal(5)
end)
it("Supports immutable set", function()
local list = List.new(3, 4, 5)
local newList = list:set(2, 1000)
expect(newList).never.to.equal(list)
expect(list:get(2)).to.equal(4)
expect(newList:get(2)).to.equal(1000)
end)
it("Supports deletion", function()
local list = List.new(3, 4, 5, 6, 7)
local newList = list:remove(1)
expect(newList).never.to.equal(list)
expect(list:get(1)).to.equal(3)
expect(newList:get(1)).to.equal(4)
expect(newList:size()).to.equal(4)
local newNewList = newList:remove(2)
expect(newNewList).never.to.equal(newList)
expect(newList:get(2)).to.equal(5)
expect(newNewList:get(2)).to.equal(6)
expect(newNewList:size()).to.equal(3)
end)
it("Supports insertion", function()
local list = List.new()
local newList = list:insert(1, 4)
expect(newList).never.to.equal(list)
expect(newList:get(1)).to.equal(4)
expect(newList:size()).to.equal(1)
local anotherList = List.new(3, 4, 5, 6, 7)
local newAnotherList = anotherList:insert(2, 100, 101)
expect(newAnotherList:get(2)).to.equal(100)
expect(newAnotherList:get(3)).to.equal(101)
expect(newAnotherList:get(4)).to.equal(4)
expect(newAnotherList:size()).to.equal(7)
end)
it("Supports push/pop at the back", function()
local list = List.new(3, 4, 5, 6, 7)
local newList = list:pushBack(100)
expect(newList:size()).to.equal(6)
expect(newList:get(newList:size())).to.equal(100)
local newNewList = list:popBack()
expect(newNewList:size()).to.equal(4)
expect(newNewList:get(newNewList:size())).to.equal(6)
local empty = List.new()
local nonempty = empty:pushBack(10)
expect(nonempty:size()).to.equal(1)
expect(nonempty:get(nonempty:size())).to.equal(10)
end)
it("Supports push/pop at the front", function()
local list = List.new(3, 4, 5, 6, 7)
local newList = list:pushFront(100)
expect(newList:size()).to.equal(6)
expect(newList:get(1)).to.equal(100)
local newNewList = list:popFront()
expect(newNewList:size()).to.equal(4)
expect(newNewList:get(1)).to.equal(4)
local empty = List.new()
local nonempty = empty:pushFront(10)
expect(nonempty:size()).to.equal(1)
expect(nonempty:get(1)).to.equal(10)
end)
it("Supports batch setting", function()
local list = List.new(2, 3, 4, 5, 6, 7)
local newList = list:batchSet({
[2] = 100,
[4] = 1000,
})
expect(newList).never.to.equal(list)
expect(newList:get(1)).to.equal(2)
expect(newList:get(2)).to.equal(100)
expect(newList:get(4)).to.equal(1000)
expect(newList:size()).to.equal(6)
end)
end)
describe("More advanced functionality", function()
describe("Filtering", function()
it("should use the callback", function()
local list = List.new(3, 4, 5, 6, 7)
local newList = list:filter(function(value, index)
return value % 2 == 0
end)
expect(newList).never.to.equal(list)
expect(newList:size()).to.equal(2)
expect(list:get(1)).to.equal(3)
expect(newList:get(1)).to.equal(4)
expect(list:get(2)).to.equal(4)
expect(newList:get(2)).to.equal(6)
end)
it("should work with an empty List", function()
local called = false
local function callback()
called = true
return true
end
local list = List.new()
local newList = list:filter(callback)
expect(newList:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("FilterMapping", function()
it("should return a new table", function()
local list = List.new(1, 2, 3)
local function callback()
return 1
end
local newList = list:filterMap(callback)
expect(list).never.to.equal(newList)
end)
it("should correctly use the filter callback", function()
local list = List.new(1, 2, 3, 4, 5)
local function doubleOddOnly(value)
if value % 2 == 0 then
return nil
else
return value * 2
end
end
local newList = list:filterMap(doubleOddOnly)
expect(newList:size()).to.equal(3)
expect(newList:get(1)).to.equal(2)
expect(newList:get(2)).to.equal(6)
expect(newList:get(3)).to.equal(10)
end)
it("should work with an empty table", function()
local called = false
local function callback()
called = true
return true
end
local list = List.new()
local newList = list:filterMap(callback)
expect(newList:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("Joining", function()
it("Should work with two tables", function()
local first = List.new(1, 2, 3, 4, 5)
local second = List.new(100, 101, 102)
local newList = first:join(second)
expect(newList:get(1)).to.equal(1)
expect(newList:get(5)).to.equal(5)
expect(newList:get(6)).to.equal(100)
expect(newList:get(8)).to.equal(102)
expect(newList:size()).to.equal(8)
end)
it("Should work with multiple tables", function()
local first = List.new(1, 2, 3, 4, 5)
local second = List.new(100, 101, 102)
local third = List.new(1000, 1001, 1002)
local newList = first:join(second, third)
expect(newList:get(1)).to.equal(1)
expect(newList:get(5)).to.equal(5)
expect(newList:get(6)).to.equal(100)
expect(newList:get(8)).to.equal(102)
expect(newList:get(9)).to.equal(1000)
expect(newList:get(11)).to.equal(1002)
expect(newList:size()).to.equal(11)
end)
end)
describe("Iterators", function()
it("Should work in a for loop", function()
local list = List.new(3, 4, 5, 6, 7)
local count = 1
for index, value in list:iterator() do
if count == 1 then
expect(value).to.equal(3)
elseif count == 5 then
expect(value).to.equal(7)
end
expect(value).to.equal(index + 2)
count = count + 1
end
end)
it("Should work for empty lists", function()
local list = List.new()
local wasAnythingDone = false
for _, _ in list:iterator() do
wasAnythingDone = true
end
expect(wasAnythingDone).to.equal(false)
end)
it("Should work in reverse", function()
local list = List.new(3, 4, 5, 6, 7)
local count = 1
for index, value in list:reverseIterator() do
if count == 5 then
expect(value).to.equal(3)
elseif count == 1 then
expect(value).to.equal(7)
end
expect(value).to.equal(index + 2)
count = count + 1
end
end)
describe("binarySearch", function()
it("Works for a sorted List", function()
local list = List.new(1, 3, 5, 6, 7)
expect(list:binarySearch(1)).to.equal(1)
expect(list:binarySearch(3)).to.equal(2)
expect(list:binarySearch(5)).to.equal(3)
expect(list:binarySearch(6)).to.equal(4)
expect(list:binarySearch(7)).to.equal(5)
expect(list:binarySearch(100)).to.never.be.ok()
end)
it("Works for a sorted List with duplicates", function()
local list = List.new(1, 1, 3, 3, 3, 5, 5, 10, 11)
expect(list:binarySearch(1)).to.equal(1)
expect(list:binarySearch(3)).to.equal(3)
expect(list:binarySearch(5)).to.equal(6)
expect(list:binarySearch(10)).to.equal(8)
expect(list:binarySearch(11)).to.equal(9)
end)
it("Works for an empty List", function()
local list = List.new()
expect(list:binarySearch(1)).to.never.be.ok()
end)
it("Works for a special comparator", function()
local list = List.new(5, 6, 1, 5, 7, 3)
local reverse = function(lhs, rhs)
return rhs < lhs
end
local newList = list:sort(reverse)
expect(newList:binarySearch(7, reverse)).to.equal(1)
expect(newList:binarySearch(6, reverse)).to.equal(2)
expect(newList:binarySearch(5, reverse)).to.equal(3)
expect(newList:binarySearch(3, reverse)).to.equal(5)
expect(newList:binarySearch(1, reverse)).to.equal(6)
end)
end)
end)
--[[
TODO: port over more of these Cryo tests, if necessary.
]]
describe("Cryo functions", function()
describe("find", function()
local list = List.new(5, 4, 3, 2, 1)
it("should return the correct index", function()
expect(list:find(1)).to.equal(5)
expect(list:find(2)).to.equal(4)
expect(list:find(3)).to.equal(3)
expect(list:find(4)).to.equal(2)
expect(list:find(5)).to.equal(1)
end)
it("should work with an empty table", function()
local empty = List.new()
expect(empty:find(1)).to.never.be.ok()
end)
it("should return nil when the given value is not found", function()
expect(list:find(1000)).to.never.be.ok()
end)
it("should return the index of the first value found", function()
local repeated = List.new(1, 2, 2)
expect(repeated:find(2)).to.equal(2)
end)
end)
describe("findWhere", function()
it("should return the correct index", function()
local list = List.new(1, 5, 10, 7)
local isEven = function(value)
return value % 2 == 0
end
local isOdd = function(value)
return value % 2 == 1
end
expect(list:findWhere(isEven)).to.equal(3)
expect(list:findWhere(isOdd)).to.equal(1)
end)
it("should work with an empty table", function()
local empty = List.new()
local anything = function()
return true
end
expect(empty:findWhere(anything)).to.never.be.ok()
end)
it("should return nil when the when no value satisfies the predicate", function()
local numbers = List.new(1, 2, 3)
local isFour = function(value)
return value == 4
end
expect(numbers:findWhere(isFour)).to.never.be.ok()
end)
it("should return the index of the first value for which the predicate is true", function()
local numbers = List.new(1, 1, 1, 2, 2)
local isTwo = function(value)
return value == 2
end
expect(numbers:findWhere(isTwo)).to.equal(4)
end)
it("should allow access to both value and index in the predicate function", function()
local numbers = List.new(1, 1, 2, 2, 1)
local sumValueAndIndexToFive = function(value, index)
return value + index == 5
end
expect(numbers:findWhere(sumValueAndIndexToFive)).to.equal(3)
end)
end)
describe("foldLeft", function()
it("should call the callback for each element", function()
local a = List.new(4, 5, 6)
local copy = {}
a:foldLeft(function(accum, value, index)
copy[index] = value
return accum
end, 0)
expect(#copy).to.equal(a:size())
for key, value in a:iterator() do
expect(value).to.equal(copy[key])
end
end)
end)
describe("foldRight", function()
it("should call the callback for each element", function()
local a = List.new(4, 5, 6)
local copy = {}
a:foldRight(function(accum, value, index)
copy[index] = value
return accum
end, 0)
expect(#copy).to.equal(a:size())
for key, value in a:iterator() do
expect(value).to.equal(copy[key])
end
end)
end)
describe("getRange", function()
it("should return the correct range", function()
local a = List.new(1, 2, 3, 4)
local b = a:getRange(2, 3)
expect(b:get(1)).to.equal(2)
expect(b:get(2)).to.equal(3)
expect(b:size()).to.equal(2)
local c = a:getRange(4, 4)
expect(c:size()).to.equal(1)
expect(c:get(1)).to.equal(4)
end)
end)
describe("map", function()
it("should call the callback for each element", function()
local a = List.new(5, 6, 7)
local copy = {}
a:map(function(value, index)
copy[index] = value
return value
end)
for key, value in a:iterator() do
expect(copy[key]).to.equal(value)
end
for key, value in pairs(copy) do
expect(value).to.equal(a:get(key))
end
end)
end)
describe("removeRange", function()
it("should remove elements properly", function()
local a = List.new(1, 2, 3)
local b = a:removeRange(2, 2)
expect(b:size()).to.equal(2)
expect(b:get(1)).to.equal(1)
expect(b:get(2)).to.equal(3)
local c = List.new(1, 2, 3, 4, 5, 6)
local d = c:removeRange(1, 4)
expect(d:size()).to.equal(2)
expect(d:get(1)).to.equal(5)
expect(d:get(2)).to.equal(6)
local e = c:removeRange(2, 5)
expect(e:size()).to.equal(2)
expect(e:get(1)).to.equal(1)
expect(e:get(2)).to.equal(6)
end)
end)
describe("removeValue", function()
it("should remove all occurences of the same given value", function()
local a = List.new(1, 2, 2, 3)
local b = a:removeValue(2)
expect(b:size()).to.equal(2)
expect(b:get(1)).to.equal(1)
expect(b:get(2)).to.equal(3)
end)
end)
describe("reverse", function()
it("should reverse the list", function()
local a = List.new(1, 2, 3, 4)
local b = a:reverse()
expect(b:get(1)).to.equal(4)
expect(b:get(2)).to.equal(3)
expect(b:get(3)).to.equal(2)
expect(b:get(4)).to.equal(1)
end)
end)
describe("sort", function()
it("should sort with the default table.sort when no callback is given", function()
local a = List.new(4, 2, 5, 3, 1)
local b = a:sort()
local aTable = a:toTable()
table.sort(aTable)
expect(b:size()).to.equal(a:size())
for i = 1, #aTable do
expect(b:get(i)).to.equal(aTable[i])
end
end)
it("should sort with the given callback", function()
local a = List.new(1, 2, 5, 3, 4)
local function order(first, second)
return first > second
end
local b = a:sort(order)
local aTable = a:toTable()
table.sort(aTable, order)
expect(b:size()).to.equal(#aTable)
for i = 1, #a do
expect(b:get(i)).to.equal(aTable[i])
end
end)
end)
end)
end)
end
@@ -0,0 +1,3 @@
-- Alias for Cryo.None
local Cryo = require(script.Parent.Parent.Cryo)
return Cryo.None
@@ -0,0 +1,479 @@
local Root = script.Parent.Parent
local None = require(Root.None)
local List = require(Root.List.List)
local UnorderedMap = require(Root.UnorderedMap.UnorderedMap)
local binarySearch = require(Root.binarySearch.binarySearch)
local deepJoin = require(Root.deepJoin.deepJoin)
local sort = require(Root.sort.sort)
local OrderedMap = {}
--[[
Currently, exactly batchSet and join will remove None values.
]]
OrderedMap.__index = OrderedMap
--[[
A BST implementation is not necessary for an immutable copy-on-write OrderedMap, since
we cannot take much advantage of O(log n) insert/deletes anyways.
]]
--[[
Create a new OrderedMap from a sortPredicate and any number of dictionaries of values of the form
{ [key] = value }
sortPredicate should be a function with the following signature:
sortPredicate(key1, key2) -> bool
returning true if key1 < key2 in the sorting invariant.
]]
function OrderedMap.new(sortPredicate, ...)
local self = {
keys = {},
values = {},
sortPredicate = sortPredicate or sort.default,
_immutableDataStructureType = OrderedMap,
}
setmetatable(self, OrderedMap)
OrderedMap._insertInPlace(self, ...)
return self
end
--[[
Returns a new UnorderedMap with the same key-value pairs.
]]
function OrderedMap:toUnorderedMap()
return UnorderedMap._newCannibalizeTable(self.values)
end
--[[
Returns if a value is an OrderedMap.
]]
function OrderedMap.is(value)
if type(value) ~= "table" then
return false
end
return value._immutableDataStructureType == OrderedMap
end
--[[
Returns the value at key.
]]
function OrderedMap:get(key)
return self.values[key]
end
--[[
Returns a new OrderedMap, setting the value at key to be value.
]]
function OrderedMap:set(key, value)
if self:get(key) == nil then
local new = self:copy()
new:_insertPairInPlace(key, value)
return new
end
local new = self:copy()
new.values[key] = value
return new
end
--[[
Gets the index-th key, value in the OrderedMap according to the sorting invariant.
]]
function OrderedMap:getByIndex(index)
local id = self.keys[index]
if id == nil then
return nil
end
return id, self.values[id]
end
--[[
Returns a List of all of the values in the map.
]]
function OrderedMap:getValues()
local new = {}
for index, key in ipairs(self.keys) do
new[index] = self.values[key]
end
return List._newCannibalizeTable(new)
end
--[[
Returns a List of all of the keys in the map.
]]
function OrderedMap:getKeys()
return List._newCannibalizeTable(self.keys)
end
--[[
Returns the size (number of key-value pairs) of the map.
]]
function OrderedMap:size()
return #self.keys
end
--[[
Returns a new OrderedMap with the pairs at all given keys removed.
]]
function OrderedMap:remove(...)
local new = OrderedMap.new(self.sortPredicate)
local len = select("#", ...)
local newKeys = new.keys
local newValues = new.values
for key, value in pairs(self.values) do
newValues[key] = value
end
for i = 1, len do
local key = select(i, ...)
newValues[key] = nil
end
for _, value in ipairs(self.keys) do
if new.values[value] ~= nil then
newKeys[#(newKeys)+1] = value
end
end
return new
end
--[[
Internal method for removing a value from the map, in place.
]]
function OrderedMap:_removeInPlace(key)
self.values[key] = nil
local indexToRemove = binarySearch(self.keys, key, self.sortPredicate)
if not indexToRemove then
return
end
table.remove(self.keys, indexToRemove)
end
--[[
Joins any number of (basic table) dictionaries of values of the form
{ [key] = value }
into the OrderedMap, creating a new OrderedMap.
]]
function OrderedMap:batchSet(...)
local new = self:copyRemoveNone()
new:_insertInPlaceRemoveNone(...)
return new
end
--[[
Creates a (shallow) copy of the OrderedMap.
]]
function OrderedMap:copy()
local new = OrderedMap.new(self.sortPredicate)
local newKeys = new.keys
local newValues = new.values
for key, value in ipairs(self.keys) do
newKeys[key] = value
end
for key, value in pairs(self.values) do
newValues[key] = value
end
return new
end
--[[
Creates a (shallow) copy of the OrderedMap, with all pairs with value None removed.
]]
function OrderedMap:copyRemoveNone()
local new = OrderedMap.new(self.sortPredicate)
local newKeys = new.keys
local newValues = new.values
for key, value in ipairs(self.keys) do
if self.values[value] ~= None then
newKeys[key] = value
end
end
for key, value in pairs(self.values) do
if value ~= None then
newValues[key] = value
end
end
return new
end
--[[
Returns the first key, value in the OrderedMap.
]]
function OrderedMap:first()
if self.keys[1] then
return self.keys[1], self:get(self.keys[1])
end
end
--[[
Returns the last key, value in the OrderedMap.
]]
function OrderedMap:last()
local i = #self.keys
if self.keys[i] then
return self.keys[i], self:get(self.keys[i])
end
end
--[[
Create a new OrderedMap, applying the given predicate to each element in
this OrderedMap.
Predicate should have the signature
predicate(value, key) -> newValue, newKey
Analogous to 'map' on a list.
]]
function OrderedMap:map(predicate)
local new = OrderedMap.new(self.sortPredicate)
local keyChanged = false
for key, value in self:iterator() do
local newValue, newKey = predicate(value, key)
newKey = newKey or key
newValue = newValue or value
if key ~= newKey then
keyChanged = true
end
new:_insertPairInPlaceUnsorted(newKey, newValue)
end
if keyChanged then
new:_sortInPlace()
end
return new
end
--[[
Create a new OrderedMap, where each key-value pair is included iff callback(value, key) is truthy.
callback should be a function of signature
callback(value, key) -> bool
]]
function OrderedMap:filter(callback)
local new = OrderedMap.new(self.sortPredicate)
for key, value in self:iterator() do
if callback(value, key) then
new:_insertPairInPlaceUnsorted(key, value)
end
end
new:_sortInPlace()
return new
end
--[[
Join any number of OrderedMaps. The sorting comparator of the leftmost argument/self is
used for the returned OrderedMap.
]]
function OrderedMap:join(...)
local new = self:copyRemoveNone()
for i = 1, select("#", ...) do
local other = select(i, ...)
if other:size() > 0 then
for key, value in other:iterator() do
new:_insertPairInPlaceUnsortedRemoveNone(key, value)
end
end
end
new:_sortInPlace()
return new
end
--[[
Internal method for inserting values without sorting the map.
This means that the invariants that the map exposes will be broken until
the _sortInPlace() method is called.
]]
function OrderedMap:_insertInPlaceUnsorted(...)
local len = select("#", ...)
for i = 1, len do
local pair = select(i, ...)
for key, value in pairs(pair) do
self:_insertPairInPlaceUnsorted(key, value)
end
end
end
--[[
Internal method for inserting a single pair without sorting the map.
This means that the invariants that the map exposes will be broken until
the _sortInPlace() method is called.
]]
function OrderedMap:_insertPairInPlaceUnsorted(key, value)
if self.values[key] == nil then
table.insert(self.keys, key)
end
self.values[key] = value
end
--[[
Internal method for inserting values without sorting the map, removing None instances.
This means that the invariants that the map exposes will be broken until
the _sortInPlace() method is called.
]]
function OrderedMap:_insertInPlaceUnsortedRemoveNone(...)
local len = select("#", ...)
for i = 1, len do
local pair = select(i, ...)
for key, value in pairs(pair) do
self:_insertPairInPlaceUnsortedRemoveNone(key, value)
end
end
end
--[[
Internal method for inserting a single pair without sorting the map, removing None instances.
This means that the invariants that the map exposes will be broken until
the _sortInPlace() method is called.
]]
function OrderedMap:_insertPairInPlaceUnsortedRemoveNone(key, value)
if value == None then
self:_removeInPlace(key)
else
if self.values[key] == nil then
table.insert(self.keys, key)
end
self.values[key] = value
end
end
--[[
Sorts the map; used in cases where the map would become out-of-order when
using internal recommendations.
]]
function OrderedMap:_sortInPlace()
table.sort(self.keys, self.sortPredicate)
end
--[[
Returns an iterator for the OrderedMap.
Example:
for key, value in orderedMap:iterator() do
]]
function OrderedMap:iterator()
local i = 0
local length = #self.keys
-- Iterator function
return function()
i = i + 1
if i <= length then
local key = self.keys[i]
return key, self.values[key], i
end
end
end
--[[
Returns an iterator for the OrderedMap that traverses in the reverse direction.
Example:
for key, value in orderedMap:reverseIterator() do
]]
function OrderedMap:reverseIterator()
local i = #self.keys + 1
-- Iterator function
return function()
i = i - 1
if i > 0 then
local key = self.keys[i]
return key, self.values[key], i
end
end
end
--[[
Internal function that inserts the given values into the map in-place.
Expects { [key] = value } dictionaries.
This operation mutates the map; generally you should use set or batchSet instead.
]]
function OrderedMap:_insertInPlace(...)
self:_insertInPlaceUnsorted(...)
self:_sortInPlace()
end
--[[
Internal function that inserts the given pair into the map in-place.
This operation mutates the map; generally you should use set or batchSet instead.
]]
function OrderedMap:_insertPairInPlace(key, value)
self:_insertPairInPlaceUnsorted(key, value)
self:_sortInPlace()
end
--[[
Internal function that inserts the given values into the map in-place, removing None instances.
Expects { [key] = value } dictionaries.
This operation mutates the map; generally you should use set or batchSet instead.
]]
function OrderedMap:_insertInPlaceRemoveNone(...)
self:_insertInPlaceUnsortedRemoveNone(...)
self:_sortInPlace()
end
--[[
Specify the behavior of deepJoin for OrderedMap.
]]
function OrderedMap:deepJoin(rhs)
local newMap = OrderedMap.new(self.sortPredicate)
for lhsKey, lhsValue in self:iterator() do
local rhsValue = rhs:get(lhsKey)
if rhsValue then
if type(rhsValue) == "table" and type(lhsValue) == "table" then
newMap:_insertPairInPlaceUnsorted(lhsKey, deepJoin(lhsValue, rhsValue))
else
if rhsValue ~= None then
newMap:_insertPairInPlaceUnsorted(lhsKey, rhsValue)
end
end
else
if lhsValue ~= None then
newMap:_insertPairInPlaceUnsorted(lhsKey, lhsValue)
end
end
end
-- Copy over rhs keys that aren't in lhs
for rhsKey, rhsValue in rhs:iterator() do
local lhsValue = self:get(rhsKey)
if not lhsValue and rhsValue ~= None then
newMap:_insertPairInPlaceUnsorted(rhsKey, rhsValue)
end
end
newMap:_sortInPlace()
return newMap
end
return OrderedMap
@@ -0,0 +1,621 @@
return function()
local Root = script.Parent.Parent
local OrderedMap = require(script.Parent.OrderedMap)
local UnorderedMap = require(Root.UnorderedMap.UnorderedMap)
local None = require(Root.None)
local sort = require(Root.sort.sort)
describe("Basic ordered map operations", function()
it("Creates a new empty table", function()
local map = OrderedMap.new()
expect(map).to.be.ok()
expect(map:size()).to.equal(0)
end)
it("Can tell apart OrderedMaps from non-OrderedMaps", function()
local nonmap = {}
expect(OrderedMap.is(nonmap)).to.equal(false)
local number = 5
expect(OrderedMap.is(number)).to.equal(false)
local map = OrderedMap.new()
expect(OrderedMap.is(map)).to.equal(true)
end)
it("Correctly returns nil when accessing an OOB index", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
expect(map:getByIndex(1000)).to.never.be.ok()
expect(map:size()).to.equal(3)
expect(map:get("apple")).to.equal(1)
expect(map:get("grapes")).to.equal(2)
expect(map:get("banana")).to.equal(10)
end)
it("Creates an ordered with some values", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
expect(map).to.be.ok()
expect(map:size()).to.equal(3)
expect(map:get("apple")).to.equal(1)
expect(map:get("grapes")).to.equal(2)
expect(map:get("banana")).to.equal(10)
-- Good sorting by key
local firstKey, firstValue = map:first()
local secondKey, secondValue = map:getByIndex(2)
local lastKey, lastValue = map:last()
expect(firstKey).to.equal("apple")
expect(firstValue).to.equal(1)
expect(secondKey).to.equal("banana")
expect(secondValue).to.equal(10)
expect(lastKey).to.equal("grapes")
expect(lastValue).to.equal(2)
end)
it("Returns nil on a miss", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
expect(map:get("broccoli")).never.to.be.ok()
end)
it("Creates a table with some values from multiple tables", function()
local map = OrderedMap.new(sort.reverse, {
apple = 1,
grapes = 10,
banana = 2,
}, {
orange = 5,
banana = 5,
})
expect(map).to.be.ok()
expect(map:size()).to.equal(4)
expect(map:get("orange")).to.equal(5)
expect(map:get("grapes")).to.equal(10)
expect(map:get("banana")).to.equal(5)
end)
it("Can create a copy", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local mapCopy = map:copy()
expect(mapCopy).never.to.equal(map)
expect(mapCopy).to.be.ok()
expect(mapCopy:size()).to.equal(3)
expect(mapCopy:get("apple")).to.equal(1)
expect(mapCopy:get("grapes")).to.equal(2)
expect(mapCopy:get("banana")).to.equal(10)
-- Good sorting by key
local firstKey, firstValue = mapCopy:first()
local secondKey, secondValue = mapCopy:getByIndex(2)
local lastKey, lastValue = map:last()
expect(firstKey).to.equal("apple")
expect(firstValue).to.equal(1)
expect(secondKey).to.equal("banana")
expect(secondValue).to.equal(10)
expect(lastKey).to.equal("grapes")
expect(lastValue).to.equal(2)
end)
it("Can be converted immutably to Lists of keys and values", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local keys = map:getKeys()
local values = map:getValues()
expect(keys:get(1)).to.equal("apple")
expect(keys:get(2)).to.equal("banana")
expect(keys:get(3)).to.equal("grapes")
expect(values:get(1)).to.equal(1)
expect(values:get(2)).to.equal(10)
expect(values:get(3)).to.equal(2)
end)
it("Supports immutable set", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local newMap = map:set("apple", 1000)
expect(newMap).never.to.equal(map)
expect(map:get("apple")).to.equal(1)
expect(newMap:get("apple")).to.equal(1000)
local newNewMap = newMap:set("lettuce", -100)
expect(newNewMap:get("lettuce")).to.equal(-100)
end)
it("Supports batch set", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local newMap = map:batchSet({
apple = 100,
watermelon = 4,
}, {
watermelon = 100,
kiwi = 3,
})
expect(newMap).never.to.equal(map)
expect(map:get("apple")).to.equal(1)
expect(newMap:get("apple")).to.equal(100)
expect(newMap:get("watermelon")).to.equal(100)
expect(newMap:get("kiwi")).to.equal(3)
expect((newMap:first())).to.equal("apple")
expect((newMap:last())).to.equal("watermelon")
expect(map:get("kiwi")).to.never.be.ok()
expect(newMap:size()).to.equal(5)
end)
it("Supports join", function()
local map1 = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local map2 = OrderedMap.new(sort.reverse, {
apple = 100,
watermelon = 4,
})
local map3 = OrderedMap.new(sort.reverse, {
watermelon = 100,
kiwi = 3,
})
local newMap = map1:join(map2, map3)
expect(newMap).never.to.equal(map1)
expect(map1:get("apple")).to.equal(1)
expect(newMap:get("apple")).to.equal(100)
expect(newMap:get("watermelon")).to.equal(100)
expect(newMap:get("kiwi")).to.equal(3)
expect((newMap:first())).to.equal("apple")
expect((newMap:last())).to.equal("watermelon")
expect(map1:get("kiwi")).to.never.be.ok()
expect(newMap:size()).to.equal(5)
end)
it("Supports None", function()
local map1 = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
useless = None,
})
expect(map1:size()).to.equal(4)
local map2 = OrderedMap.new(sort.reverse, {
apple = None,
watermelon = 4,
})
local map3 = OrderedMap.new(sort.reverse, {
watermelon = 100,
kiwi = 3,
})
local newMap = map1:join(map2, map3)
expect(newMap).never.to.equal(map1)
expect(newMap:get("apple")).to.never.be.ok()
expect(newMap:get("watermelon")).to.equal(100)
expect(newMap:get("kiwi")).to.equal(3)
expect(newMap:get("useless")).to.never.be.ok()
expect((newMap:first())).to.equal("banana")
expect((newMap:last())).to.equal("watermelon")
expect(newMap:size()).to.equal(4)
newMap = map1:batchSet({
apple = None,
watermelon = 4,
}, {
watermelon = 100,
kiwi = 3,
})
expect(newMap).never.to.equal(map1)
expect(newMap:get("apple")).to.never.be.ok()
expect(newMap:get("watermelon")).to.equal(100)
expect(newMap:get("kiwi")).to.equal(3)
expect(newMap:get("useless")).to.never.be.ok()
expect((newMap:first())).to.equal("banana")
expect((newMap:last())).to.equal("watermelon")
expect(newMap:size()).to.equal(4)
end)
it("Supports deletion", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local newMap = map:remove("apple", "grapes")
expect(newMap:get("apple")).to.never.be.ok()
expect(newMap:get("grapes")).to.never.be.ok()
expect(newMap:get("banana")).to.equal(10)
expect(map:get("apple")).to.be.ok()
expect(map:get("grapes")).to.be.ok()
expect(map:get("banana")).to.be.ok()
expect(newMap:size()).to.equal(1)
end)
end)
describe("More advanced functionality", function()
describe("Mapping", function()
it("should use the callback", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local newMap = map:map(function(value, key)
return value * 2, key .. " fruit"
end)
expect(newMap).never.to.equal(map)
expect(newMap:size()).to.equal(3)
expect(map:get("apple fruit")).to.never.be.ok()
expect(newMap:get("apple fruit")).to.equal(2)
expect(newMap:get("apple")).to.never.be.ok()
expect(newMap:get("banana fruit")).to.equal(20)
end)
it("should maintain the sorting invariant", function()
local map = OrderedMap.new(sort.default, {
apple = 1,
cheese = 5,
banana = 10,
})
local newMap = map:map(function(value, key)
return value, key:sub(2)
end)
expect(newMap).never.to.equal(map)
expect(newMap:size()).to.equal(3)
expect(newMap:get("pple")).to.equal(1)
local firstKey, firstValue = newMap:first()
expect(firstKey).to.equal("anana")
expect(firstValue).to.equal(10)
local secondKey, secondValue = newMap:getByIndex(2)
expect(secondKey).to.equal("heese")
expect(secondValue).to.equal(5)
local lastKey, lastValue = newMap:last()
expect(lastKey).to.equal("pple")
expect(lastValue).to.equal(1)
end)
it("should work with an empty OrderedMap", function()
local called = false
local function callback()
called = true
return "placeholderkey", "placeholdervalue"
end
local map = OrderedMap.new()
local newMap = map:map(callback)
expect(newMap:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("Filtering", function()
it("should use the callback", function()
local map = OrderedMap.new(sort.default, {
orange = 100,
apple = 1,
grapes = 2,
banana = 10,
lemons = 4,
})
local newMap = map:filter(function(value, key)
return value < 9 or key == "orange"
end)
expect(newMap).never.to.equal(map)
expect(newMap:size()).to.equal(4)
expect(newMap:get("apple")).to.equal(1)
expect(newMap:get("banana")).to.never.be.ok()
expect(map:get("banana")).to.equal(10)
expect(newMap:get("orange")).to.equal(100)
end)
it("should maintain the sorting invariant", function()
local map = OrderedMap.new(sort.default, {
orange = 100,
apple = 1,
grapes = 2,
banana = 10,
lemons = 4,
})
local newMap = map:filter(function(value, key)
return value < 9 or key == "orange"
end)
local firstKey, firstValue = newMap:first()
expect(firstKey).to.equal("apple")
expect(firstValue).to.equal(1)
local secondKey, secondValue = newMap:getByIndex(2)
expect(secondKey).to.equal("grapes")
expect(secondValue).to.equal(2)
local lastKey, lastValue = newMap:last()
expect(lastKey).to.equal("orange")
expect(lastValue).to.equal(100)
end)
it("should work with an empty OrderedMap", function()
local called = false
local function callback()
called = true
return true
end
local map = OrderedMap.new()
local newMap = map:filter(callback)
expect(newMap:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("Iterators", function()
it("Should work in a for loop", function()
local map = OrderedMap.new(sort.default, {
orange = 100,
apple = 1,
grapes = 2,
banana = 10,
lemons = 4,
})
local count = 1
for key, value in map:iterator() do
if count == 1 then
expect(key).to.equal("apple")
expect(value).to.equal(1)
elseif count == 2 then
expect(key).to.equal("banana")
expect(value).to.equal(10)
elseif count == 3 then
expect(key).to.equal("grapes")
expect(value).to.equal(2)
elseif count == 4 then
expect(key).to.equal("lemons")
expect(value).to.equal(4)
elseif count == 5 then
expect(key).to.equal("orange")
expect(value).to.equal(100)
end
count = count + 1
end
expect(count - 1).to.equal(5)
end)
it("Should work for empty maps", function()
local map = OrderedMap.new()
local doAnything = false
for _, _ in map:iterator() do
doAnything = true
end
expect(doAnything).to.equal(false)
end)
it("Should work in reverse", function()
local map = OrderedMap.new(sort.default, {
orange = 100,
apple = 1,
grapes = 2,
banana = 10,
lemons = 4,
})
local count = 1
for key, value in map:reverseIterator() do
if count == 5 then
expect(key).to.equal("apple")
expect(value).to.equal(1)
elseif count == 4 then
expect(key).to.equal("banana")
expect(value).to.equal(10)
elseif count == 3 then
expect(key).to.equal("grapes")
expect(value).to.equal(2)
elseif count == 2 then
expect(key).to.equal("lemons")
expect(value).to.equal(4)
elseif count == 1 then
expect(key).to.equal("orange")
expect(value).to.equal(100)
end
count = count + 1
end
expect(count - 1).to.equal(5)
end)
end)
describe("Downcasting", function()
it("Should successfully downcast to an UnorderedMap", function()
local ordered = OrderedMap.new(sort.default, {
orange = 100,
apple = 1,
grapes = 2,
banana = 10,
lemons = 4,
})
local unordered = ordered:toUnorderedMap()
expect(UnorderedMap.is(unordered)).to.equal(true)
expect(unordered:get("orange")).to.equal(100)
expect(unordered:get("apple")).to.equal(1)
expect(unordered:get("grapes")).to.equal(2)
expect(unordered:get("banana")).to.equal(10)
expect(unordered:get("lemons")).to.equal(4)
expect(unordered:size()).to.equal(5)
end)
end)
end)
describe("deepJoin", function()
it("Should work with basic maps", function()
local map1 = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local map2 = OrderedMap.new(sort.default, {
apple = 100,
watermelon = 4,
})
local newMap = map1:deepJoin(map2)
expect(newMap:get("apple")).to.equal(100)
expect(newMap:get("watermelon")).to.equal(4)
expect(newMap:get("grapes")).to.equal(2)
expect(newMap:get("banana")).to.equal(10)
expect((newMap:first())).to.equal("apple")
expect((newMap:last())).to.equal("watermelon")
expect(newMap:size()).to.equal(4)
end)
it("Should work with nested maps", function()
local innerMap1 = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local innerMap2 = OrderedMap.new(sort.default, {
peach = -10,
grapes = 15
})
local innerMap3 = OrderedMap.new(sort.default, {
apple = 100,
banana = 1000,
beans = 400,
})
local innerMap4 = OrderedMap.new(sort.default, {
peach = 30,
})
local outerMap1 = OrderedMap.new(sort.default, {
inner1 = innerMap1,
inner2 = innerMap2,
irrelevantInteger = 5,
})
local outerMap2 = OrderedMap.new(sort.default, {
inner1 = innerMap3,
inner2 = innerMap4,
irrelevantString = "hello!",
})
local newMap = outerMap1:deepJoin(outerMap2)
expect(newMap:size()).to.equal(4)
expect(newMap:get("irrelevantInteger")).to.equal(5)
expect(newMap:get("irrelevantString")).to.equal("hello!")
local mergedInner1 = newMap:get("inner1")
local mergedInner2 = newMap:get("inner2")
expect(mergedInner1:size()).to.equal(4)
expect(mergedInner1:get("apple")).to.equal(100)
expect(mergedInner1:get("grapes")).to.equal(2)
expect(mergedInner1:get("banana")).to.equal(1000)
expect(mergedInner1:get("beans")).to.equal(400)
expect(mergedInner2:size()).to.equal(2)
expect(mergedInner2:get("peach")).to.equal(30)
expect(mergedInner2:get("grapes")).to.equal(15)
end)
it("Should work with an empty map", function()
local map1 = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
})
local map2 = OrderedMap.new(sort.default)
local newMap = map1:deepJoin(map2)
expect(newMap:get("apple")).to.equal(1)
expect(newMap:get("grapes")).to.equal(2)
expect(newMap:get("banana")).to.equal(10)
end)
it("Should work with None", function()
local innerMap1 = OrderedMap.new(sort.default, {
apple = 1,
grapes = 2,
banana = 10,
useless = None,
})
local innerMap2 = OrderedMap.new(sort.default, {
peach = -10,
grapes = 15,
})
local innerMap3 = OrderedMap.new(sort.default, {
apple = None,
banana = 1000,
beans = 400,
})
local innerMap4 = OrderedMap.new(sort.default, {
grapes = None,
peach = 30,
})
local outerMap1 = OrderedMap.new(sort.default, {
inner1 = innerMap1,
inner2 = innerMap2,
irrelevantInteger = 5,
})
local outerMap2 = OrderedMap.new(sort.default, {
inner1 = innerMap3,
inner2 = innerMap4,
irrelevantInteger = None,
})
local newMap = outerMap1:deepJoin(outerMap2)
expect(newMap:size()).to.equal(2)
expect(newMap:get("irrelevantInteger")).to.never.be.ok()
local mergedInner1 = newMap:get("inner1")
local mergedInner2 = newMap:get("inner2")
expect(mergedInner1:size()).to.equal(3)
expect(mergedInner1:get("apple")).to.never.be.ok()
expect(mergedInner2:size()).to.equal(1)
expect(mergedInner2:get("grapes")).to.never.be.ok()
end)
end)
end
@@ -0,0 +1,278 @@
local Root = script.Parent.Parent
local OrderedMap = require(Root.OrderedMap.OrderedMap)
local UnorderedSet = require(Root.UnorderedSet.UnorderedSet)
local sort = require(Root.sort.sort)
local OrderedSet = {}
OrderedSet.__index = OrderedSet
--[[
Create a new OrderedSet from a sortPredicate and any number of keys
sortPredicate should be a function with the following signature:
sortPredicate(key1, key2) -> bool
returning true if key1 < key2 in the sorting invariant.
]]
function OrderedSet.new(sortPredicate, ...)
sortPredicate = sortPredicate or sort.default
local keyMap = OrderedSet._unpackedToTrueMap(...)
local self = {
internalMap = OrderedMap.new(sortPredicate, keyMap),
_immutableDataStructureType = OrderedSet,
}
setmetatable(self, OrderedSet)
return self
end
--[[
Returns a new UnorderedSet with the same entries.
]]
function OrderedSet:toUnorderedSet()
local newInternalMap = self.internalMap:toUnorderedMap()
return UnorderedSet._newCannibalizeMap(newInternalMap)
end
--[[
Internal method that absorbs an existing OrderedMap as the underlying data structure for a
new OrderedSet.
]]
function OrderedSet._newCannibalizeMap(orderedMap)
local self = {
internalMap = orderedMap,
_immutableDataStructureType = OrderedSet,
}
setmetatable(self, OrderedSet)
return self
end
--[[
Internal method that takes an unpacked list of values (...) and transforms it into a table
in which each value is a key with value true.
]]
function OrderedSet._unpackedToTrueMap(...)
local keyMap = {}
local len = select("#", ...)
for i = 1, len do
keyMap[select(i, ...)] = true
end
return keyMap
end
--[[
Returns if a value is an OrderedSet.
]]
function OrderedSet.is(value)
if type(value) ~= "table" then
return false
end
return value._immutableDataStructureType == OrderedSet
end
--[[
Returns true if a key is in the set, false otherwise.
]]
function OrderedSet:find(key)
return self.internalMap:get(key) ~= nil
end
--[[
Returns a new OrderedSet, inserting a number of values into the set.
]]
function OrderedSet:insert(...)
local keyMap = OrderedSet._unpackedToTrueMap(...)
local newMap = self.internalMap:batchSet(keyMap)
return OrderedSet._newCannibalizeMap(newMap)
end
--[[
Gets the index-th key in the OrderedSet according to the sorting invariant.
]]
function OrderedSet:getByIndex(index)
return (self.internalMap:getByIndex(index))
end
--[[
Returns a copy of the table of all of the keys in the map.
]]
function OrderedSet:getKeys()
return self.internalMap:getKeys()
end
--[[
Return the number of keys in the set.
]]
function OrderedSet:size()
return self.internalMap:size()
end
--[[
Returns a new OrderedSet, removing a number of values into the set.
]]
function OrderedSet:remove(...)
local newMap = self.internalMap:remove(...)
return OrderedSet._newCannibalizeMap(newMap)
end
--[[
Returns a (shallow) copy of the OrderedSet.
]]
function OrderedSet:copy()
local newMap = self.internalMap:copy()
return OrderedSet._newCannibalizeMap(newMap)
end
--[[
Returns the first key in the OrderedSet.
]]
function OrderedSet:first()
return (self.internalMap:first())
end
--[[
Returns the last key in the OrderedSet.
]]
function OrderedSet:last()
return (self.internalMap:last())
end
--[[
Create a new OrderedSet, applying the given predicate to each element in
this OrderedSet.
Predicate should have the signature
predicate(key) -> newKey
Analogous to 'map' on a list.
]]
function OrderedSet:map(predicate)
local alteredPredicate = function(_, key)
return nil, predicate(key)
end
local newMap = self.internalMap:map(alteredPredicate)
return OrderedSet._newCannibalizeMap(newMap)
end
--[[
Create a new OrderedSet, where each key is included iff callback(key) is truthy.
callback should be a function of signature
callback(key) -> bool
]]
function OrderedSet:filter(callback)
local alteredCallback = function(_, key)
return callback(key)
end
local newMap = self.internalMap:filter(alteredCallback)
return OrderedSet._newCannibalizeMap(newMap)
end
--[[
Union any number of OrderedSets. The sorting comparator of the leftmost argument/self is
used for the returned OrderedSet.
]]
function OrderedSet:union(...)
local internalMaps = {}
local len = select("#", ...)
for i = 1, len do
internalMaps[i] = select(i, ...).internalMap
end
local newMap = self.internalMap:join(unpack(internalMaps))
return OrderedSet._newCannibalizeMap(newMap)
end
--[[
Intersect any number of OrderedSets. The sorting comparator of the leftmost argument/self is
used for the returned OrderedSet.
]]
function OrderedSet:intersection(...)
local len = select("#", ...)
local args = { ... }
local filterer = function(key)
for i = 1, len do
local set = args[i]
if not set:find(key) then
return false
end
end
return true
end
return self:filter(filterer)
end
--[[
Find the difference between any number OrderedSets. The sorting comparator of the leftmost argument/self is
used for the returned OrderedSet.
The behavior of A:difference(B, C, D) is equivalent to the behavior of
A:difference(B:union(C, D)).
]]
function OrderedSet:difference(...)
local newSet = OrderedSet.new(self.sortPredicate)
local len = select("#", ...)
for _, key in self:iterator() do
local shouldRemain = true
for i = 1, len do
if select(i, ...):find(key) then
shouldRemain = false
break
end
end
if shouldRemain then
newSet.internalMap:_insertPairInPlaceUnsorted(key, true)
end
end
newSet.internalMap:_sortInPlace()
return newSet
end
--[[
Returns an iterator for the OrderedSet.
Example:
for index, key in orderedSet:iterator() do
]]
function OrderedSet:iterator()
local i = 0
local length = self.internalMap:size()
-- Iterator function
return function()
i = i + 1
if i <= length then
local key = self:getByIndex(i)
return i, key
end
end
end
--[[
Returns an iterator for the OrderedSet that traverses in the reverse direction.
Example:
for index, key in orderedSet:reverseIterator() do
]]
function OrderedSet:reverseIterator()
local i = self.internalMap:size() + 1
-- Iterator function
return function()
i = i - 1
if i > 0 then
local key = self:getByIndex(i)
return i, key
end
end
end
OrderedSet.join = OrderedSet.union
--[[
Specify the behavior of deepJoin for OrderedSet.
]]
OrderedSet.deepJoin = OrderedSet.union
return OrderedSet
@@ -0,0 +1,359 @@
return function()
local Root = script.Parent.Parent
local OrderedSet = require(script.Parent.OrderedSet)
local UnorderedSet = require(Root.UnorderedSet.UnorderedSet)
local sort = require(Root.sort.sort)
describe("Basic ordered map operations", function()
it("Creates a new empty set", function()
local set = OrderedSet.new()
expect(set).to.be.ok()
expect(set:size()).to.equal(0)
end)
it("Can tell apart OrderedSets from non-OrderedSets", function()
local nonset = {}
expect(OrderedSet.is(nonset)).to.equal(false)
local number = 5
expect(OrderedSet.is(number)).to.equal(false)
local set = OrderedSet.new()
expect(OrderedSet.is(set)).to.equal(true)
end)
it("Creates an ordered set with some values", function()
local set = OrderedSet.new(sort.default, 3, 2, 1, 3, 4)
expect(set).to.be.ok()
expect(set:size()).to.equal(4)
expect(set:find(1)).to.equal(true)
expect(set:find(2)).to.equal(true)
expect(set:find(3)).to.equal(true)
expect(set:find(4)).to.equal(true)
-- Good sorting by key
expect(set:first()).to.equal(1)
expect(set:getByIndex(1)).to.equal(1)
expect(set:getByIndex(2)).to.equal(2)
expect(set:getByIndex(3)).to.equal(3)
expect(set:getByIndex(4)).to.equal(4)
expect(set:last()).to.equal(4)
end)
it("Returns nil on a miss", function()
local set = OrderedSet.new(sort.default, 3, 2, 1, 3, 4)
expect(set:find(10000)).to.equal(false)
end)
it("Creates a table with a reverse sort invariant", function()
local set = OrderedSet.new(sort.reverse, 3, 2, 1, 3, 4)
-- Good sorting by key
expect(set:first()).to.equal(4)
expect(set:getByIndex(4)).to.equal(1)
expect(set:getByIndex(3)).to.equal(2)
expect(set:getByIndex(2)).to.equal(3)
expect(set:getByIndex(1)).to.equal(4)
expect(set:last()).to.equal(1)
end)
it("Can create a copy", function()
local set = OrderedSet.new(sort.default, 3, 2, 1, 3, 4)
local setCopy = set:copy()
expect(setCopy).never.to.equal(set)
expect(setCopy).to.be.ok()
expect(setCopy:size()).to.equal(4)
expect(setCopy:find(1)).to.equal(true)
expect(setCopy:find(2)).to.equal(true)
expect(setCopy:find(3)).to.equal(true)
expect(setCopy:find(4)).to.equal(true)
-- Good sorting by key
expect(setCopy:first()).to.equal(1)
expect(setCopy:getByIndex(1)).to.equal(1)
expect(setCopy:getByIndex(2)).to.equal(2)
expect(setCopy:getByIndex(3)).to.equal(3)
expect(setCopy:getByIndex(4)).to.equal(4)
expect(setCopy:last()).to.equal(4)
end)
it("Can be converted immutably to a list of keys", function()
local set = OrderedSet.new(sort.default, 10, 11, 11, 10, 15)
local keys = set:getKeys()
expect(keys:get(1)).to.equal(10)
expect(keys:get(2)).to.equal(11)
expect(keys:get(3)).to.equal(15)
end)
it("Supports immutable insert", function()
local set = OrderedSet.new(sort.default, 10, 11, 11, 10, 15)
local newSet = set:insert(12)
expect(newSet).never.to.equal(set)
expect(newSet:find(12)).to.equal(true)
expect(newSet:size()).to.equal(4)
expect(set:size()).to.equal(3)
expect(set:find(12)).to.equal(false)
local newNewSet = newSet:insert(100, -100, 25, 10, 100)
expect(newNewSet:size()).to.equal(7)
end)
it("Supports union", function()
local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1)
local set2 = OrderedSet.new(sort.default, 3, 4, 5, 6)
local set3 = OrderedSet.new(sort.default, 3, 6, 100)
local newSet = set1:union(set2, set3)
expect(newSet).never.to.equal(set1)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(5)).to.equal(true)
expect(newSet:find(6)).to.equal(true)
expect(newSet:find(100)).to.equal(true)
expect(set1:size()).to.equal(4)
expect(newSet:size()).to.equal(7)
local emptySet = OrderedSet.new()
local newNewSet = newSet:union(emptySet)
expect(newNewSet:size()).to.equal(7)
end)
it("Supports deletion", function()
local set = OrderedSet.new(sort.default, 3, 2, 1, 4, 5, 6)
local newSet = set:remove(2, 4)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(false)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(false)
expect(newSet:find(5)).to.equal(true)
expect(newSet:find(6)).to.equal(true)
expect(newSet:size()).to.equal(4)
end)
it("Supports intersection", function()
local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1)
local set2 = OrderedSet.new(sort.default, 3, 4, 5, 6, 1)
local set3 = OrderedSet.new(sort.default, 3, 6, 100, 1)
local newSet = set1:intersection(set2, set3)
expect(newSet).never.to.equal(set1)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(false)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(false)
expect(newSet:find(5)).to.equal(false)
expect(newSet:find(6)).to.equal(false)
expect(newSet:find(100)).to.equal(false)
expect(set1:size()).to.equal(4)
expect(newSet:size()).to.equal(2)
local emptySet = OrderedSet.new()
local newNewSet = newSet:intersection(emptySet)
expect(newNewSet:size()).to.equal(0)
end)
it("Supports set difference", function()
local set1 = OrderedSet.new(sort.default, 10, 4, 3, 2, 1)
local set2 = OrderedSet.new(sort.default, 3, 4, 5)
local set3 = OrderedSet.new(sort.default, 100, 101)
local newSet = set1:difference(set2, set3)
expect(newSet).never.to.equal(set1)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(10)).to.equal(true)
expect(newSet:find(3)).to.equal(false)
expect(newSet:find(4)).to.equal(false)
expect(newSet:find(5)).to.equal(false)
expect(newSet:find(100)).to.equal(false)
expect(newSet:find(100)).to.equal(false)
expect(newSet:find(101)).to.equal(false)
expect(set1:size()).to.equal(5)
expect(newSet:size()).to.equal(3)
local emptySet = OrderedSet.new()
local newNewSet = set1:difference(emptySet)
expect(newNewSet:size()).to.equal(5)
end)
end)
describe("More advanced functionality", function()
describe("Mapping", function()
it("should use the callback", function()
local set = OrderedSet.new(sort.default, 4, 3, 2, 1)
local newSet = set:map(function(key)
return key * 2
end)
expect(newSet:find(1)).to.equal(false)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(6)).to.equal(true)
expect(newSet:find(8)).to.equal(true)
expect(newSet:size()).to.equal(4)
end)
it("should maintain the sorting invariant", function()
local set = OrderedSet.new(sort.default, 3, 2, 1, 4)
local newSet = set:map(function(key)
return key % 3
end)
expect(newSet:find(0)).to.equal(true)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(true)
expect(newSet:getByIndex(1)).to.equal(0)
expect(newSet:getByIndex(2)).to.equal(1)
expect(newSet:getByIndex(3)).to.equal(2)
expect(newSet:size()).to.equal(3)
end)
it("should work with an empty OrderedSet", function()
local called = false
local function callback()
called = true
return "placeholderkey"
end
local set = OrderedSet.new()
local newSet = set:map(callback)
expect(newSet:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("Filtering", function()
it("should use the callback", function()
local set = OrderedSet.new(sort.default, 4, 3, 2, 1)
local newSet = set:filter(function(key)
return key >= 3
end)
expect(newSet).never.to.equal(set)
expect(newSet:size()).to.equal(2)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(1)).to.equal(false)
expect(newSet:find(2)).to.equal(false)
end)
it("should maintain the sorting invariant", function()
local set = OrderedSet.new(sort.default, 4, 3, 2, 1, 6, 5, 100)
local newSet = set:filter(function(key)
return key >= 3
end)
expect(newSet).never.to.equal(set)
expect(newSet:size()).to.equal(5)
expect(newSet:getByIndex(1)).to.equal(3)
expect(newSet:getByIndex(2)).to.equal(4)
expect(newSet:last()).to.equal(100)
end)
it("should work with an empty OrderedSet", function()
local called = false
local function callback()
called = true
return true
end
local set = OrderedSet.new()
local newSet = set:filter(callback)
expect(newSet:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("Iterators", function()
it("Should work in a for loop", function()
local set = OrderedSet.new(sort.default, 8, 3, 2, 1, 100)
local count = 1
for index, key in set:iterator() do
if count == 1 then
expect(index).to.equal(1)
expect(key).to.equal(1)
elseif count == 2 then
expect(index).to.equal(2)
expect(key).to.equal(2)
elseif count == 3 then
expect(index).to.equal(3)
expect(key).to.equal(3)
elseif count == 4 then
expect(index).to.equal(4)
expect(key).to.equal(8)
elseif count == 5 then
expect(index).to.equal(5)
expect(key).to.equal(100)
end
count = count + 1
end
expect(count - 1).to.equal(5)
end)
it("Should work for empty sets", function()
local set = OrderedSet.new()
local doAnything = false
for _, _ in set:iterator() do
doAnything = true
end
expect(doAnything).to.equal(false)
end)
it("Should work in reverse", function()
local set = OrderedSet.new(sort.default, 8, 3, 2, 1, 100)
local count = 1
for index, key in set:reverseIterator() do
if count == 5 then
expect(index).to.equal(1)
expect(key).to.equal(1)
elseif count == 4 then
expect(index).to.equal(2)
expect(key).to.equal(2)
elseif count == 3 then
expect(index).to.equal(3)
expect(key).to.equal(3)
elseif count == 2 then
expect(index).to.equal(4)
expect(key).to.equal(8)
elseif count == 1 then
expect(index).to.equal(5)
expect(key).to.equal(100)
end
count = count + 1
end
expect(count - 1).to.equal(5)
end)
end)
describe("Downcasting", function()
it("Should successfully downcast to an UnorderedSet", function()
local ordered = OrderedSet.new(sort.default, 5, 4, 3, 2, 1)
local unordered = ordered:toUnorderedSet()
expect(UnorderedSet.is(unordered)).to.equal(true)
for i = 5, 1, -1 do
expect(unordered:find(i)).to.equal(true)
end
expect(unordered:size()).to.equal(5)
end)
end)
end)
end
@@ -0,0 +1,303 @@
local Root = script.Parent.Parent
local Cryo = require(Root.Parent.Cryo)
local List = require(Root.List.List)
local None = require(Root.None)
local deepJoin = require(Root.deepJoin.deepJoin)
local UnorderedMap = {}
UnorderedMap.__index = UnorderedMap
--[[
Create a new UnorderedMap from any number of dictionaries of values of the form
{ [key] = value }
]]
function UnorderedMap.new(...)
local self = {
pairs = {},
length = 0,
_immutableDataStructureType = UnorderedMap
}
setmetatable(self, UnorderedMap)
UnorderedMap._insertInPlace(self, ...)
return self
end
--[[
Internal method that absorbs an existing table and constructs a new UnorderdMap
from it as the underlying data.
]]
function UnorderedMap._newCannibalizeTable(tab)
local count = 0
for _ in pairs(tab) do
count = count + 1
end
local self = {
pairs = tab,
length = count,
_immutableDataStructureType = UnorderedMap
}
setmetatable(self, UnorderedMap)
return self
end
--[[
Returns if a value is an UnorderedMap.
]]
function UnorderedMap.is(value)
if type(value) ~= "table" then
return false
end
return value._immutableDataStructureType == UnorderedMap
end
--[[
Returns the value at key.
]]
function UnorderedMap:get(key)
return self.pairs[key]
end
--[[
Returns a new UnorderedMap, setting the value at key to be value.
]]
function UnorderedMap:set(key, value)
if self:get(key) == nil then
self.length = self.length + 1
end
local new = self:copy()
new.pairs[key] = value
return new
end
--[[
Returns a List of all of the values in the map.
]]
function UnorderedMap:getValues()
return List._newCannibalizeTable(Cryo.Dictionary.values(self.pairs))
end
--[[
Returns a List of all of the keys in the map.
]]
function UnorderedMap:getKeys()
return List._newCannibalizeTable(Cryo.Dictionary.keys(self.pairs))
end
--[[
Returns the size (number of key-value pairs) of the map.
]]
function UnorderedMap:size()
return self.length
end
--[[
Returns a new UnorderedMap with the pairs at all given keys removed.
]]
function UnorderedMap:remove(...)
local new = self:copy()
local newPairs = new.pairs
local len = select("#", ...)
for i = 1, len do
local key = select(i, ...)
if new:get(key) ~= nil then
new.length = new.length - 1
newPairs[key] = nil
end
end
return new
end
--[[
Joins any number of (basic table) dictionaries of values of the form
{ [key] = value }
into the OrderedMap, creating a new OrderedMap.
]]
function UnorderedMap:batchSet(...)
local new = self:copyRemoveNone()
new:_insertInPlaceRemoveNone(...)
return new
end
--[[
Returns a (shallow) copy of the UnorderedMap.
]]
function UnorderedMap:copy()
return UnorderedMap.new(self.pairs)
end
--[[
Returns a (shallow) copy of the UnorderedMap, removing all None instances.
]]
function UnorderedMap:copyRemoveNone()
local new = {}
for key, value in self:iterator() do
if value ~= None then
new[key] = value
end
end
return UnorderedMap._newCannibalizeTable(new)
end
--[[
Create a new UnorderedMap, applying the given predicate to each element in
this UnorderedMap.
Predicate should have the signature
predicate(value, key) -> newValue, newKey
Analogous to 'map' on a list.
]]
function UnorderedMap:map(predicate)
local new = UnorderedMap.new()
for key, value in self:iterator() do
local newValue, newKey = predicate(value, key)
newKey = newKey or key
newValue = newValue or value
new:_insertPairInPlace(newKey, newValue)
end
return new
end
--[[
Create a new UnorderedMap, where each key-value pair is included iff callback(value, key) is truthy.
callback should be a function of signature
callback(value, key) -> bool
]]
function UnorderedMap:filter(callback)
local new = UnorderedMap.new()
for key, value in self:iterator() do
if callback(value, key) then
new:_insertPairInPlace(key, value)
end
end
return new
end
--[[
Join any number of UnorderedMaps.
]]
function UnorderedMap:join(...)
local new = self:copyRemoveNone()
for i = 1, select("#", ...) do
local other = select(i, ...)
if other:size() > 0 then
for key, value in other:iterator() do
new:_insertPairInPlaceRemoveNone(key, value)
end
end
end
return new
end
--[[
Internal method for inserting values in place.
]]
function UnorderedMap:_insertInPlace(...)
local len = select("#", ...)
for i = 1, len do
local pair = select(i, ...)
for key, value in pairs(pair) do
self:_insertPairInPlace(key, value)
end
end
end
--[[
Internal method for inserting a single pair in place.
]]
function UnorderedMap:_insertPairInPlace(key, value)
if self:get(key) == nil then
self.length = self.length + 1
end
self.pairs[key] = value
end
--[[
Internal method for inserting values in place, removing all None instances.
]]
function UnorderedMap:_insertInPlaceRemoveNone(...)
local len = select("#", ...)
for i = 1, len do
local pair = select(i, ...)
for key, value in pairs(pair) do
self:_insertPairInPlaceRemoveNone(key, value)
end
end
end
--[[
Internal method for inserting values in place, removing all None instances.
]]
function UnorderedMap:_insertPairInPlaceRemoveNone(key, value)
if value == None then
if self:get(key) ~= nil then
self.pairs[key] = nil
self.length = self.length - 1
end
else
if self:get(key) == nil then
self.length = self.length + 1
end
self.pairs[key] = value
end
end
--[[
Specify the behavior of deepJoin for UnorderedMap.
]]
function UnorderedMap:deepJoin(rhs)
local newMap = UnorderedMap.new()
for lhsKey, lhsValue in self:iterator() do
local rhsValue = rhs:get(lhsKey)
if rhsValue then
if type(rhsValue) == "table" and type(lhsValue) == "table" then
newMap:_insertPairInPlace(lhsKey, deepJoin(lhsValue, rhsValue))
else
if rhsValue ~= None then
newMap:_insertPairInPlace(lhsKey, rhsValue)
end
end
else
if lhsValue ~= None then
newMap:_insertPairInPlace(lhsKey, lhsValue)
end
end
end
-- Copy over rhs keys that aren't in lhs
for rhsKey, rhsValue in rhs:iterator() do
local lhsValue = self:get(rhsKey)
if not lhsValue and rhsValue ~= None then
newMap:_insertPairInPlace(rhsKey, rhsValue)
end
end
return newMap
end
--[[
Returns an iterator for the UnorderedMap.
Key-value pairs are returned in an undefined order.
Example:
for key, value in unorderedMap:iterator() do
]]
function UnorderedMap:iterator()
return next, self.pairs, nil
end
return UnorderedMap
@@ -0,0 +1,476 @@
return function()
local Root = script.Parent.Parent
local UnorderedMap = require(script.Parent.UnorderedMap)
local None = require(Root.None)
describe("Basic unordered map operations", function()
it("Creates a new empty table", function()
local map = UnorderedMap.new()
expect(map).to.be.ok()
expect(map:size()).to.equal(0)
end)
it("Can tell apart UnorderedMaps from non-UnorderedMaps", function()
local nonmap = {}
expect(UnorderedMap.is(nonmap)).to.equal(false)
local number = 5
expect(UnorderedMap.is(number)).to.equal(false)
local map = UnorderedMap.new()
expect(UnorderedMap.is(map)).to.equal(true)
end)
it("Creates an ordered with some values", function()
local map = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
expect(map).to.be.ok()
expect(map:size()).to.equal(3)
expect(map:get("apple")).to.equal(1)
expect(map:get("grapes")).to.equal(2)
expect(map:get("banana")).to.equal(10)
end)
it("Returns nil on a miss", function()
local map = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
expect(map:get("broccoli")).never.to.be.ok()
end)
it("Creates a table with some values from multiple tables", function()
local map = UnorderedMap.new({
apple = 1,
grapes = 10,
banana = 2,
}, {
orange = 5,
banana = 5,
})
expect(map).to.be.ok()
expect(map:size()).to.equal(4)
expect(map:get("orange")).to.equal(5)
expect(map:get("grapes")).to.equal(10)
expect(map:get("banana")).to.equal(5)
end)
it("Can create a copy", function()
local map = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local mapCopy = map:copy()
expect(mapCopy).never.to.equal(map)
expect(mapCopy).to.be.ok()
expect(mapCopy:size()).to.equal(3)
expect(mapCopy:get("apple")).to.equal(1)
expect(mapCopy:get("grapes")).to.equal(2)
expect(mapCopy:get("banana")).to.equal(10)
end)
it("Can be converted immutably to lists of keys and values", function()
local map = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local keys = map:getKeys()
local values = map:getValues()
expect(keys:find("apple")).to.be.ok()
expect(keys:find("banana")).to.be.ok()
expect(keys:find("grapes")).to.be.ok()
expect(keys:size()).to.equal(3)
expect(values:find(1)).to.be.ok()
expect(values:find(2)).to.be.ok()
expect(values:find(10)).to.be.ok()
expect(values:size()).to.equal(3)
end)
it("Supports immutable set", function()
local map = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local newMap = map:set("apple", 1000)
expect(newMap).never.to.equal(map)
expect(map:get("apple")).to.equal(1)
expect(newMap:get("apple")).to.equal(1000)
local newNewMap = newMap:set("lettuce", -100)
expect(newNewMap:get("lettuce")).to.equal(-100)
end)
it("Supports batch set", function()
local map = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local newMap = map:batchSet({
apple = 100,
watermelon = 4,
}, {
watermelon = 100,
kiwi = 3,
})
expect(newMap).never.to.equal(map)
expect(map:get("apple")).to.equal(1)
expect(newMap:get("apple")).to.equal(100)
expect(newMap:get("watermelon")).to.equal(100)
expect(newMap:get("kiwi")).to.equal(3)
expect(map:get("kiwi")).to.never.be.ok()
expect(newMap:size()).to.equal(5)
end)
it("Supports join", function()
local map1 = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local map2 = UnorderedMap.new({
apple = 100,
watermelon = 4,
})
local map3 = UnorderedMap.new({
watermelon = 100,
kiwi = 3,
})
local newMap = map1:join(map2, map3)
expect(newMap).never.to.equal(map1)
expect(map1:get("apple")).to.equal(1)
expect(newMap:get("apple")).to.equal(100)
expect(newMap:get("watermelon")).to.equal(100)
expect(newMap:get("kiwi")).to.equal(3)
expect(map1:get("kiwi")).to.never.be.ok()
expect(newMap:size()).to.equal(5)
end)
it("Supports None", function()
local map1 = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
useless = None,
})
expect(map1:size()).to.equal(4)
local map2 = UnorderedMap.new({
apple = None,
watermelon = 4,
})
local map3 = UnorderedMap.new({
watermelon = 100,
kiwi = 3,
})
local newMap = map1:join(map2, map3)
expect(newMap).never.to.equal(map1)
expect(newMap:get("apple")).to.never.be.ok()
expect(newMap:get("watermelon")).to.equal(100)
expect(newMap:get("kiwi")).to.equal(3)
expect(newMap:get("useless")).to.never.be.ok()
expect(newMap:size()).to.equal(4)
newMap = map1:batchSet({
apple = None,
watermelon = 4,
}, {
watermelon = 100,
kiwi = 3,
})
expect(newMap).never.to.equal(map1)
expect(newMap:get("apple")).to.never.be.ok()
expect(newMap:get("watermelon")).to.equal(100)
expect(newMap:get("kiwi")).to.equal(3)
expect(newMap:get("useless")).to.never.be.ok()
expect(newMap:size()).to.equal(4)
end)
it("Supports deletion", function()
local map = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local newMap = map:remove("apple", "grapes")
expect(newMap:get("apple")).to.never.be.ok()
expect(newMap:get("grapes")).to.never.be.ok()
expect(newMap:get("banana")).to.equal(10)
expect(map:get("apple")).to.be.ok()
expect(map:get("grapes")).to.be.ok()
expect(map:get("banana")).to.be.ok()
expect(newMap:size()).to.equal(1)
end)
end)
describe("More advanced functionality", function()
describe("Mapping", function()
it("should use the callback", function()
local map = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local newMap = map:map(function(value, key)
return value * 2, key .. " fruit"
end)
expect(newMap).never.to.equal(map)
expect(newMap:size()).to.equal(3)
expect(map:get("apple fruit")).to.never.be.ok()
expect(newMap:get("apple fruit")).to.equal(2)
expect(newMap:get("apple")).to.never.be.ok()
expect(newMap:get("banana fruit")).to.equal(20)
end)
it("should work with an empty UnorderedMap", function()
local called = false
local function callback()
called = true
return "placeholderkey", "placeholdervalue"
end
local map = UnorderedMap.new()
local newMap = map:map(callback)
expect(newMap:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("Filtering", function()
it("should use the callback", function()
local map = UnorderedMap.new({
orange = 100,
apple = 1,
grapes = 2,
banana = 10,
lemons = 4,
})
local newMap = map:filter(function(value, key)
return value < 9 or key == "orange"
end)
expect(newMap).never.to.equal(map)
expect(newMap:size()).to.equal(4)
expect(newMap:get("apple")).to.equal(1)
expect(newMap:get("banana")).to.never.be.ok()
expect(map:get("banana")).to.equal(10)
expect(newMap:get("orange")).to.equal(100)
end)
it("should work with an empty OrderedMap", function()
local called = false
local function callback()
called = true
return true
end
local map = UnorderedMap.new()
local newMap = map:filter(callback)
expect(newMap:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("Iterators", function()
it("Should work in a for loop", function()
local map = UnorderedMap.new({
orange = 100,
apple = 1,
grapes = 2,
banana = 10,
lemons = 4,
})
local count = 0
for key, value in map:iterator() do
if key == "orange" then
expect(value).to.equal(100)
elseif key == "apple" then
expect(value).to.equal(1)
elseif key == "grapes" then
expect(value).to.equal(2)
elseif key == "banana" then
expect(value).to.equal(10)
elseif key == "lemons" then
expect(value).to.equal(4)
else
error("There should not be such a key")
end
count = count + 1
end
expect(count).to.equal(5)
end)
it("Should work for empty maps", function()
local map = UnorderedMap.new()
local doAnything = false
for _, _ in map:iterator() do
doAnything = true
end
expect(doAnything).to.equal(false)
end)
end)
end)
describe("deepJoin", function()
it("Should work with basic maps", function()
local map1 = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local map2 = UnorderedMap.new({
apple = 100,
watermelon = 4,
})
local newMap = map1:deepJoin(map2)
expect(newMap:get("apple")).to.equal(100)
expect(newMap:get("grapes")).to.equal(2)
expect(newMap:get("banana")).to.equal(10)
expect(newMap:get("watermelon")).to.equal(4)
expect(newMap:size()).to.equal(4)
end)
it("Should work with nested maps", function()
local innerMap1 = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local innerMap2 = UnorderedMap.new({
peach = -10,
grapes = 15
})
local innerMap3 = UnorderedMap.new({
apple = 100,
banana = 1000,
beans = 400,
})
local innerMap4 = UnorderedMap.new({
peach = 30,
})
local outerMap1 = UnorderedMap.new({
inner1 = innerMap1,
inner2 = innerMap2,
irrelevantInteger = 5,
})
local outerMap2 = UnorderedMap.new({
inner1 = innerMap3,
inner2 = innerMap4,
irrelevantString = "hello!",
})
local newMap = outerMap1:deepJoin(outerMap2)
expect(newMap:size()).to.equal(4)
expect(newMap:get("irrelevantInteger")).to.equal(5)
expect(newMap:get("irrelevantString")).to.equal("hello!")
local mergedInner1 = newMap:get("inner1")
local mergedInner2 = newMap:get("inner2")
expect(mergedInner1:size()).to.equal(4)
expect(mergedInner1:get("apple")).to.equal(100)
expect(mergedInner1:get("grapes")).to.equal(2)
expect(mergedInner1:get("banana")).to.equal(1000)
expect(mergedInner1:get("beans")).to.equal(400)
expect(mergedInner2:size()).to.equal(2)
expect(mergedInner2:get("peach")).to.equal(30)
expect(mergedInner2:get("grapes")).to.equal(15)
end)
it("Should work with an empty map", function()
local map1 = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
})
local map2 = UnorderedMap.new()
local newMap = map1:deepJoin(map2)
expect(newMap:get("apple")).to.equal(1)
expect(newMap:get("grapes")).to.equal(2)
expect(newMap:get("banana")).to.equal(10)
end)
it("Should work with None", function()
local innerMap1 = UnorderedMap.new({
apple = 1,
grapes = 2,
banana = 10,
useless = None,
})
local innerMap2 = UnorderedMap.new({
peach = -10,
grapes = 15,
})
local innerMap3 = UnorderedMap.new({
apple = None,
banana = 1000,
beans = 400,
})
local innerMap4 = UnorderedMap.new({
grapes = None,
peach = 30,
})
local outerMap1 = UnorderedMap.new({
inner1 = innerMap1,
inner2 = innerMap2,
irrelevantInteger = 5,
})
local outerMap2 = UnorderedMap.new({
inner1 = innerMap3,
inner2 = innerMap4,
irrelevantInteger = None,
})
local newMap = outerMap1:deepJoin(outerMap2)
expect(newMap:size()).to.equal(2)
expect(newMap:get("irrelevantInteger")).to.never.be.ok()
local mergedInner1 = newMap:get("inner1")
local mergedInner2 = newMap:get("inner2")
expect(mergedInner1:size()).to.equal(3)
expect(mergedInner1:get("apple")).to.never.be.ok()
expect(mergedInner2:size()).to.equal(1)
expect(mergedInner2:get("grapes")).to.never.be.ok()
end)
end)
end
@@ -0,0 +1,216 @@
local Root = script.Parent.Parent
local UnorderedMap = require(Root.UnorderedMap.UnorderedMap)
local UnorderedSet = {}
UnorderedSet.__index = UnorderedSet
--[[
Create a new UnorderedSet from any number of keys
]]
function UnorderedSet.new(...)
local keyMap = UnorderedSet._unpackedToTrueMap(...)
local self = {
internalMap = UnorderedMap.new(keyMap),
_immutableDataStructureType = UnorderedSet,
}
setmetatable(self, UnorderedSet)
return self
end
--[[
Internal method that absorbs an existing Unorderedmap as the underlying data structure for a
new UnorderedSet.
]]
function UnorderedSet._newCannibalizeMap(unorderedMap)
local self = {
internalMap = unorderedMap,
_immutableDataStructureType = UnorderedSet,
}
setmetatable(self, UnorderedSet)
return self
end
--[[
Internal method that takes an unpacked list of values (...) and transforms it into a table
in which each value is a key with value true.
]]
function UnorderedSet._unpackedToTrueMap(...)
local keyMap = {}
local len = select("#", ...)
for i = 1, len do
keyMap[select(i, ...)] = true
end
return keyMap
end
--[[
Returns if a value is an UnorderedSet.
]]
function UnorderedSet.is(value)
if type(value) ~= "table" then
return false
end
return value._immutableDataStructureType == UnorderedSet
end
--[[
Returns true if a key is in the set, false otherwise.
]]
function UnorderedSet:find(id)
return self.internalMap:get(id) ~= nil
end
--[[
Returns a new UnorderedSet, inserting a number of values into the set.
]]
function UnorderedSet:insert(...)
local keyMap = UnorderedSet._unpackedToTrueMap(...)
local newMap = self.internalMap:batchSet(keyMap)
return UnorderedSet._newCannibalizeMap(newMap)
end
--[[
Returns a copy of the table of all of the keys in the map.
]]
function UnorderedSet:getKeys()
return self.internalMap:getKeys()
end
--[[
Return the number of keys in the set.
]]
function UnorderedSet:size()
return self.internalMap:size()
end
--[[
Returns a new UnorderedSet, removing a number of values into the set.
]]
function UnorderedSet:remove(...)
local newMap = self.internalMap:remove(...)
return UnorderedSet._newCannibalizeMap(newMap)
end
--[[
Returns a (shallow) copy of the UnorderedSet.
]]
function UnorderedSet:copy()
local newMap = self.internalMap:copy()
return UnorderedSet._newCannibalizeMap(newMap)
end
--[[
Create a new UnorderedSet, applying the given predicate to each element in
this OrderedSet.
Predicate should have the signature
predicate(key) -> newKey
Analogous to 'map' on a list.
]]
function UnorderedSet:map(predicate)
local alteredPredicate = function(_, key)
return nil, predicate(key)
end
local newMap = self.internalMap:map(alteredPredicate)
return UnorderedSet._newCannibalizeMap(newMap)
end
--[[
Create a new OrderedSet, where each key is included iff callback(key) is truthy.
callback should be a function of signature
callback(key) -> bool
]]
function UnorderedSet:filter(callback)
local alteredCallback = function(_, key)
return callback(key)
end
local newMap = self.internalMap:filter(alteredCallback)
return UnorderedSet._newCannibalizeMap(newMap)
end
--[[
Union any number of UnorderedSets.
]]
function UnorderedSet:union(...)
local internalMaps = {}
local len = select("#", ...)
for i = 1, len do
internalMaps[i] = select(i, ...).internalMap
end
local newMap = self.internalMap:join(unpack(internalMaps))
return UnorderedSet._newCannibalizeMap(newMap)
end
--[[
Find the difference between any number UnorderedSets.
The behavior of A:difference(B, C, D) is equivalent to the behavior of
A:difference(B:union(C, D)).
]]
function UnorderedSet:difference(...)
local newSet = UnorderedSet.new()
local len = select("#", ...)
for key in self:iterator() do
local shouldRemain = true
for i = 1, len do
if select(i, ...):find(key) then
shouldRemain = false
break
end
end
if shouldRemain then
newSet.internalMap:_insertPairInPlace(key, true)
end
end
return newSet
end
--[[
Intersect any number of UnorderedSets.
]]
function UnorderedSet:intersection(...)
local len = select("#", ...)
local args = { ... }
local filterer = function(key)
for i = 1, len do
local set = args[i]
if not set:find(key) then
return false
end
end
return true
end
return self:filter(filterer)
end
--[[
Returns an iterator for the UnorderedSet.
Keys are returned in an undefined order.
Example:
for key in orderedSet:iterator() do
]]
function UnorderedSet:iterator()
local alteredNext = function(table, key)
return (next(table, key))
end
return alteredNext, self.internalMap.pairs
end
--[[
Specify the behavior of deepJoin for UnorderedSet.
]]
UnorderedSet.deepJoin = UnorderedSet.union
UnorderedSet.join = UnorderedSet.union
return UnorderedSet
@@ -0,0 +1,255 @@
return function()
local UnorderedSet = require(script.Parent.UnorderedSet)
describe("Basic ordered map operations", function()
it("Creates a new empty set", function()
local set = UnorderedSet.new()
expect(set).to.be.ok()
expect(set:size()).to.equal(0)
end)
it("Can tell apart UnorderedSets from non-UnorderedSets", function()
local nonset = {}
expect(UnorderedSet.is(nonset)).to.equal(false)
local number = 5
expect(UnorderedSet.is(number)).to.equal(false)
local set = UnorderedSet.new()
expect(UnorderedSet.is(set)).to.equal(true)
end)
it("Creates an unordered set with some values", function()
local set = UnorderedSet.new(3, 2, 1, 3, 4)
expect(set).to.be.ok()
expect(set:size()).to.equal(4)
expect(set:find(1)).to.equal(true)
expect(set:find(2)).to.equal(true)
expect(set:find(3)).to.equal(true)
expect(set:find(4)).to.equal(true)
end)
it("Returns nil on a miss", function()
local set = UnorderedSet.new(3, 2, 1, 3, 4)
expect(set:find(10000)).to.equal(false)
end)
it("Can create a copy", function()
local set = UnorderedSet.new(3, 2, 1, 3, 4)
local setCopy = set:copy()
expect(setCopy).never.to.equal(set)
expect(setCopy).to.be.ok()
expect(setCopy:size()).to.equal(4)
expect(setCopy:find(1)).to.equal(true)
expect(setCopy:find(2)).to.equal(true)
expect(setCopy:find(3)).to.equal(true)
expect(setCopy:find(4)).to.equal(true)
end)
it("Can be converted immutably to a List of keys", function()
local set = UnorderedSet.new(10, 11, 11, 10, 15)
local keys = set:getKeys()
expect(keys:size()).to.equal(3)
end)
it("Supports immutable insert", function()
local set = UnorderedSet.new(10, 11, 11, 10, 15)
local newSet = set:insert(12)
expect(newSet).never.to.equal(set)
expect(newSet:find(12)).to.equal(true)
expect(newSet:size()).to.equal(4)
expect(set:size()).to.equal(3)
expect(set:find(12)).to.equal(false)
local newNewSet = newSet:insert(100, -100, 25, 10, 100)
expect(newNewSet:size()).to.equal(7)
end)
it("Supports union", function()
local set1 = UnorderedSet.new(4, 3, 2, 1)
local set2 = UnorderedSet.new(3, 4, 5, 6)
local set3 = UnorderedSet.new(3, 6, 100)
local newSet = set1:union(set2, set3)
expect(newSet).never.to.equal(set1)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(5)).to.equal(true)
expect(newSet:find(6)).to.equal(true)
expect(newSet:find(100)).to.equal(true)
expect(set1:size()).to.equal(4)
expect(newSet:size()).to.equal(7)
local emptySet = UnorderedSet.new()
local newNewSet = newSet:union(emptySet)
expect(newNewSet:size()).to.equal(7)
end)
it("Supports deletion", function()
local set = UnorderedSet.new(3, 2, 1, 4, 5, 6)
local newSet = set:remove(2, 4)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(false)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(false)
expect(newSet:find(5)).to.equal(true)
expect(newSet:find(6)).to.equal(true)
expect(newSet:size()).to.equal(4)
end)
it("Supports intersection", function()
local set1 = UnorderedSet.new(4, 3, 2, 1)
local set2 = UnorderedSet.new(3, 4, 5, 6, 1)
local set3 = UnorderedSet.new(3, 6, 100, 1)
local newSet = set1:intersection(set2, set3)
expect(newSet).never.to.equal(set1)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(false)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(false)
expect(newSet:find(5)).to.equal(false)
expect(newSet:find(6)).to.equal(false)
expect(newSet:find(100)).to.equal(false)
expect(set1:size()).to.equal(4)
expect(newSet:size()).to.equal(2)
local emptySet = UnorderedSet.new()
local newNewSet = newSet:intersection(emptySet)
expect(newNewSet:size()).to.equal(0)
end)
it("Supports set difference", function()
local set1 = UnorderedSet.new(10, 4, 3, 2, 1)
local set2 = UnorderedSet.new(3, 4, 5)
local set3 = UnorderedSet.new(100, 101)
local newSet = set1:difference(set2, set3)
expect(newSet).never.to.equal(set1)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(10)).to.equal(true)
expect(newSet:find(3)).to.equal(false)
expect(newSet:find(4)).to.equal(false)
expect(newSet:find(5)).to.equal(false)
expect(newSet:find(100)).to.equal(false)
expect(newSet:find(100)).to.equal(false)
expect(newSet:find(101)).to.equal(false)
expect(set1:size()).to.equal(5)
expect(newSet:size()).to.equal(3)
local emptySet = UnorderedSet.new()
local newNewSet = set1:difference(emptySet)
expect(newNewSet:size()).to.equal(5)
end)
end)
describe("More advanced functionality", function()
describe("Mapping", function()
it("should use the callback", function()
local set = UnorderedSet.new(4, 3, 2, 1)
local newSet = set:map(function(key)
return key * 2
end)
expect(newSet:find(1)).to.equal(false)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(6)).to.equal(true)
expect(newSet:find(8)).to.equal(true)
expect(newSet:size()).to.equal(4)
end)
it("should work with an empty UnorderedSet", function()
local called = false
local function callback()
called = true
return "placeholderkey"
end
local set = UnorderedSet.new()
local newSet = set:map(callback)
expect(newSet:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("Filtering", function()
it("should use the callback", function()
local set = UnorderedSet.new(4, 3, 2, 1)
local newSet = set:filter(function(key)
return key >= 3
end)
expect(newSet).never.to.equal(set)
expect(newSet:size()).to.equal(2)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(1)).to.equal(false)
expect(newSet:find(2)).to.equal(false)
end)
it("should work with an empty UnorderedSet", function()
local called = false
local function callback()
called = true
return true
end
local set = UnorderedSet.new()
local newSet = set:filter(callback)
expect(newSet:size()).to.equal(0)
expect(called).to.equal(false)
end)
end)
describe("Iterators", function()
it("Should work in a for loop", function()
local set = UnorderedSet.new(8, 3, 2, 1, 100)
local seen = {}
local count = 0
for key in set:iterator() do
seen[key] = true
count = count + 1
end
expect(count).to.equal(5)
expect(seen[8]).to.equal(true)
expect(seen[3]).to.equal(true)
expect(seen[2]).to.equal(true)
expect(seen[1]).to.equal(true)
expect(seen[100]).to.equal(true)
end)
it("Should work for empty sets", function()
local set = UnorderedSet.new()
local doAnything = false
for _ in set:iterator() do
doAnything = true
end
expect(doAnything).to.equal(false)
end)
it("Should only provide keys", function()
local set = UnorderedSet.new(1, 2)
for key, value in set:iterator() do
expect(value).to.never.be.ok()
expect(key).to.be.ok()
end
end)
end)
end)
end
@@ -0,0 +1,29 @@
--[[
Utility that returns the leftmost index of the occurence of value in a sorted list according to sortPredicate,
if provided, otherwise <. If not found, returns nil.
]]
local function binarySearch(list, value, sortPredicate)
sortPredicate = sortPredicate or function(lhs, rhs)
return lhs < rhs
end
local low = 1
local high = #list
while low <= high do
local mid = low + math.floor((high - low) / 2)
if sortPredicate(value, list[mid]) then
high = mid - 1
elseif sortPredicate(list[mid], value) then
low = mid + 1
else
-- Go as left as we can
while mid >= 1 and not (sortPredicate(list[mid], value) or sortPredicate(value, list[mid])) do
mid = mid - 1
end
return mid + 1
end
end
return nil
end
return binarySearch
@@ -0,0 +1,38 @@
return function()
local binarySearch = require(script.Parent.binarySearch)
it("Works for a sorted list", function()
local list = { 1, 3, 5, 6, 7 }
expect(binarySearch(list, 1)).to.equal(1)
expect(binarySearch(list, 3)).to.equal(2)
expect(binarySearch(list, 5)).to.equal(3)
expect(binarySearch(list, 6)).to.equal(4)
expect(binarySearch(list, 7)).to.equal(5)
expect(binarySearch(list, 100)).to.never.be.ok()
end)
it("Works for a sorted list with duplicates", function()
local list = { 1, 1, 3, 3, 3, 5, 5, 10, 11 }
expect(binarySearch(list, 1)).to.equal(1)
expect(binarySearch(list, 3)).to.equal(3)
expect(binarySearch(list, 5)).to.equal(6)
expect(binarySearch(list, 10)).to.equal(8)
expect(binarySearch(list, 11)).to.equal(9)
end)
it("Works for an empty list", function()
local list = {}
expect(binarySearch(list, 1)).to.never.be.ok()
end)
it("Works for a special comparator", function()
local list = { 7, 6, 5, 5, 3, 1 }
local reverse = function(lhs, rhs)
return rhs < lhs
end
expect(binarySearch(list, 7, reverse)).to.equal(1)
expect(binarySearch(list, 6, reverse)).to.equal(2)
expect(binarySearch(list, 5, reverse)).to.equal(3)
expect(binarySearch(list, 3, reverse)).to.equal(5)
expect(binarySearch(list, 1, reverse)).to.equal(6)
end)
end
@@ -0,0 +1,50 @@
local Root = script.Parent.Parent
local Cryo = require(Root.Parent.Cryo)
local None = require(Root.None)
local _deepJoinHelper
--[[
deepJoin deeply merges any number of immutable data structures.
It conforms to the following behavior:
1. UnorderedMaps and OrderedMaps are merged with pairs in later maps overwriting pairs in earlier maps. Values
are recursively deepJoined. None is respected. UnorderedMaps will not be joined with OrderedMaps, and vice versa.
2. UnorderedSets and OrderedSets are unioned. UnorderedSets will not be unioned with OrderedSets, and vice versa.
3. Lists are joined (i.e. appended) to one another according to List:join().
4. Joining two differently typed (i.e. OrderedSet and UnorderedSet) immutable data structures will return the
RHS immutable data structure.
5. All other values are overwritten, taking the furthest right value.
]]
local function deepJoin(...)
local values = { ... }
if #values == 0 then
return nil
end
return Cryo.List.foldLeft(values, function(accumulator, value, index)
if index == 1 then
return accumulator
end
return _deepJoinHelper(accumulator, value)
end, values[1])
end
function _deepJoinHelper(table1, table2)
if table2 == None then
return nil
end
if type(table1) ~= type(table2) or type(table1) ~= "table" then
return table2
end
if getmetatable(table1) == getmetatable(table2) and type(table1.deepJoin) == "function" then
if type(table1.deepJoin) == "function" then
return table1:deepJoin(table2)
else
warn([[deepJoining two tables with the same metatable, but not implementing deepJoin.
Overriding with rightmost table.]])
end
end
return table2
end
return deepJoin
@@ -0,0 +1,267 @@
return function()
local Root = script.Parent.Parent
local OrderedMap = require(Root.OrderedMap.OrderedMap)
local UnorderedMap = require(Root.UnorderedMap.UnorderedMap)
local OrderedSet = require(Root.OrderedSet.OrderedSet)
local UnorderedSet = require(Root.UnorderedSet.UnorderedSet)
local List = require(Root.List.List)
local None = require(Root.None)
local sort = require(Root.sort.sort)
local deepJoin = require(script.Parent.deepJoin)
describe("deepJoin", function()
it("Should return nil when called on no values", function()
expect(deepJoin()).to.never.be.ok()
end)
it("Should work with basic types", function()
expect(deepJoin(1, 2)).to.equal(2)
local table1 = {
key1 = "value1",
key2 = "value2"
}
local table2 = {
key1 = "adifferentvalue1",
key3 = "value3"
}
local newtable = deepJoin(table1, table2)
expect(newtable).to.equal(table2)
end)
it("Should work with OrderedSet", function()
local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1)
local set2 = OrderedSet.new(sort.default, 3, 5)
local newSet = deepJoin(set1, set2)
expect(OrderedSet.is(newSet)).to.equal(true)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(5)).to.equal(true)
expect(newSet:size()).to.equal(5)
expect(newSet:first()).to.equal(1)
expect(newSet:last()).to.equal(5)
end)
it("Should work with UnorderedSet", function()
local set1 = UnorderedSet.new(4, 3, 2, 1)
local set2 = UnorderedSet.new(3, 5)
local newSet = deepJoin(set1, set2)
expect(UnorderedSet.is(newSet)).to.equal(true)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(5)).to.equal(true)
expect(newSet:size()).to.equal(5)
end)
it("Should work with Lists", function()
local list1 = List.new(5, 4, 3, 2, 1)
local list2 = List.new(100, 101)
local newList = deepJoin(list1, list2)
expect(List.is(newList)).to.equal(true)
expect(newList:size()).to.equal(7)
expect(newList:get(1)).to.equal(5)
expect(newList:get(newList:size())).to.equal(101)
end)
it("Should work for some crazy nested maps", function()
local outermostMap1 = UnorderedMap.new({
innerMap1 = UnorderedMap.new({
innerList = List.new(3, 4, 4),
innerObject = {
key1 = "value1",
key2 = "value2"
}
}),
innerMap2 = OrderedMap.new(sort.reverse, {
apple = 1,
orange = 100,
}),
differentlyTypedStructure = UnorderedSet.new(3, 4, 5),
irrelevantValue = -1000,
})
local outermostMap2 = UnorderedMap.new({
innerMap1 = UnorderedMap.new({
innerList = List.new(1000),
innerObject = {
key1 = "differentvalue1",
key3 = "value3"
}
}),
innerMap2 = OrderedMap.new(sort.default, {
kiwi = 3,
orange = -5,
}),
differentlyTypedStructure = List.new(3, 4, 5),
irrelevantValue = 1000,
anotherIrrelvantValue = 50,
})
local mergedMap = deepJoin(outermostMap1, outermostMap2)
expect(UnorderedMap.is(mergedMap)).to.equal(true)
expect(mergedMap:size()).to.equal(5)
expect(mergedMap:get("irrelevantValue")).to.equal(1000)
expect(mergedMap:get("anotherIrrelvantValue")).to.equal(50)
local differentlyTypedStructure = mergedMap:get("differentlyTypedStructure")
expect(List.is(differentlyTypedStructure)).to.equal(true)
expect(differentlyTypedStructure:get(2)).to.equal(4)
local innerMap1 = mergedMap:get("innerMap1")
local innerMap2 = mergedMap:get("innerMap2")
expect(UnorderedMap.is(innerMap1)).to.equal(true)
expect(OrderedMap.is(innerMap2)).to.equal(true)
local innerList = innerMap1:get("innerList")
local innerObject = innerMap1:get("innerObject")
expect(List.is(innerList)).to.equal(true)
expect(innerList:size()).to.equal(4)
expect(innerList:get(1)).to.equal(3)
expect(innerList:get(2)).to.equal(4)
expect(innerList:get(3)).to.equal(4)
expect(innerList:get(4)).to.equal(1000)
expect(innerObject["key1"]).to.equal("differentvalue1")
expect(innerObject["key2"]).to.never.be.ok()
expect(innerObject["key3"]).to.equal("value3")
expect(OrderedMap.is(innerMap2)).to.equal(true)
expect(innerMap2:get("apple")).to.equal(1)
expect(innerMap2:get("orange")).to.equal(-5)
expect(innerMap2:get("kiwi")).to.equal(3)
expect((innerMap2:first())).to.equal("orange")
expect((innerMap2:last())).to.equal("apple")
end)
it("Should work for a reasonable case", function()
local state = UnorderedMap.new({
[100] = UnorderedMap.new({
username = "Cool username 1",
information = {
settings = "example 1",
},
}),
[105] = UnorderedMap.new({
username = "Cool username 2",
information = {
settings = "example 2",
},
}),
[110] = UnorderedMap.new({
username = "Cool username 3",
information = {
settings = "example 3",
},
}),
[111] = UnorderedMap.new({
username = "Cool username 4",
information = {
settings = "example 4",
},
}),
})
local actionData = UnorderedMap.new({
[100] = UnorderedMap.new({
username = "Updated cool username 1",
information = {
settings = "updated example 1",
},
}),
[110] = UnorderedMap.new({
username = "Updated cool username 3",
information = {
settings = "updated example 3",
},
}),
})
local newState = deepJoin(state, actionData)
expect(newState:size()).to.equal(4)
expect(newState:get(105)).to.equal(state:get(105))
expect(newState:get(111)).to.equal(state:get(111))
expect(newState:get(100)).never.to.equal(state:get(100))
expect(newState:get(110)).never.to.equal(state:get(110))
local user100 = newState:get(100)
local user110 = newState:get(110)
expect(user100:get("username")).to.equal("Updated cool username 1")
expect(user110:get("username")).to.equal("Updated cool username 3")
expect(user100:get("information")["settings"]).to.equal("updated example 1")
expect(user110:get("information")["settings"]).to.equal("updated example 3")
end)
describe("Multiple joins and None", function()
it("Works in basic cases", function()
expect(deepJoin(1, 2, 3)).to.equal(3)
local data1 = {
name = "Jim",
age = 22
}
local data2 = {
name = "James"
}
local data3 = {
birthday = 11
}
local newData = deepJoin(data1, data2, data3)
expect(newData).to.equal(data3)
end)
it("Should work with OrderedSet", function()
local set1 = OrderedSet.new(sort.default, 4, 3, 2, 1)
local set2 = OrderedSet.new(sort.default, 3, 5)
local set3 = OrderedSet.new(sort.default, 100, 4)
local newSet = deepJoin(set1, set2, set3)
expect(OrderedSet.is(newSet)).to.equal(true)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(5)).to.equal(true)
expect(newSet:find(100)).to.equal(true)
expect(newSet:size()).to.equal(6)
expect(newSet:first()).to.equal(1)
expect(newSet:last()).to.equal(100)
end)
it("Should work with UnorderedSet", function()
local set1 = UnorderedSet.new(4, 3, 2, 1)
local set2 = UnorderedSet.new(3, 5)
local set3 = UnorderedSet.new(100, 4)
local newSet = deepJoin(set1, set2, set3)
expect(UnorderedSet.is(newSet)).to.equal(true)
expect(newSet:find(1)).to.equal(true)
expect(newSet:find(2)).to.equal(true)
expect(newSet:find(3)).to.equal(true)
expect(newSet:find(4)).to.equal(true)
expect(newSet:find(5)).to.equal(true)
expect(newSet:find(100)).to.equal(true)
expect(newSet:size()).to.equal(6)
end)
it("Should work with Lists", function()
local list1 = List.new(5, 4, 3, 2, 1)
local list2 = List.new(100, 101)
local list3 = List.new(1000, 1001)
local newList = deepJoin(list1, list2, list3)
expect(List.is(newList)).to.equal(true)
expect(newList:size()).to.equal(9)
expect(newList:get(1)).to.equal(5)
expect(newList:get(6)).to.equal(100)
expect(newList:get(newList:size())).to.equal(1001)
end)
describe("Working with None", function()
it("Should work in basic cases", function()
local result = deepJoin(5, None)
expect(result).to.never.be.ok()
end)
end)
end)
end)
end
@@ -0,0 +1,10 @@
return {
List = require(script.List.List),
sort = require(script.sort.sort),
OrderedMap = require(script.OrderedMap.OrderedMap),
OrderedSet = require(script.OrderedSet.OrderedSet),
UnorderedMap = require(script.UnorderedMap.UnorderedMap),
UnorderedSet = require(script.UnorderedSet.UnorderedSet),
deepJoin = require(script.deepJoin.deepJoin),
None = require(script.None),
}
@@ -0,0 +1,5 @@
return function()
it("should load", function()
require(script.Parent)
end)
end
@@ -0,0 +1,10 @@
local sort = {
default = function(key1, key2)
return key1 < key2
end,
reverse = function(key1, key2)
return key2 < key1
end,
}
return sort
@@ -0,0 +1,25 @@
return function()
local sort = require(script.Parent.sort)
it("Should be an table with two functions", function()
expect(sort.default).to.be.a("function")
expect(sort.reverse).to.be.a("function")
local less = 1
local more = 2
expect(sort.default(less, more)).to.equal(true)
expect(sort.default(more, less)).to.equal(false)
expect(sort.reverse(less, more)).to.equal(false)
expect(sort.reverse(more, less)).to.equal(true)
end)
it("Should should work well with table.sort", function()
local values = {4, 3, 5, 2, 1}
table.sort(values, sort.default)
for i = 1, #values do
expect(values[i]).to.equal(i)
end
table.sort(values, sort.reverse)
for i = 1, #values do
expect(values[i]).to.equal(5 - i + 1)
end
end)
end
@@ -0,0 +1,6 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "freeze"
version = "0.1.0"
commit = "ef89f9d0444a2a7f63d5fd913a38824f3edba69f"
source = "git+https://github.com/roblox/freeze#master"
dependencies = ["Cryo roblox/cryo 1.0.0 url+https://github.com/roblox/cryo"]