add gs
This commit is contained in:
@@ -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
|
||||
+559
@@ -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
|
||||
+479
@@ -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
|
||||
+621
@@ -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
|
||||
+278
@@ -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
|
||||
+359
@@ -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
|
||||
+303
@@ -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
|
||||
+476
@@ -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
|
||||
+216
@@ -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
|
||||
+255
@@ -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
|
||||
+29
@@ -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
|
||||
+38
@@ -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
|
||||
+50
@@ -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
|
||||
+267
@@ -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
|
||||
+25
@@ -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"]
|
||||
Reference in New Issue
Block a user