--- A collection library to simplify sequential table operations -- -- local Enumerable = require('__stdlib__/stdlib/vendor/enumerable') -- Enumerable.create({1,2,3}) -- @classmod Vendor.Enumerable -- @author Billiam local Enumerable = {} Enumerable.mt = { __index = function(self, key) return Enumerable[key] or self._data[key] end } --- Tests tables for numeric keys with no gaps -- @tparam table t Table to check -- @treturn boolean True if table is sequence-like, false otherwise local function isSequence(t) local i = 0 for _ in pairs(t) do i = i + 1 if t[i] == nil then return false end end return true end --- Whether the item passed in may be called as a function -- @param f Item to test for callability -- @treturn boolean True if passed in item may be called, false otherwise local function isCallable(f) local t = type(f) if t == 'function' then return true end if t == 'table' then local meta = getmetatable(f) return meta and type(meta.__call) == 'function' end return false end --- Enumerable constructor. -- If no collection is provided, a new empty table will be generated. -- -- @usage -- collection = Enumerable.create({123}) -- @tparam ?table collection Sequential table to wrap -- @treturn enumerable A new collection instance -- @raise 'Enumerable data must be a sequence' if given a non-sequential table function Enumerable.create(collection) if collection and not (type(collection) == 'table' and isSequence(collection)) then error('Enumerable data must be a sequence') end local instance = { --- Internal collection data -- @table _data _data = collection or {} } setmetatable(instance, Enumerable.mt) return instance end --- Return the unwrapped collection data. -- @usage -- collectionData = collection:to_table() -- @treturn table function Enumerable:data() return self._data end --- Create a shallow copy of the unwrapped collection data. -- @usage -- clonedData = collection:to_table() -- @treturn table function Enumerable:to_table() local meta = getmetatable(self._data) local target = {} for k, v in pairs(self._data) do target[k] = v end setmetatable(target, meta) return target end --- Pass all elements in the collection to a callback. -- @usage -- collection:each(function(value, index) ... end) -- @tparam callable callback -- @treturn enumerable The collection instance function Enumerable:each(callback) for i,v in ipairs(self._data) do callback(v, i) end return self end --- Pass all elements in collection to a callback. -- returns a new enumerable instance containing values -- returned by the callback. -- @usage -- collection = Enumerable.create({1, 2, 3}) -- collection:map(function(value, index) return value* 2 end) -- -> Enumerable containing {2, 4, 6} -- @tparam callable callback -- @treturn enumerable New enumerable instance function Enumerable:map(callback) local map = {} for i,v in ipairs(self._data) do local result = callback(v, i) if result ~= nil then table.insert(map, result) end end return Enumerable.create(map) end --- Find the position of the first item in collection for which -- the callback returns true. -- @usage -- collection = Collection.create({0, 1, 2, 3, 4}) -- collection:findIndex(function(value, index) return value > 2 end) -- -> 4 -- @tparam callable callback -- @treturn int the position of the matched element function Enumerable:find_index(callback) for i,v in ipairs(self._data) do if callback(v, i) then return i end end end --- Whether the collection has no elements. -- @usage collection = Enumerable.create() -- if collection:empty() then -- print('Collection is empty') -- end -- -> Collection is empty -- @treturn bool function Enumerable:empty() return #self._data == 0 end --- Return the first element or elements in the collection. -- @see Enumerable:last -- @usage -- collection = Enumerable.create({1,2,3,4}) -- collection:first() -- -> 1 -- collection:first(3) -- -> {1,2,3} -- @tparam ?int n Number of elements to return. If absent, the first item will be returned. -- @treturn table|* function Enumerable:first(n) if not n or n == 1 then return self._data[1] end local list = {} n = math.min(n, #self._data) for i=1,n do table.insert(list, self._data[i]) end return list end --- Return the last element or elements in the collection. -- @see Enumerable:first -- @usage -- collection = Enumerable.create({1,2,3,4}) -- collection:last() -- -> 4 -- collection:last(3) -- -> {2, 3, 4} -- @tparam ?int n Number of elements to return. If absent, the last item will be returned. -- @treturn table function Enumerable:last(n) if not n or n == 1 then return self._data[#self._data] end local list = {} n = math.max(1, #self._data - (n - 1)) for i=n,#self._data do table.insert(list, self._data[i]) end return list end --- Return the number of items in the collection. -- If a callback is given, count will return the -- number of elements for which the callback returns true -- @usage -- collection = Enumerable.create({1,2,3}) -- collection:count() -- -> 3 -- collection:count(function(value, index) return value % 2 == 0 end) -- -> 1 -- @tparam callable callback Callback used to determine whether element should be counted -- @treturn int function Enumerable:count(callback) if not callback then return #self._data end local count = 0 for i,v in ipairs(self._data) do if callback(v, i) then count = count + 1 end end return count end --- Append the contents of one table onto the end of the existing enumerable. -- @usage -- pets = Enumerable:create({'dog', 'cat'}) -- pets:concat({'turtle', 'wizard'}) -- -> pets now contains {'dog', 'cat', 'turtle', 'wizard'} -- @tparam table other Table with content to append to enumerable -- @treturn enumerable The enumerable instance function Enumerable:concat(other) return self:push(unpack(other)) end --- Combine elements of enumerable by passing all items to a callback. -- Values returned by the callback will be used as the accumulator value -- for subsequent callbacks. -- @usage -- numbers = Enumerable.create({1,2,3}) -- numbers:reduce(function(accumulator, value) return (accumulator or 0) + value end) -- -> 6 -- numbers:reduce(20, function(accumulator, value) return accumulator + value end) -- -> 26 -- @tparam ?int initial Initial value for accumulator -- @tparam callable callback -- @return Accumulator value function Enumerable:reduce(initial, callback) if not callback then if isCallable(initial) then callback = initial initial = nil else error('Callback must be a function or table with a __call metamethod') end end local reduce = initial for i,v in ipairs(self._data) do reduce = callback(reduce, v, i) end return reduce end --- Find the lowest value in the enumerable instance. -- If a callback is provided, the return value will be used -- to determine the lowest value. -- @usage -- strings = Enumerable.create({'aaaaaa', 'bbb', 'c'}) -- strings:min() -- -> 'aaaaa' -- strings:min(function(value) return #value end) -- -> 'c' -- @tparam ?callable callback -- @return Lowest value function Enumerable:min(callback) callback = callback or function(v) return v end local lowestValue return self:reduce(function(output, v) local result = callback(v) if not output or (result and result < lowestValue) then lowestValue = result return v end return output end) end --- Find the highest value in the enumerable instance. -- If a callback is provided, the return value will be used -- to determine the highest value. -- @usage -- strings = Enumerable.create({'aaaaaa', 'bbb', 'c'}) -- strings:max() -- -> 'c' -- strings:max(function(value) return #value end) -- -> 'aaaaa' -- @tparam ?callable callback -- @return Highest value function Enumerable:max(callback) callback = callback or function(v) return v end local highestValue return self:reduce(function(output, v) local result = callback(v) if not output or (result and result > highestValue) then highestValue = result return v end return output end) end --- Find the highest and lowest values in the enumerable. -- If a callback is provided, the return value will be used -- to determine the highest and lowest values. -- @usage -- numbers = Enumerable.create({6,3,1,5,2,4}) -- lowest, highest = numbers:minmax() -- -> (1,6) -- strings:max(function(value) return 10 - value end) -- -> (6, 1) -- @tparam ?callable callback -- @return Lowest value -- @return Highest value function Enumerable:minmax(callback) return self:min(callback), self:max(callback) end --- Sort the enumerable by optional callback in place. -- If a callback is not provided, data will be sorted in ascending order. -- If callback is provided, it will be passed two table elements, and should -- return true if the first element should appear first, otherwise false. -- See also: [table.sort documentation](http://www.lua.org/manual/5.1/manual.html#pdf-table.sort) -- @usage -- numbers = Enumerable.create({2,1,3}) -- numbers:sort() -- -> numbers now contains {1,2,3} -- numbers:sort(function(a, b) return b < a end) -- -> numbers now contains {3,2,1} -- @tparam ?callable callback sort method -- @treturn enumerable The collection instance function Enumerable:sort(callback) table.sort(self._data, callback) return self end --- Add one or more items to the enumerable. -- @usage -- items = Enumerable.create({1,2,3}) -- items:push(4, 5) -- -> items contains {1,2,3,4,5} -- @param ... Items to append -- @treturn enumerable The collection instance function Enumerable:push(...) for i,v in ipairs({...}) do table.insert(self._data, v) end return self end --- Remove and return the last item from the collection. -- @usage -- items = Enumerable.create({1,2,3}) -- items:pop() -- -> returns 3 -- -> items now contains {1,2} -- @treturn enumerable The collection instance function Enumerable:pop() return table.remove(self._data, #self._data) end --- Remove and return the first item from the collection. -- @usage -- items = Enumerable.create({1,2,3}) -- items:shift() -- -> returns 1 -- -> items now contains {2,3} -- @treturn enumerable The collection instance function Enumerable:shift() return table.remove(self._data, 1) end --- Insert one or more items into the beginning of the collection. -- @usage -- items = Enumerable.create({4,5,6}) -- items:unshift(1,2,3) -- -> Items now contains {1,2,3,4,5,6} -- @param ... Elements to insert -- @treturn enumerable The collection instance function Enumerable:unshift(...) for i,v in ipairs({...}) do table.insert(self._data, i, v) end return self end --- Returns the first element in the collection where the callback returns true. -- @usage -- numbers = Enumerable.create({20, 30, 40}) -- numbers:find(function(value, index) return value > 25 end) -- -> 30 -- @tparam callable callback -- @return Matching item function Enumerable:find(callback) for i,v in ipairs(self._data) do if callback(v, i) then return v end end end --- Create a new collection with elements which the callback returns false. -- @usage -- items = Enumerable.create({1,2,3,4,5,6}) -- odd = Enumerable:reject(function(value, index) return value % 2 == 0 end) -- -> Enumerable containing {1,3,5} -- @tparam callable callback -- @treturn enumerable New collection instance function Enumerable:reject(callback) local reject = {} for i,v in ipairs(self._data) do if not callback(v, i) then table.insert(reject, v) end end return Enumerable.create(reject) end --- Create a new collection with elements which the callback returns true. -- @usage -- items = Enumerable.create({1,2,3,4,5,6}) -- even = Enumerable:select(function(value, index) return value % 2 == 0 end) -- -> Enumerable containing {2,4,6} -- @tparam callable callback -- @treturn enumerable New collection instance -- @alias find_all function Enumerable:select(callback) local select = {} for i,v in ipairs(self._data) do if callback(v, i) then table.insert(select, v) end end return Enumerable.create(select) end --- Returns true if callback returns truthy for all elements in the collection. -- @usage -- items = Enumerable.create({10, 20, 30}) -- items:all(function(value, index) return value > 5 end) -- -> true -- items:all(function(value, index) return value < 25 end) -- -> false -- @tparam callable callback -- @treturn bool function Enumerable:all(callback) for i,v in ipairs(self._data) do if not callback(v, i) then return false end end return true end --- Returns true if callback returns truthy for any element in the collection. -- @usage -- items = Enumerable.create({10, 20, 30}) -- items:any(function(value, index) return value > 25 end) -- -> true -- items:any(function(value, index) return value > 30 end) -- -> false -- @tparam callable callback -- @treturn bool function Enumerable:any(callback) for i,v in ipairs(self._data) do if callback(v, i) then return true end end return false end --- Groups elements into collections based on the result of the provided callback. -- Resulting table will have keys matching the returned value of the callback, and -- values as a table of elements which returned that value. -- @usage -- numbers = Enumerable.create({1,2,3,4,5,6}) -- result = Enumerable.group_by(function(value, index) return value % 3 end) -- result[0] -- -> Enumerable containing {3, 6} -- result[1] -- -> Enumerable containing {2, 5} -- result[2] -- -> Enumerable containing {1, 4} -- @tparam callable callback -- @treturn table function Enumerable:group_by(callback) local groups = {} for i,v in ipairs(self._data) do local criteria = callback(v, i) groups[criteria] = groups[criteria] or Enumerable.create() groups[criteria]:push(v) end return groups end --- Split enumerable into two groups, based on the true or false result of the callback. -- -- Aliases: find_all, detect -- -- @usage -- numbers = Enumerable.create({1,2,3,4,5,6}) -- even, odd = Enumerable:partition(function(value, index) return value % 2 == 1 end) -- -> even is a collection containing {2, 4, 6} -- -> odd is a collection containing {1, 3, 5} -- @tparam callable callback -- @treturn enumerable Collection containing items which returned true -- @treturn enumerable Collection containing items which returned false function Enumerable:partition(callback) local truthyCallback = function(v, i) return callback(v, i) and true or false end local results = self:group_by(truthyCallback) return results[true] or Enumerable.create(), results[false] or Enumerable.create() end --- -- @function Enumerable:find_all -- @see Enumerable:select Enumerable.find_all = Enumerable.select --- -- @function Enumerable:detect -- @see Enumerable:select Enumerable.detect = Enumerable.select --- -- @function Enumerable:collect -- @see Enumerable:map Enumerable.collect = Enumerable.map --- -- @function Enumerable:inject -- @see Enumerable:reduce Enumerable.inject = Enumerable.reduce return Enumerable