diff --git a/collections/.package b/collections/.package new file mode 100644 index 0000000..cb0e412 --- /dev/null +++ b/collections/.package @@ -0,0 +1,9 @@ +{ + title = 'Collections', + repository = 'kepler155c/opus-apps/{{OPUS_BRANCH}}/collections', + description = [[Collections (rework) +See: https://github.com/imliam/Lua-Collections + +Collections are like tables on steroids. They are designed to act as a fluent wrapper when working with structured data, offering the developer convenience for common tasks.]], + license = 'MIT', +} diff --git a/collections/apis/init.lua b/collections/apis/init.lua new file mode 100644 index 0000000..8a98395 --- /dev/null +++ b/collections/apis/init.lua @@ -0,0 +1,1257 @@ +-- collections.lua - A robust collection class based on Laravel collections +-- +-- @module collections.lua +-- @alias Collection +-- @url https://github.com/ImLiam/Lua-Collections + +local Collection = { + version = '0.2.0' +} + +--- Returns all elements from a collection as a table +function Collection:all() + local tbl = {} + for key, value in pairs(self.table) do + tbl[key] = value + end + return tbl +end + +--- Adds an item to the end of a collection +function Collection:append(value) + table.insert(self.table, value) + return self +end + +--- Returns the average value of a list or given key +function Collection:average(key) + local count = self:count() + if count > 0 then + return self:sum(key) / count + end +end + +--- Breaks the collection into multiple smaller collections of a given size +function Collection:chunk(count) + local chunks = self:new() + local currentChunk = {} + + if count <= 0 then + return Collection:new({ {} }) + end + + for _, value in pairs(self.table) do + table.insert(currentChunk, value) + if #currentChunk == count then + chunks:push(currentChunk) + currentChunk = {} + end + end + if #currentChunk > 0 then + chunks:push(currentChunk) + end + return chunks +end + +--- Returns a copy of the collection +function Collection:clone() + local cloned = {} + for key, value in pairs(self.table) do + cloned[key] = value + end + return self:new(cloned) +end + +--- Collapses a collection of tables into a single, flat collection +function Collection:collapse() + local collapsed = self:new() + for _, value in pairs(self.table) do + for _, innerValue in pairs(value) do + collapsed:push(innerValue) + end + end + return collapsed +end + +--- Combines the keys of the collection with the values of another table +function Collection:combine(values) + local combined = self:new() + for key, value in pairs(values) do + if self.table[key] then + combined:set(self.table[key], value) + end + end + return combined +end + +--- Determines whether the collection contains a given item +function Collection:contains(containValue, recursive) + + local function checkContains(key, value) + if type(containValue) == 'function' then + local result = containValue(key, value) + if result then + return true + end + else + if value == containValue then + return true + end + end + return false + end + + for key, value in pairs(self.table) do + if type(value) == 'table' and recursive then + for innerKey, innerValue in pairs(value) do + if checkContains(innerKey, innerValue) then + return true + end + end + else + if checkContains(key, value) then + return true + end + end + end + + return false +end + +--- Turns an associative table into an indexed one, removing string keys +function Collection:convertToIndexed() + local notAssociative = self:new() + for _, value in pairs(self.table) do + notAssociative:push(value) + end + return notAssociative +end + +--- Returns the total number of items in the collection +function Collection:count() + local i = 0 + for _ in pairs(self.table) do + i = i + 1 + end + return i +end + +--- Deals the collection into a number of groups in order, one at a time +function Collection:deal(hands) + local splitted = self:times(hands, function() return {} end) + local currentSection = 1 + + for _, value in pairs(self.table) do + table.insert(splitted.table[currentSection], value) + + currentSection = currentSection + 1 + if currentSection > #splitted.table then + currentSection = 1 + end + end + + return splitted +end + +--- Compares a collection against another table based on its values +--- Returns the values in the original collection that are not present in the given table +function Collection:diff(difference) + local differenceList = {} + for _, value in pairs(difference) do + differenceList[value] = true + end + + local finalDifferences = self:new() + for _, value in pairs(self.table) do + if not differenceList[value] then + finalDifferences:push(value) + end + end + return finalDifferences +end + +--- Compares the collection against another table based on its keys +--- Returns the key / value pairs in the original collection that are not present in the table +function Collection:diffKeys(difference) + local differenceList = {} + for key in pairs(difference) do + differenceList[key] = true + end + + local finalDifferences = self:new() + for key, value in pairs(self.table) do + if not differenceList[key] then + finalDifferences:set(key, value) + end + end + return finalDifferences +end + +--- Iterates over the items in the collection and passes each to a callback +function Collection:each(callback) + for key, value in pairs(self.table) do + if callback(key, value) == false then + break + end + end + return self +end + +--- Iterates over the numerically indexed items in the collection and passes each to a callback +function Collection:eachi(callback) + for key, value in ipairs(self.table) do + if callback(key, value) == false then + break + end + end + return self +end + +--- Compares a table with the internal table of the collection +function Collection:equals(tbl, ignoreMetatables, tbl2) + tbl2 = tbl2 or self.table + if tbl == tbl2 then return true end + local tblType = type(tbl) + local tbl2Type = type(tbl2) + if tblType ~= tbl2Type then return false end + if tblType ~= 'table' then return false end + + if not ignoreMetatables then + local mt1 = getmetatable(tbl) + if mt1 and mt1.__eq then + return tbl == tbl2 + end + end + + local keySet = {} + + for key1, value1 in pairs(tbl) do + local value2 = tbl2[key1] + if value2 == nil or self:equals(value1, ignoreMetatables, value2) == false then + return false + end + keySet[key1] = true + end + + for key2, _ in pairs(tbl2) do + if not keySet[key2] then return false end + end + return true +end + +--- Verify that all elements of the collection pass a truth test +function Collection:every(callback) + for key, value in pairs(self.table) do + if not callback(key, value) then + return false + end + end + return true +end + +--- Returns all items in the collection except for those with specified keys +function Collection:except(keys) + local exceptList = {} + for _, value in pairs(keys) do + exceptList[value] = true + end + + local tbl = self:new() + for key, value in pairs(self.table) do + if not exceptList[key] then + tbl:set(key, value) + end + end + return tbl +end + +--- Internal function used to determine if a value is falsey +function Collection.falsyValue(_, value) + for _, v in ipairs({0, false, ''}) do + if v == value then + return true + end + end + + if type(value) == 'table' then + if next(value) == nil then + return true + end + end + + if not value then + return true + end + + return false +end + +--- Filters the collection using the given callback, keeping only items that pass a truth test +function Collection:filter(callback) + local filtered = self:new() + for key, value in pairs(self.table) do + local response = false + if callback then + response = callback(key, value) + elseif not self:falsyValue(value) then + response = true + end + if response then + filtered:set(key, value) + end + end + return filtered +end + +--- Returns the first element in the collection, or that passes a truth test +function Collection:first(callback) + for key, value in pairs(self.table) do + if callback then + if callback(key, value) then + return value + end + else + return value + end + end +end + +--- Flattens a multi-dimensional collection into a single dimension +function Collection:flatten(depth, tbl, currentDepth) + local flattened = self:new() + local iterable = tbl or self.table + currentDepth = currentDepth or 0 + for _, value in pairs(iterable) do + if type(value) == 'table' + and ((depth and currentDepth < depth - 1) or not depth) then + local flatInside = self:flatten(depth, value, currentDepth + 1):all() + for _, v in pairs(flatInside) do + flattened:push(v) + end + else + flattened:push(value) + end + end + if tbl then + return flattened + else + return flattened + end +end + +--- Swaps the collection's keys with their corresponding values +function Collection:flip() + local flipped = self:new() + for key, value in pairs(self.table) do + flipped:set(value, key) + end + return flipped +end + +--- Removes an item from the collection by its key +function Collection:forget(key) + if self.table[key] then + self.table[key] = nil + end + return self +end + +--- Returns a collection containing the items that would be present for a given page number +function Collection:forPage(pageNumber, perPage) + local page = self:new() + local i = 1 + for _, value in pairs(self.table) do + if i > (pageNumber - 1) * perPage and i <= pageNumber * perPage then + page:push(value) + end + i = i + 1 + end + return page +end + +--- Returns the item of a given key +function Collection:get(key, default) + if self.table[key] then + return self.table[key] + elseif default then + if type(default) == 'function' then + return default(key) + else + return default + end + end +end + +--- Groups the collection's items by a given key +function Collection:groupBy(groupKey) + local grouped = self:new() + for _, value in pairs(self.table) do + + local currentGroupKey = groupKey + + if value[currentGroupKey] then + if not grouped:has(value[currentGroupKey]) then + grouped:set(value[currentGroupKey], {}) + end + table.insert(grouped.table[value[currentGroupKey]], value) + end + + end + return grouped +end + +--- Determines if a given key exists in the collection +function Collection:has(key) + if self.table[key] then + return true + end + return false +end + +--- Joins the items in a collection into a string +function Collection:implode(implodedKey, delimeter) + if type(self:first()) == 'table' then + local toImplode = {} + for _, value in pairs(self.table) do + if value[implodedKey] then + table.insert(toImplode, value[implodedKey]) + end + end + return table.concat(toImplode, delimeter or ', ') + else + return table.concat(self.table, implodedKey or ', ') + end +end + +--- Inserts a value at a given numeric index +function Collection:insert(value, position) + table.insert(self.table, position, value) + return self +end + +--- Removes any values from the original collection that are not present in the passed table +function Collection:intersect(intersection) + local intersected = self:new() + intersection = Collection:new(intersection):flip():all() + + for key, value in pairs(self.table) do + if intersection[value] then + intersected:set(key, value) + end + end + + return intersected +end + +--- Determines whether the collection is associative +function Collection:isAssociative() + if self:count() > #self.table then + return true + end + return false +end + +--- Determines if the collection is empty +function Collection:isEmpty() + if next(self.table) == nil then + return true + end + return false +end + +--- Determines if the collection is not empty +function Collection:isNotEmpty() + if next(self.table) ~= nil then + return true + end + return false +end + +--- Keys the collection by the given key +function Collection:keyBy(keyName) + local keyed = self:new() + for key, value in pairs(self.table) do + if type(keyName) == 'function' then + local response = keyName(key, value) + keyed:set(response, value) + else + keyed:set(value[keyName], value) + end + end + return keyed +end + +--- Returns a list of the collection's keys +function Collection:keys() + local keys = self:new() + for key in pairs(self.table) do + keys:push(key) + end + return keys +end + +--- Returns the last element in the collection, or that passes a truth test +function Collection:last(callback) + local currentValue + for key, value in pairs(self.table) do + if callback then + if callback(key, value) then + currentValue = value + end + else + currentValue = value + end + end + return currentValue +end + +--- Iterates through the collection and passes each value to the callback, which can then modify the values +function Collection:map(callback) + local remapped = self:new() + for key, value in pairs(self.table) do + local newKey, newValue = callback(key, value) + remapped:set(newKey, newValue) + end + return remapped +end + +--- Iterates through the the collection and remaps the key and value based on the return of a callback +function Collection:mapWithKeys(callback) + local mapped = self:new() + for key, value in pairs(self.table) do + local k, v = callback(key, value) + mapped:set(k, v) + end + return mapped +end + +--- Returns the maximum value of a set of given values +function Collection:max(maxKey) + local max + for _, value in pairs(self.table) do + if maxKey then + if not max or value[maxKey] > max then + max = value[maxKey] + end + else + if not max or value > max then + max = value + end + end + end + return max +end + +--- Returns the median value of a set of given values +function Collection:median(medianKey) + local all = {} + for _, value in pairs(self.table) do + if medianKey then + table.insert(all, value[medianKey]) + else + table.insert(all, value) + end + end + table.sort(all, function(a, b) return a < b end) + + if math.fmod(#all, 2) == 0 then + return (all[#all / 2] + all[(#all / 2) + 1] ) / 2 + else + return all[math.ceil(#all/2)] + end +end + +--- Merges the given table with the original collection +function Collection:merge(toMerge) + local merged = self:clone() + for key, value in pairs(toMerge) do + if type(key) == 'number' then + merged:push(value) + else + merged:set(key, value) + end + end + return merged +end + +--- Returns the minimum value of a set of given values +function Collection:min(minKey) + local min + for _, value in pairs(self.table) do + if minKey then + if not min or value[minKey] < min then + min = value[minKey] + end + else + if not min or value < min then + min = value + end + end + end + return min +end + +--- Returns the mode value of a given key +function Collection:mode(modeKey) + local counts = {} + + for _, value in pairs(self.table) do + if modeKey then + value = value[modeKey] + end + if counts[value] == nil then + counts[value] = 1 + else + counts[value] = counts[value] + 1 + end + end + + local biggestCount = 0 + + for _, value in pairs(counts) do + if value > biggestCount then + biggestCount = value + end + end + + local temp = self:new() + + for key, value in pairs(counts) do + if value == biggestCount then + temp:push(key) + end + end + + return temp +end + +--- Creates a new collection instance +function Collection.new(_, tbl) + return setmetatable({ table = tbl or {} }, { __index = Collection, __tostring = Collection.toString }) +end + +--- Creates a new collection consisting of every nth element +function Collection:nth(step, offset) + local nth = self:new() + local position = 1 + offset = (offset and offset + 1) or 1 + + for _, value in pairs(self.table) do + if position % step == offset then + nth:push(value) + end + position = position + 1 + end + + return nth +end + +--- Returns the items in the collection with the specified keys +function Collection:only(keys) + local onlyList = {} + for _, value in pairs(keys) do + onlyList[value] = true + end + + local tbl = self:new() + for key, value in pairs(self.table) do + if onlyList[key] then + tbl:set(key, value) + end + end + return tbl +end + +--- Returns a pair of elements that pass and fail a given truth test +function Collection:partition(callback) + local valid = Collection:new() + local invalid = Collection:new() + + for key, value in pairs(self.table) do + local result = callback(key, value) + if result then + valid:push(value) + else + invalid:push(value) + end + end + + return valid, invalid +end + +-- Passes the collection to the given callback and returns the result +function Collection:pipe(callback) + return callback(self) +end + +--- Retrives all of the values for a given key +function Collection:pluck(valueName, keyName) + local plucked = self:new() + for _, value in pairs(self.table) do + if value[valueName] then + if keyName then + plucked:set(value[keyName], value[valueName]) + else + plucked:push(value[valueName]) + end + end + end + return plucked +end + +--- Removes and returns the last item from the collection +function Collection:pop() + return table.remove(self.table, #self.table) +end + +--- Adds an item to the beginning of the collection +function Collection:prepend(value) + table.insert(self.table, 1, value) + return self +end + +--- Removes and returns an item from the collection by key +function Collection:pull(key) + if type(key) == 'number' then + return table.remove(self.table, key) + else + local pulled = self.table[key] + self.table[key] = nil + return pulled + end +end + +--- Sets the given key and value in the collection +function Collection:put(key, value) + self.table[key] = value + return self +end + + +--- Returns a random item or number of items from the collection +function Collection:random(count, rep) + local all = self:new(self.table):convertToIndexed():all() + local random = self:new() + count = count or 1 + + if count < 0 then + error('Positive number expected, negative number given.') + end + + for _ = 1, count do + if #all > 0 then + local randomElement + if rep then + randomElement = all[math.random(#all)] + else + randomElement = table.remove(all, math.random(#all)) + end + random:push(randomElement) + end + end + + if count == 1 and random[1] then + return random[1] + end + return random +end + +--- Reduces the collection to a single value, passing the result of each iteration into the next +function Collection:reduce(callback, default) + local carry = default + for _, value in pairs(self.table) do + carry = callback(carry, value) + end + return carry +end + +--- Filters the collection using the given fallback +function Collection:reject(callback) + local notRejected = self:new() + for key, value in pairs(self.table) do + local rejected = false + if callback then + rejected = callback(key, value) + elseif not self:falsyValue(value) then + rejected = true + end + if not rejected then + notRejected:set(key, value) + end + end + return notRejected +end + +--- Fixes numerical keys to put them in order +function Collection:resort() + local sorted = self:new() + for key, value in pairs(self.table) do + if type(key) == 'number' then + sorted:push(value) + else + sorted[key] = value + end + end + return sorted +end + +--- Reverses the order of the numerical keys in the collection +function Collection:reverse() + local reversed = self:new() + for key, value in pairs(self.table) do + if type(key) == 'number' then + reversed:prepend(value) + else + reversed:set(key, value) + end + end + return reversed +end + +--- Searches the collection for a value and returns the key +function Collection:search(callback) + for key, value in pairs(self.table) do + if type(callback) == 'function' then + local result = callback(key, value) + if result then + return key + end + else + if callback == value then + return key + end + end + end +end + +--- Removes and returns the first item from the collection +function Collection:shift() + for key, value in pairs(self.table) do + if type(key) == 'number' then + return table.remove(self.table, key) + else + self.table[key] = nil + return value + end + end +end + +--- Randomly shuffles the order of items in the collection +function Collection:shuffle() + local shuffled = self:new() + local numericKeys = {} + local numericValues = {} + for key, value in pairs(self.table) do + if type(key) == 'number' then + table.insert(numericKeys, key) + table.insert(numericValues, value) + end + end + + for key, value in pairs(self.table) do + if type(key) == 'number' then + shuffled:set(table.remove(numericKeys, math.random(#numericKeys)), table.remove(numericValues, math.random(#numericValues))) + -- todo: make this push into a random spot in the array + else + shuffled:set(key, value) + end + end + return shuffled +end + +--- Returns a slice of the collection at the given index +function Collection:slice(index, size) + local slice = self:new() + local i = 0 + for key, value in ipairs(self.table) do + if key >= index + 1 then + slice:append(value) + if size then + i = i + 1 + if i == size then + return slice + end + end + end + end + return slice +end + +--- Sorts the items in the collection +function Collection:sort(callback) + local sorted = self:clone() + if callback and type(callback) == 'function' then + table.sort(sorted.table, callback) + elseif callback then + table.sort(sorted.table, function(a, b) return a[callback] < b[callback] end) + else + table.sort(sorted.table, function(a, b) return a < b end) + end + return sorted +end + +--- Same as the Collection:sort() method, but returns the collection in the opposite order +function Collection:sortDesc(callback) + local sorted = self:clone() + if callback and type(callback) == 'function' then + table.sort(sorted.table, callback) + sorted = sorted:reverse() + elseif callback then + table.sort(sorted.table, function(a, b) return a[callback] > b[callback] end) + else + table.sort(sorted.table, function(a, b) return a > b end) + end + return sorted +end + +--- Removes and returns a slice of items starting at the specified index +function Collection:splice(index, size, replacements) + local spliced = self:new() + local toRemove = {} + local i = 0 + for key, value in ipairs(self.table) do + if key >= index + 1 then + + spliced:append(value) + table.insert(toRemove, key) + + if size then + i = i + 1 + if i == size then + break + end + end + + end + end + + local removedIndex = 0 + for _, key in pairs(toRemove) do + if type(key) == 'number' then + table.remove(self.table, key + removedIndex) + removedIndex = removedIndex - 1 + else + self.table[key] = nil + end + end + + if replacements then + for _ = 1, #replacements do + self:insert(table.remove(replacements, #replacements), index + 1) + end + end + + return spliced +end + +--- Breaks the collection into the given number of groups +function Collection:split(count) + local groupSize = math.ceil(self:count() / count) + return self:chunk(groupSize) +end + +--- Returns the sum of items in the collection +function Collection:sum(key) + local sum = 0 + for i, value in pairs(self.table) do + if key then + if value[key] then + sum = sum + value[key] + else + error('Value "' .. key .. '" does not exist in collection object with key "' .. i .. '"') + end + else + sum = sum + value + end + end + return sum +end + +--- Returns a collection with the specified number of items +function Collection:take(count) + local taken = self:new() + if count >= 0 then + for i = 1, count do + if self.table[i] then + taken:append(self.table[i]) + else + break + end + end + else + local iterations = 0 + for i = #self.table, 1, -1 do + if self.table[i] then + taken:prepend(self.table[i]) + else + break + end + iterations = iterations + 1 + if iterations == -count then + break + end + end + end + return taken +end + +--- Internal function used to determine if a table is associative +function Collection.tableIsAssociative(_, tbl) + local totalCount = 0 + for _ in pairs(tbl) do + totalCount = totalCount + 1 + end + if totalCount > #tbl then + return true + end + return false +end + +--- Internal method used by the Collection:toJSON method to recursively convert tables +function Collection:tableToJSON(tbl) + local jsonRepresentation = function(value) + if type(value) == 'table' then + return self:tableToJSON(value) + elseif type(value) == 'string' then + return '"' .. value:gsub('"', '\\"') .. '"' + elseif type(value) == 'number' then + return value + elseif type(value) == 'boolean' then + return (value and 'true' or 'false') + end + end + + local jsonElements = {} + if self:tableIsAssociative(tbl) then + for key, value in pairs(tbl) do + local json = jsonRepresentation(value) + if json then + json = '"' .. key:gsub('"', '\\"') .. '":' .. json + table.insert(jsonElements, json) + end + end + return '{' .. table.concat(jsonElements, ',') .. '}' + else + for _, value in pairs(tbl) do + local json = jsonRepresentation(value) + table.insert(jsonElements, json) + end + return '[' .. table.concat(jsonElements, ',') .. ']' + end +end + +--- Internal method used by the Collection:toString method to recursively convert tables +function Collection:tableToString(tbl) + local luaRepresentation = function(value) + if type(value) == 'table' then + return self:tableToString(value) + elseif type(value) == 'string' then + return '"' .. value .. '"' + elseif type(value) == 'number' then + return value + elseif type(value) == 'boolean' then + return (value and 'true' or 'false') + end + end + + local luaElements = {} + + for key, value in pairs(tbl) do + local luaString = luaRepresentation(value) + if luaString then + if type(key) == 'number' then + luaString = '[' .. key .. ']=' .. luaString + elseif type(key) == 'string' then + if key:match('%W') then + luaString = '["' .. key:gsub('"', '\\"') .. '"]=' .. luaString + else + luaString = key .. '=' .. luaString + end + end + table.insert(luaElements, luaString) + end + end + return '{' .. table.concat(luaElements, ',') .. '}' +end + +--- Executes the given callback without affecting the collection itself +function Collection:tap(callback) + callback(self) + return self +end + +--- Creates a new collection by invoking the callback a given amount of times +function Collection:times(count, callback) + local tbl = {} + for i = 1, count do + table.insert(tbl, callback(i, tbl)) + end + return self:new(tbl) +end + +--- Returns a reference to the underlying table of the collection +function Collection:toTable() + return self.table +end + +--- Returns a JSON string representation of the collection's values +function Collection:toJSON() + return self:tableToJSON(self.table) +end + +--- Returns a string representation of a Lua table +function Collection:toString() + return self:tableToString(self.table) +end + +--- Iterates over the collection and calls the given callback with each item in the collection, replacing the values in the collection with the response +function Collection:transform(callback) + local transformed = self:new() + for key, value in pairs(self.table) do + transformed:set(key, callback(key, value)) + end + return transformed +end + +--- Adds the given table to the collection +function Collection:union(tbl) + local unionised = self:clone() + for key, value in pairs(tbl) do + if not unionised:has(key) then + unionised:set(key, value) + end + end + return unionised +end + +--- Returns all of the unique items in the collection +function Collection:unique(callback) + local unique = self:new() + local keyList = {} + + for key, value in pairs(self.table) do + local valueToCheck = value + if type(callback) == 'function' then + valueToCheck = callback(key, value) + elseif callback then + valueToCheck = value[callback] + end + if keyList[valueToCheck] ~= nil then + keyList[valueToCheck] = false + else + keyList[valueToCheck] = key + end + end + + for _, key in pairs(keyList) do + if key ~= false then + unique:push(self:get(key)) + end + end + + return unique +end + +--- Executes the given callback when a condition is met +function Collection:when(condition, callback) + if condition then + callback(self) + end + return self +end + +--- Filters the collection by a given key / value pair +function Collection:where(filterKey, filterValue) + local filtered = self:new() + for _, value in pairs(self.table) do + if value[filterKey] == filterValue then + filtered:push(value) + end + end + return filtered +end + +--- Filters the collection by a given key / value contained within the given table +function Collection:whereIn(filterKey, filterValues) + local filtered = self:new() + for _, value in pairs(self.table) do + for _, filterValue in ipairs(filterValues) do + if value[filterKey] == filterValue then + filtered:push(value) + end + end + end + return filtered +end + +--- Filters the collection by a given key / value not contained within the given table +function Collection:whereNotIn(filterKey, filterValues) + local filtered = self:new() + for _, value in pairs(self.table) do + local allowed = true + for _, filterValue in ipairs(filterValues) do + if value[filterKey] == filterValue then + allowed = false + end + end + if allowed then + filtered:push(value) + end + end + return filtered +end + +--- Merges the value of the given table to the value of the original collection at the same index +function Collection:zip(values) + local zipped = self:clone() + for key, value in pairs(values) do + if zipped:has(key) then + zipped:set(key, {zipped:get(key), value}) + end + end + return zipped +end + + + +-----[[ ALIASES FOR OTHER FUNCTIONS ]]----- + + + +--- Alias for the Collection:average() method +Collection.avg = Collection.average + +--- Alias for the Collection:average() method +Collection.mean = Collection.average + +--- Alias for the Collection:each() method +Collection.forEach = Collection.each + +--- Alias for the Collection:eachi() method +Collection.forEachi = Collection.eachi + +--- Alias for the Collection:forget() method +Collection.remove = Collection.forget + +--- Alias for the Collection:convertToIndexed() method +Collection.deassociate = Collection.convertToIndexed + +--- Alias for the Collection:append() method +Collection.push = Collection.append + +--- Alias for the Collection:put() method +Collection.set = Collection.put + +--- Alias for the Collection:resort() method +Collection.values = Collection.resort + +--- Alias for the Collection:sort() method +Collection.sortAsc = Collection.sort + +--- Alias for the Collection:average() method +Collection.replace = Collection.splice + +return setmetatable( + { new = Collection.new }, + { __call = Collection.new } +) diff --git a/collections/etc/fstab b/collections/etc/fstab new file mode 100644 index 0000000..115abbc --- /dev/null +++ b/collections/etc/fstab @@ -0,0 +1 @@ +packages/collections/tests/tests.lua urlfs https://raw.githubusercontent.com/imliam/Lua-Collections/master/tests.lua \ No newline at end of file diff --git a/collections/tests/tests.lua b/collections/tests/tests.lua new file mode 100644 index 0000000..0f177a4 --- /dev/null +++ b/collections/tests/tests.lua @@ -0,0 +1,1030 @@ +local Collection = require "collections" + +local collect = Collection +--- Dump and die (for debugging purposes) +local function dd(value) + if type(value) == 'table' then + print(Collection:tableToString(value)) + elseif type(value) == 'string' then + print('"' .. value .. '"') + else + print(value) + end + os.exit() +end + +--- Assert that two tables are equal +function assert_tables_equal(tbl1, tbl2) + if Collection(tbl1):equals(tbl2) then + return true + end + return error('Compared tables are not identical.') +end + +--[[ all ]]-- +do + assert_tables_equal( + Collection({'a', 'b', 'c'}):all(), + {'a', 'b', 'c'} + ) +end + +--[[ append ]]-- +do + assert_tables_equal( + Collection({1, 2, 3, 4}):append(5):all(), + {1, 2, 3, 4, 5} + ) +end + +--[[ average ]]-- +do + assert(Collection({1, 1, 2, 4}):average() == 2) + + assert(Collection({ {foo = 10}, {foo = 10}, {foo = 20}, {foo = 40} }):average('foo') == 20) +end + +--[[ chunk ]]-- +do + assert_tables_equal( + Collection({1, 2, 3, 4, 5, 6, 7}):chunk(4):all(), + { {1, 2, 3, 4}, {5, 6, 7} } + ) + + assert_tables_equal( + Collection({1, 2, 3, 4, 5, 6, 7}):chunk(0):all(), + { {} } + ) +end + +--[[ clone ]]-- +do + local collection = Collection({1, 2, 3, 4, 5}) + local clone = collection:clone():append(6) + + assert_tables_equal( + clone:all(), + {1, 2, 3, 4, 5, 6} + ) + + assert_tables_equal( + collection:all(), + {1, 2, 3, 4, 5} + ) + +end + +--[[ collapse ]]-- +do + assert_tables_equal( + Collection({ {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }):collapse():all(), + {1, 2, 3, 4, 5, 6, 7, 8, 9} + ) +end + +--[[ combine ]]-- +do + assert_tables_equal( + Collection({'name', 'age'}):combine({'George', 29}):all(), + {name = 'George', age = 29} + ) +end + +--[[ contains ]]-- +do + assert(Collection({'Cat', 'Dog'}):contains('Cat') == true) + + assert(Collection({'Cat', 'Dog'}):contains('Walrus') == false) + + assert(Collection({evil = 'Cat', good = 'Dog'}):contains('Cat') == true) + + assert(Collection({1, 2, 3, 4, 5}):contains(function(key, value) + return value > 5 + end) == false) + + assert(Collection({ {'Cat', 'Dog'}, {'Rabbit', 'Mouse'} }):contains('Cat', true) == true) + + assert(Collection({ {'Cat', 'Dog'}, {'Rabbit', 'Mouse'} }):contains('Cat') == false) +end + +--[[ convertToIndexed ]]-- +do + assert_tables_equal( + Collection({name = 'Liam', language = 'Lua'}):convertToIndexed():all(), + {'Liam', 'Lua'} + ) +end + +--[[ count ]]-- +do + assert(Collection({'a', 'b', 'c', 'd', 'e'}):count() == 5) +end + +--[[ deal ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}):deal(3):all(), + { {1, 4, 7, 10}, {2, 5, 8}, {3, 6, 9} } + ) +end + +--[[ diff ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6}):diff({2, 4, 6, 8}):all(), + {1, 3, 5} + ) +end + +--[[ diffKeys ]]-- +do + assert_tables_equal( + collect({one = 10, two = 20, three = 30, four = 40, five = 50}) + :diffKeys({two = 2, four = 4, six = 6, eight = 8}) + :all(), + {one = 10, three = 30, five = 50} + ) + +end + +--[[ each ]]-- +do + local tbl = {} + collect({'a', 'b', 'c'}):each(function(key, value) + tbl[key] = value + end) + + assert_tables_equal(tbl, {'a', 'b', 'c'}) +end + +--[[ every ]]-- +do + assert(collect({1, 2, 3, 4}):every(function(key, value) + return value > 2 + end) == false) +end + +--[[ except ]]-- +do + assert_tables_equal( + collect({productID = 1, price=100, discount = false}) + :except({'price', 'discount'}) + :all(), + {productID = 1} + ) +end + +--[[ equals ]]-- +do + local collection = collect({ + 1, 2, 3, + [97] = 97, [98] = 98, [99] = 99, key = true, + sub = {1, 2, 3, sub = 'Hello world.'} + }) + + assert(collection.table[1] == 1) + assert(collection.table[2] == 2) + assert(collection.table[3] == 3) + assert(collection.table[97] == 97) + assert(collection.table[98] == 98) + assert(collection.table[99] == 99) + assert(collection.table['key'] == true) + assert(collection.table['sub'][1] == 1) + assert(collection.table['sub'][2] == 2) + assert(collection.table['sub'][3] == 3) + assert(collection.table['sub']['sub'] == 'Hello world.') + + assert(collection:equals({1, 2, 3, 4, 5}) == false) + + assert(collection:equals({ + 1, 2, 3, + [97] = 97, [98] = 98, [99] = 99, key = true, + sub = {1, 2, 3, sub = 'Hello world.'} + }) == true) + + assert(collection:equals({ + 1, 2, 3, 4, + [97] = 97, [98] = 98, [99] = 99, key = true, + sub = {1, 2, 3, sub = 'Hello world.'} + }) == false) + + assert(collect({1, 2, 3, 4, 5}):equals({1, 2, 3, 4, 5}) == true) + + assert(collect({1, 2, 3, 4, 5}):equals({1, 2, 3}) == false) +end + +--[[ filter ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4}):filter(function(key, value) + return value > 2 + end):all(), + {[3] = 3, [4] = 4} + ) + + assert_tables_equal( + collect({1, 2, 3, nil, false, '', 0, {}}):filter():all(), + {1, 2, 3} + ) +end + +--[[ first ]]-- +do + assert(collect({1, 2, 3, 4}):first() == 1) + + assert(collect({1, 2, 3, 4}):first(function(key, value) + return value > 2 + end) == 3) +end + +--[[ flatten ]]-- +do + assert_tables_equal( + collect({name = 'Taylor', languages = {'php', 'javascript', 'lua'} }):flatten():all(), + {'Taylor', 'php', 'javascript', 'lua'} + ) + + assert_tables_equal( + collect({Apple = {name = 'iPhone 6S', brand = 'Apple'}, Samsung = {name = 'Galaxy S7', brand = 'Samsung'} }) + :flatten(1):resort():all(), + { {name = 'iPhone 6S', brand = 'Apple'}, {name = 'Galaxy S7', brand = 'Samsung'} } + ) + + assert_tables_equal( + collect({Apple = {name = 'iPhone 6S', brand = 'Apple'}, Samsung = {name = 'Galaxy S7', brand = 'Samsung'} }) + :flatten(2):resort():all(), + {'iPhone 6S', 'Apple', 'Galaxy S7', 'Samsung'} + ) +end + +--[[ flip ]]-- +do + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}):flip():all(), + {Liam = 'name', Lua = 'language'} + ) +end + +--[[ forget ]]-- +do + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}):forget('language'):all(), + {name = 'Liam'} + ) + +end + +--[[ forPage ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9}):forPage(2, 3):all(), + {4, 5, 6} + ) +end + +--[[ get ]]-- +do + assert(collect({name = 'Liam', language = 'Lua'}):get('name') == 'Liam') + + assert(collect({name = 'Liam', language = 'Lua'}):get('foo', 'Default value') == 'Default value') + + assert(collect({name = 'Liam', language = 'Lua'}):get('foo', function(key) + return '"' .. key .. '" was not found in the collection' + end) == '"foo" was not found in the collection') +end + +--[[ groupBy ]]-- +do + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'}, + {name = 'Taylor', language = 'PHP'} + }):groupBy('language'):all(), + + { + PHP = { + {name = 'Jeffrey', language = 'PHP'}, + {name = 'Taylor', language = 'PHP'} + }, + Lua = { + {name = 'Liam', language = 'Lua'} + } + } + ) +end + +--[[ has ]]-- +do + assert(collect({name = 'Liam', language = 'Lua'}):has('language') == true) +end + +--[[ implode ]]-- +do + assert(collect({'Lua', 'PHP'}):implode() == 'Lua, PHP') + + assert(collect({'Lua', 'PHP'}):implode(' | ') == 'Lua | PHP') + + assert(collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):implode('language') == 'Lua, PHP') + + assert(collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):implode('language', ' | ') == 'Lua | PHP') +end + +--[[ intersect ]]-- +do + assert_tables_equal( + collect({'Desk', 'Sofa', 'Chair'}) + :intersect({'Desk', 'Chair', 'Bookcase'}) + :all(), + {[1] = 'Desk', [3] = 'Chair'} + ) +end + +--[[ isAssociative ]]-- +do + assert(collect({1, 2, 3, 4, 5}):isAssociative() == false) + + assert(collect({name = 'Liam', language = 'Lua'}):isAssociative() == true) +end + +--[[ isEmpty ]]-- +do + assert(collect({'Desk', 'Sofa', 'Chair'}):isEmpty() == false) + + assert(collect():isEmpty() == true) +end + +--[[ isNotEmpty ]]-- +do + assert(collect({'Desk', 'Sofa', 'Chair'}):isNotEmpty() == true) + + assert(collect():isNotEmpty() == false) +end + +--[[ keyBy ]]-- +do + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):keyBy('language'):all(), + { + Lua = {name = 'Liam', language = 'Lua'}, + PHP = {name = 'Jeffrey', language = 'PHP'} + } + ) + + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):keyBy(function(key, value) + return value['language']:lower() + end):all(), + { + lua = {name = 'Liam', language = 'Lua'}, + php = {name = 'Jeffrey', language = 'PHP'} + } + ) +end + +--[[ keys ]]-- +do + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}):keys():all(), + {'name', 'language'} + ) +end + +--[[ last ]]-- +do + assert(collect({1, 2, 3, 4}):last() == 4) + + assert(collect({1, 2, 3, 4}):last(function(key, value) + return value > 2 + end) == 4) +end + +--[[ map ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):map(function(key, value) + return key, value * 2 + end):all(), + {2, 4, 6, 8, 10} + ) +end + +--[[ mapWithKeys ]]-- +do + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):mapWithKeys(function(key, value) + return value['language'], value['name'] + end):all(), + { + Lua = 'Liam', + PHP = 'Jeffrey' + } + ) +end + +--[[ max ]]-- +do + assert(collect({1, 2, 3, 4, 5}):max() == 5) + + assert(collect({ {foo = 10}, {foo = 20} }):max('foo') == 20) +end + +--[[ median ]]-- +do + assert(collect({1, 1, 2, 4}):median() == 1.5) + + assert(collect({ {foo = 10}, {foo = 10}, {foo = 20}, {foo = 40} }):median('foo') == 15) +end + +--[[ merge ]]-- +do + assert_tables_equal( + collect({'Desk', 'Chair'}):merge({'Bookcase', 'Door'}):all(), + {'Desk', 'Chair', 'Bookcase', 'Door'} + ) + + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}) + :merge({name = 'Taylor', experiencedYears = 14 }) + :all(), + {name = 'Taylor', language = 'Lua', experiencedYears = 14} + ) +end + +--[[ min ]]-- +do + assert(collect({1, 2, 3, 4, 5}):min() == 1) + + assert(collect({ {foo = 10}, {foo = 20} }):min('foo') == 10) +end + +--[[ mode ]]-- +do + assert_tables_equal( + collect({1, 1, 2, 4}):mode():all(), + {1} + ) + + assert_tables_equal( + collect({ {foo = 10}, {foo = 10}, {foo = 20}, {foo = 20}, {foo = 40} }) + :mode('foo') + :all(), + {10, 20} + ) +end + +--[[ new ]]-- +do + assert_tables_equal( + Collection:new({'Hello', 'world'}):all(), + {'Hello', 'world'} + ) +end + +--[[ nth ]]-- +do + assert_tables_equal( + collect({'a', 'b', 'c', 'd', 'e', 'f'}):nth(4):all(), + {'a', 'e'} + ) + + assert_tables_equal( + collect({'a', 'b', 'c', 'd', 'e', 'f'}):nth(4, 1):all(), + {'b', 'f'} + ) +end + +--[[ only ]]-- +do + assert_tables_equal( + collect({name = 'Taylor', language = 'Lua', experiencedYears = 14}) + :only({'name', 'experiencedYears'}) + :all(), + {name = 'Taylor', experiencedYears = 14} + ) +end + +--[[ partition ]]-- +do + local passed, failed = collect({1, 2, 3, 4, 5, 6}):partition(function(key, value) + return value < 3 + end) + + assert_tables_equal( + passed:all(), + {1, 2} + ) + + assert_tables_equal( + failed:all(), + {3, 4, 5, 6} + ) +end + +--[[ pipe ]]-- +do + assert(collect({1, 2, 3}):pipe(function(collection) + return collection:sum() + end) == 6) +end + +--[[ pluck ]]-- +do + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):pluck('name'):all(), + {'Liam', 'Jeffrey'} + ) + + assert_tables_equal( + collect({ + {name = 'Liam', language = 'Lua'}, + {name = 'Jeffrey', language = 'PHP'} + }):pluck('name', 'language'):all(), + {Lua = 'Liam', PHP = 'Jeffrey'} + ) +end + +--[[ pop ]]-- +do + local collection = collect({1, 2, 3, 4, 5}) + + assert(collection:pop() == 5) + + assert_tables_equal( + collection:all(), + {1, 2, 3, 4} + ) +end + +--[[ prepend ]]-- +do + local collection = collect({1, 2, 3, 4, 5}) + collection:prepend(0) + + assert_tables_equal( + collection:all(), + {0, 1, 2, 3, 4, 5} + ) +end + +--[[ pull ]]-- +do + local collection = collect({name = 'Liam', language = 'Lua'}) + + assert(collection:pull('language') == 'Lua') + + assert_tables_equal( + collection:all(), + {name = 'Liam'} + ) +end + +--[[ put ]]-- +do + assert_tables_equal( + collect({name = 'Liam', language = 'Lua'}) + :put('count', 12) + :all(), + {name = 'Liam', language = 'Lua', count = 12} + ) +end + +--[[ random ]]-- +do + assert(type(collect({1, 2, 3, 4, 5}):random():first()) == 'number') + + assert(type(collect({1, 2, 3, 4, 5}):random(3):all()) == 'table') + + assert(collect({1, 2, 3, 4, 5}):random(12):count() == 5) + + assert(collect({1, 2, 3, 4, 5}):random(12, true):count() == 12) +end + +--[[ reduce ]]-- +do + assert(collect({1, 2, 3}):reduce(function(carry, value) + return carry + value + end, 4) == 10) +end + +--[[ reject ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4}):reject(function(key, value) + return value > 2 + end):all(), + {1, 2} + ) +end + +--[[ resort ]]-- +do + assert_tables_equal( + collect({[1] = 'a', [5] = 'b'}):resort():all(), + {[1] = 'a', [2] = 'b'} + ) +end + +--[[ reverse ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):reverse():all(), + {5, 4, 3, 2, 1} + ) +end + +--[[ search ]]-- +do + assert(collect({2, 4, 6, 8}):search(4) == 2) + + assert(collect({2, 4, 6, 8}):search(function(key, value) + return value > 5 + end) == 3) +end + +--[[ shift ]]-- +do + local collection = collect({1, 2, 3, 4, 5}) + + assert(collection:shift() == 1) + + assert_tables_equal( + collection:all(), + {2, 3, 4, 5} + ) + +end + +--[[ shuffle ]]-- +do + assert(type(collect({1, 2, 3, 4, 5}):shuffle():all()) == 'table') + + assert(collect({1, 2, 3, 4, 5}):shuffle():count() == 5) +end + +--[[ slice ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}):slice(4):all(), + {5, 6, 7, 8, 9, 10} + ) + + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}):slice(4, 2):all(), + {5, 6} + ) +end + +--[[ sort ]]-- +do + assert_tables_equal( + collect({5, 3, 1, 2, 4}):sort():all(), + {1, 2, 3, 4, 5} + ) + + assert_tables_equal( + collect({ + {application = 'Google +', users = 12}, + {application = 'Facebook', users = 593}, + {application = 'MySpace', users = 62} + }):sort('users'):all(), + { + {application = 'Google +', users = 12}, + {application = 'MySpace', users = 62}, + {application = 'Facebook', users = 593} + } + ) + + assert_tables_equal( + collect({ + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + }):sort(function(a, b) + return #a['colors'] < #b['colors'] + end):all(), + { + {name = 'Chair', colors = {'Black'}}, + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + } + ) +end + +--[[ sortDesc ]]-- +do + assert_tables_equal( + collect({5, 3, 1, 2, 4}):sortDesc():all(), + {5, 4, 3, 2, 1} + ) + + assert_tables_equal( + collect({ + {application = 'Google +', users = 12}, + {application = 'Facebook', users = 593}, + {application = 'MySpace', users = 62} + }):sortDesc('users'):all(), + { + {application = 'Facebook', users = 593}, + {application = 'MySpace', users = 62}, + {application = 'Google +', users = 12} + } + ) + + assert_tables_equal( + collect({ + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + }):sortDesc(function(a, b) + return #a['colors'] < #b['colors'] + end):all(), + { + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}}, + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}} + } + ) +end + +--[[ splice ]]-- +do + local collection1 = collect({1, 2, 3, 4, 5}) + + assert_tables_equal( + collection1:splice(2):all(), + {3, 4, 5} + ) + + assert_tables_equal( + collection1:all(), + {1, 2} + ) + + + + local collection2 = collect({1, 2, 3, 4, 5}) + + assert_tables_equal( + collection2:splice(2, 2):all(), + {3, 4} + ) + + assert_tables_equal( + collection2:all(), + {1, 2, 5} + ) + + + + local collection3 = collect({1, 2, 3, 4, 5}) + + assert_tables_equal( + collection3:splice(2, 2, {'c', 'd'}):all(), + {3, 4} + ) + + assert_tables_equal( + collection3:all(), + {1, 2, 'c', 'd', 5} + ) +end + +--[[ split ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):split(3):all(), + { {1, 2}, {3, 4}, {5} } + ) + + assert_tables_equal( + collect({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}):split(3):all(), + { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10} } + ) +end + +--[[ sum ]]-- +do + assert(collect({1, 2, 3, 4, 5}):sum() == 15) + + assert(collect({ {pages = 176}, {pages = 1096} }):sum('pages') == 1272) +end + +--[[ take ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):take(2):all(), + {1, 2} + ) + + assert_tables_equal( + collect({1, 2, 3, 4, 5}):take(-2):all(), + {4, 5} + ) +end + +--[[ tap ]]-- +do + local count + collect({1, 2, 3, 4, 5}):tap(function(collection) + count = collection:count() + end) + assert(count == 5) +end + +--[[ times ]]-- +do + assert_tables_equal( + Collection():times(10, function(count) + return count * 9 + end):all(), + {9, 18, 27, 36, 45, 54, 63, 72, 81, 90} + ) +end + +--[[ toJSON ]]-- +do + local jsonString = collect({ + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + }):toJSON() + + assert(jsonString == '[{"name":"Desk","colors":["Black","Mahogany"]},{"name":"Chair","colors":["Black"]},{"name":"Bookcase","colors":["Red","Beige","Brown"]}]') +end + +--[[ toString ]]-- +do + local tableString = collect({ + {name = 'Desk', colors = {'Black', 'Mahogany'}}, + {name = 'Chair', colors = {'Black'}}, + {name = 'Bookcase', colors = {'Red', 'Beige', 'Brown'}} + }):toString() + + assert(tableString == '{[1]={name="Desk",colors={[1]="Black",[2]="Mahogany"}},[2]={name="Chair",colors={[1]="Black"}},[3]={name="Bookcase",colors={[1]="Red",[2]="Beige",[3]="Brown"}}}') + +end + +--[[ toTable ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):toTable(), + {1, 2, 3, 4, 5} + ) +end + +--[[ transform ]]-- +do + assert_tables_equal( + collect({1, 2, 3, 4, 5}):transform(function(key, value) + return value * 2 + end):all(), + {2, 4, 6, 8, 10} + ) +end + +--[[ union ]]-- +do + assert_tables_equal( + collect({a = 'Hello', b = 'Goodbye'}) + :union({a = 'Howdy', c = 'Pleasure to meet you'}) + :all(), + {a = 'Hello', b = 'Goodbye', c = 'Pleasure to meet you'} + ) +end + +--[[ unique ]]-- +do + assert_tables_equal( + collect({1, 1, 2, 2, 3, 4, 2}):unique():all(), + {3, 4} + ) + + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'}, + {name = 'Pixel', brand = 'Google', type = 'phone'} + }):unique('brand'):all(), + { + {name = 'Pixel', brand = 'Google', type = 'phone'} + } + ) + + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'}, + {name = 'Pixel', brand = 'Google', type = 'phone'} + }):unique(function(key, value) + return value['brand'] .. value['type'] + end):all(), + { + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Pixel', brand = 'Google', type = 'phone'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'} + } + ) +end + +--[[ when ]]-- +do + assert_tables_equal( + collect({1, 2, 3}):when(true, function(collection) + return collection:push(4) + end):all(), + {1, 2, 3, 4} + ) +end + +--[[ where ]]-- +do + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + }):where('type', 'watch'):all(), + { + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + } + ) +end + +--[[ whereIn ]]-- +do + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + }):whereIn('name', {'iPhone 6', 'iPhone 5', 'Galaxy S6'}):all(), + { + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'} + } + ) +end + +--[[ whereNotIn ]]-- +do + assert_tables_equal( + collect({ + {name = 'iPhone 6', brand = 'Apple', type = 'phone'}, + {name = 'iPhone 5', brand = 'Apple', type = 'phone'}, + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy S6', brand = 'Samsung', type = 'phone'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + }):whereNotIn('name', {'iPhone 6', 'iPhone 5', 'Galaxy S6'}):all(), + { + {name = 'Apple Watch', brand = 'Apple', type = 'watch'}, + {name = 'Galaxy Gear', brand = 'Samsung', type = 'watch'} + } + ) +end + +--[[ zip ]]-- +do + assert_tables_equal( + collect({'Chair', 'Desk'}):zip({100, 200}):all(), + { {'Chair', 100}, {'Desk', 200} } + ) + + assert_tables_equal( + collect({'Chair', 'Desk'}):zip({100, 200, 300}):all(), + { {'Chair', 100}, {'Desk', 200} } + ) +end + +print('All tests passed.') diff --git a/common/edit.lua b/common/edit.lua index a7d0b02..bfc157a 100644 --- a/common/edit.lua +++ b/common/edit.lua @@ -362,29 +362,21 @@ local page = UI.Page { event = 'slide_hide', }, apply_filter = function(self, filter) - local t = { } if filter then filter = filter:lower() self.grid.sortColumn = 'score' - self.grid.inverseSort = true - for _,v in pairs(self.listing) do - v.score = fuzzy(v.lname, filter) - if v.score then - _insert(t, v) - end + for _,v in pairs(self.grid.values) do + v.score = -fuzzy(v.lname, filter) end else self.grid.sortColumn = 'lname' - self.grid.inverseSort = false - t = self.listing end - self.grid:setValues(t) + self.grid:update() self.grid:setIndex(1) end, show = function(self) - local listing = { } local function recurse(dir) local files = fs.list(dir) for _,f in ipairs(files) do @@ -392,7 +384,7 @@ local page = UI.Page { if fs.native.isDir(fullName) then -- skip virtual dirs if f ~= '.git' then recurse(fullName) end else - _insert(listing, { + _insert(self.grid.values, { name = f, dir = dir, lname = f:lower(), @@ -402,7 +394,6 @@ local page = UI.Page { end end recurse('') - self.listing = listing self:apply_filter() self.filter_entry:reset() UI.SlideOut.show(self) @@ -1015,44 +1006,40 @@ actions = { actions.error('open available with multishell') return end + local routine = { + focused = true, + title = fs.getName(fileInfo.path), + chainExit = function(_, result) + -- display results of process before closing window + if result then -- clean exit + -- any errors will be picked up by multishells + -- error handling + print('Press enter to exit') + while true do + local e, code = os.pullEventRaw('key') + if e == 'terminate' or e == 'key' and code == keys.enter then + break + end + end + end + end, + } if undo.chain[#undo.chain] == lastSave then - local nTask = shell.openTab(fileInfo.path) - if nTask then - shell.switchTab(nTask) - else - actions.error("error starting Task") - end + routine.path = 'sys/apps/shell.lua' + routine.args = { fileInfo.path } else local fn, msg = load(_concat(tLines, '\n'), fileInfo.path) - if fn then - multishell.openTab(_ENV, { - fn = fn, - focused = true, - title = fs.getName(fileInfo.path), - chainExit = function(_, result) - -- display results of process before - -- closing window - if result then -- clean exit - -- any errors will be picked up by multishells - -- error handling - print('Press enter to exit') - while true do - local e, code = os.pullEventRaw('key') - if e == 'terminate' or e == 'key' and code == keys.enter then - break - end - end - end - end, - }) - else + if not fn then local ln = msg:match(':(%d+):') if ln and tonumber(ln) then actions.go_to(1, tonumber(ln)) end actions.error(msg) + return end + routine.fn = fn end + multishell.openTab(_ENV, routine) end, status = function() diff --git a/common/etc/fstab b/common/etc/fstab index e636640..00b921e 100644 --- a/common/etc/fstab +++ b/common/etc/fstab @@ -3,4 +3,5 @@ packages/common/hexedit.lua urlfs https://pastebin.com/raw/Ds9ajsp4 packages/common/colors.lua urlfs https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/ignore/colors.lua packages/common/cowsay.lua urlfs https://pastebin.com/raw/n00VQJsw packages/common/calc.lua urlfs https://pastebin.com/raw/nAinUn1h -packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv \ No newline at end of file +packages/common/write.lua urlfs https://pastebin.com/raw/RSyhCjqv +packages/common/apis/debugger.lua urlfs https://raw.githubusercontent.com/slembcke/debugger.lua/master/debugger.lua \ No newline at end of file diff --git a/lfs/lfs.lua b/lfs/apis/init.lua similarity index 100% rename from lfs/lfs.lua rename to lfs/apis/init.lua diff --git a/lfs/etc/fstab b/lfs/etc/fstab index 125e42a..ec3b32c 100644 --- a/lfs/etc/fstab +++ b/lfs/etc/fstab @@ -1 +1 @@ -rom/modules/main/lfs.lua linkfs /packages/lfs/lfs.lua \ No newline at end of file +packages/lfs/tests/test.lua urlfs https://raw.githubusercontent.com/keplerproject/luafilesystem/master/tests/test.lua \ No newline at end of file diff --git a/milo/MiloRemote.lua b/milo/MiloRemote.lua index 1f2caa8..fda2876 100644 --- a/milo/MiloRemote.lua +++ b/milo/MiloRemote.lua @@ -318,7 +318,7 @@ function page:applyFilter() v.score = fuzzy(v.lname, filter) if v.score then if v.count > 0 then - v.score = v.score + 1 + v.score = v.score + .2 end table.insert(r, v) end diff --git a/milo/core/listing.lua b/milo/core/listing.lua index eefff37..945c194 100644 --- a/milo/core/listing.lua +++ b/milo/core/listing.lua @@ -390,7 +390,7 @@ function page:applyFilter() v.score = fuzzy(v.lname, filter) if v.score then if v.count > 0 then - v.score = v.score + 1 + v.score = v.score + .2 end table.insert(r, v) end diff --git a/penlight/etc/fstab b/penlight/etc/fstab index ca81d64..2a1632b 100644 --- a/penlight/etc/fstab +++ b/penlight/etc/fstab @@ -1 +1 @@ -rom/modules/main/pl gitfs Tieske/Penlight/master/lua/pl \ No newline at end of file +#rom/modules/main/pl gitfs Tieske/Penlight/master/lua/pl \ No newline at end of file