diff --git a/modules/table/table.lua b/modules/table/table.lua new file mode 100644 index 0000000..8485b4a --- /dev/null +++ b/modules/table/table.lua @@ -0,0 +1,957 @@ +local tbl = {} + +-- ====================================================================== +-- BUILT-IN TABLE FUNCTIONS (Lua 5.1 wrappers for consistency) +-- ====================================================================== + +function tbl.insert(t, pos, value) + if type(t) ~= "table" then error("tbl.insert: first argument must be a table", 2) end + + if value == nil then + -- table.insert(t, value) form + table.insert(t, pos) + else + -- table.insert(t, pos, value) form + if type(pos) ~= "number" or pos ~= math.floor(pos) then + error("tbl.insert: position must be an integer", 2) + end + table.insert(t, pos, value) + end +end + +function tbl.remove(t, pos) + if type(t) ~= "table" then error("tbl.remove: first argument must be a table", 2) end + if pos ~= nil and (type(pos) ~= "number" or pos ~= math.floor(pos)) then + error("tbl.remove: position must be an integer", 2) + end + return table.remove(t, pos) +end + +function tbl.concat(t, sep, start_idx, end_idx) + if type(t) ~= "table" then error("tbl.concat: first argument must be a table", 2) end + if sep ~= nil and type(sep) ~= "string" then error("tbl.concat: separator must be a string", 2) end + if start_idx ~= nil and (type(start_idx) ~= "number" or start_idx ~= math.floor(start_idx)) then + error("tbl.concat: start index must be an integer", 2) + end + if end_idx ~= nil and (type(end_idx) ~= "number" or end_idx ~= math.floor(end_idx)) then + error("tbl.concat: end index must be an integer", 2) + end + return table.concat(t, sep, start_idx, end_idx) +end + +function tbl.sort(t, comp) + if type(t) ~= "table" then error("tbl.sort: first argument must be a table", 2) end + if comp ~= nil and type(comp) ~= "function" then error("tbl.sort: comparator must be a function", 2) end + table.sort(t, comp) +end + +-- ====================================================================== +-- BASIC TABLE OPERATIONS +-- ====================================================================== + +function tbl.length(t) + if type(t) ~= "table" then error("tbl.length: argument must be a table", 2) end + return #t +end + +function tbl.size(t) + if type(t) ~= "table" then error("tbl.size: argument must be a table", 2) end + local count = 0 + for _ in pairs(t) do + count = count + 1 + end + return count +end + +function tbl.is_empty(t) + if type(t) ~= "table" then error("tbl.is_empty: argument must be a table", 2) end + return next(t) == nil +end + +function tbl.is_array(t) + if type(t) ~= "table" then error("tbl.is_array: argument must be a table", 2) end + if tbl.is_empty(t) then return true end + + local max_index = 0 + local count = 0 + for k, v in pairs(t) do + if type(k) ~= "number" or k ~= math.floor(k) or k <= 0 then + return false + end + max_index = math.max(max_index, k) + count = count + 1 + end + return max_index == count +end + +function tbl.clear(t) + if type(t) ~= "table" then error("tbl.clear: argument must be a table", 2) end + for k in pairs(t) do + t[k] = nil + end +end + +function tbl.clone(t) + if type(t) ~= "table" then error("tbl.clone: argument must be a table", 2) end + local result = {} + for k, v in pairs(t) do + result[k] = v + end + return result +end + +function tbl.deep_copy(t) + if type(t) ~= "table" then error("tbl.deep_copy: argument must be a table", 2) end + + local function copy_recursive(obj, seen) + if type(obj) ~= "table" then return obj end + if seen[obj] then return seen[obj] end + + local result = {} + seen[obj] = result + + for k, v in pairs(obj) do + result[copy_recursive(k, seen)] = copy_recursive(v, seen) + end + + return result + end + + return copy_recursive(t, {}) +end + +-- ====================================================================== +-- SEARCHING AND FINDING +-- ====================================================================== + +function tbl.contains(t, value) + if type(t) ~= "table" then error("tbl.contains: first argument must be a table", 2) end + for _, v in pairs(t) do + if v == value then return true end + end + return false +end + +function tbl.index_of(t, value) + if type(t) ~= "table" then error("tbl.index_of: first argument must be a table", 2) end + for k, v in pairs(t) do + if v == value then return k end + end + return nil +end + +function tbl.find(t, predicate) + if type(t) ~= "table" then error("tbl.find: first argument must be a table", 2) end + if type(predicate) ~= "function" then error("tbl.find: second argument must be a function", 2) end + + for k, v in pairs(t) do + if predicate(v, k, t) then return v, k end + end + return nil +end + +function tbl.find_index(t, predicate) + if type(t) ~= "table" then error("tbl.find_index: first argument must be a table", 2) end + if type(predicate) ~= "function" then error("tbl.find_index: second argument must be a function", 2) end + + for k, v in pairs(t) do + if predicate(v, k, t) then return k end + end + return nil +end + +function tbl.count(t, value_or_predicate) + if type(t) ~= "table" then error("tbl.count: first argument must be a table", 2) end + + local count = 0 + if type(value_or_predicate) == "function" then + for k, v in pairs(t) do + if value_or_predicate(v, k, t) then count = count + 1 end + end + else + for _, v in pairs(t) do + if v == value_or_predicate then count = count + 1 end + end + end + return count +end + +-- ====================================================================== +-- FILTERING AND MAPPING +-- ====================================================================== + +function tbl.filter(t, predicate) + if type(t) ~= "table" then error("tbl.filter: first argument must be a table", 2) end + if type(predicate) ~= "function" then error("tbl.filter: second argument must be a function", 2) end + + local result = {} + if tbl.is_array(t) then + for i, v in ipairs(t) do + if predicate(v, i, t) then + table.insert(result, v) + end + end + else + for k, v in pairs(t) do + if predicate(v, k, t) then + result[k] = v + end + end + end + return result +end + +function tbl.reject(t, predicate) + if type(t) ~= "table" then error("tbl.reject: first argument must be a table", 2) end + if type(predicate) ~= "function" then error("tbl.reject: second argument must be a function", 2) end + + return tbl.filter(t, function(v, k, tbl) return not predicate(v, k, tbl) end) +end + +function tbl.map(t, transformer) + if type(t) ~= "table" then error("tbl.map: first argument must be a table", 2) end + if type(transformer) ~= "function" then error("tbl.map: second argument must be a function", 2) end + + local result = {} + for k, v in pairs(t) do + result[k] = transformer(v, k, t) + end + return result +end + +function tbl.map_values(t, transformer) + if type(t) ~= "table" then error("tbl.map_values: first argument must be a table", 2) end + if type(transformer) ~= "function" then error("tbl.map_values: second argument must be a function", 2) end + + local result = {} + for k, v in pairs(t) do + result[k] = transformer(v, k, t) + end + return result +end + +function tbl.map_keys(t, transformer) + if type(t) ~= "table" then error("tbl.map_keys: first argument must be a table", 2) end + if type(transformer) ~= "function" then error("tbl.map_keys: second argument must be a function", 2) end + + local result = {} + for k, v in pairs(t) do + local new_key = transformer(k, v, t) + result[new_key] = v + end + return result +end + +-- ====================================================================== +-- REDUCING AND AGGREGATING +-- ====================================================================== + +function tbl.reduce(t, reducer, initial) + if type(t) ~= "table" then error("tbl.reduce: first argument must be a table", 2) end + if type(reducer) ~= "function" then error("tbl.reduce: second argument must be a function", 2) end + + local accumulator = initial + local started = initial ~= nil + + for k, v in pairs(t) do + if not started then + accumulator = v + started = true + else + accumulator = reducer(accumulator, v, k, t) + end + end + + if not started then + error("tbl.reduce: empty table with no initial value", 2) + end + + return accumulator +end + +function tbl.sum(t) + if type(t) ~= "table" then error("tbl.sum: argument must be a table", 2) end + local total = 0 + for _, v in pairs(t) do + if type(v) ~= "number" then error("tbl.sum: all values must be numbers", 2) end + total = total + v + end + return total +end + +function tbl.product(t) + if type(t) ~= "table" then error("tbl.product: argument must be a table", 2) end + local result = 1 + for _, v in pairs(t) do + if type(v) ~= "number" then error("tbl.product: all values must be numbers", 2) end + result = result * v + end + return result +end + +function tbl.min(t) + if type(t) ~= "table" then error("tbl.min: argument must be a table", 2) end + if tbl.is_empty(t) then error("tbl.min: table is empty", 2) end + + local min_val = nil + for _, v in pairs(t) do + if type(v) ~= "number" then error("tbl.min: all values must be numbers", 2) end + if min_val == nil or v < min_val then + min_val = v + end + end + return min_val +end + +function tbl.max(t) + if type(t) ~= "table" then error("tbl.max: argument must be a table", 2) end + if tbl.is_empty(t) then error("tbl.max: table is empty", 2) end + + local max_val = nil + for _, v in pairs(t) do + if type(v) ~= "number" then error("tbl.max: all values must be numbers", 2) end + if max_val == nil or v > max_val then + max_val = v + end + end + return max_val +end + +function tbl.average(t) + if type(t) ~= "table" then error("tbl.average: argument must be a table", 2) end + if tbl.is_empty(t) then error("tbl.average: table is empty", 2) end + return tbl.sum(t) / tbl.size(t) +end + +-- ====================================================================== +-- BOOLEAN OPERATIONS +-- ====================================================================== + +function tbl.all(t, predicate) + if type(t) ~= "table" then error("tbl.all: first argument must be a table", 2) end + + if predicate then + if type(predicate) ~= "function" then error("tbl.all: second argument must be a function", 2) end + for k, v in pairs(t) do + if not predicate(v, k, t) then return false end + end + else + for _, v in pairs(t) do + if not v then return false end + end + end + return true +end + +function tbl.any(t, predicate) + if type(t) ~= "table" then error("tbl.any: first argument must be a table", 2) end + + if predicate then + if type(predicate) ~= "function" then error("tbl.any: second argument must be a function", 2) end + for k, v in pairs(t) do + if predicate(v, k, t) then return true end + end + else + for _, v in pairs(t) do + if v then return true end + end + end + return false +end + +function tbl.none(t, predicate) + if type(t) ~= "table" then error("tbl.none: first argument must be a table", 2) end + return not tbl.any(t, predicate) +end + +-- ====================================================================== +-- SET OPERATIONS +-- ====================================================================== + +function tbl.unique(t) + if type(t) ~= "table" then error("tbl.unique: argument must be a table", 2) end + + local seen = {} + local result = {} + + if tbl.is_array(t) then + for _, v in ipairs(t) do + if not seen[v] then + seen[v] = true + table.insert(result, v) + end + end + else + for k, v in pairs(t) do + if not seen[v] then + seen[v] = true + result[k] = v + end + end + end + + return result +end + +function tbl.intersection(t1, t2) + if type(t1) ~= "table" then error("tbl.intersection: first argument must be a table", 2) end + if type(t2) ~= "table" then error("tbl.intersection: second argument must be a table", 2) end + + local set2 = {} + for _, v in pairs(t2) do + set2[v] = true + end + + local result = {} + if tbl.is_array(t1) then + for _, v in ipairs(t1) do + if set2[v] then + table.insert(result, v) + end + end + else + for k, v in pairs(t1) do + if set2[v] then + result[k] = v + end + end + end + + return result +end + +function tbl.union(t1, t2) + if type(t1) ~= "table" then error("tbl.union: first argument must be a table", 2) end + if type(t2) ~= "table" then error("tbl.union: second argument must be a table", 2) end + + local result = tbl.clone(t1) + + if tbl.is_array(t1) and tbl.is_array(t2) then + for _, v in ipairs(t2) do + if not tbl.contains(result, v) then + table.insert(result, v) + end + end + else + for k, v in pairs(t2) do + if result[k] == nil then + result[k] = v + end + end + end + + return result +end + +function tbl.difference(t1, t2) + if type(t1) ~= "table" then error("tbl.difference: first argument must be a table", 2) end + if type(t2) ~= "table" then error("tbl.difference: second argument must be a table", 2) end + + local set2 = {} + for _, v in pairs(t2) do + set2[v] = true + end + + return tbl.filter(t1, function(v) return not set2[v] end) +end + +-- ====================================================================== +-- ARRAY OPERATIONS +-- ====================================================================== + +function tbl.reverse(t) + if type(t) ~= "table" then error("tbl.reverse: argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.reverse: argument must be an array", 2) end + + local result = {} + local len = #t + for i = 1, len do + result[i] = t[len - i + 1] + end + return result +end + +function tbl.shuffle(t) + if type(t) ~= "table" then error("tbl.shuffle: argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.shuffle: argument must be an array", 2) end + + local result = tbl.clone(t) + local len = #result + + math.randomseed(os.time() + os.clock() * 1000000) + + for i = len, 2, -1 do + local j = math.random(1, i) + result[i], result[j] = result[j], result[i] + end + + return result +end + +function tbl.rotate(t, positions) + if type(t) ~= "table" then error("tbl.rotate: first argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.rotate: first argument must be an array", 2) end + if type(positions) ~= "number" or positions ~= math.floor(positions) then + error("tbl.rotate: second argument must be an integer", 2) + end + + local len = #t + if len == 0 then return {} end + + positions = positions % len + if positions == 0 then return tbl.clone(t) end + + local result = {} + for i = 1, len do + local new_pos = ((i - 1 + positions) % len) + 1 + result[new_pos] = t[i] + end + + return result +end + +function tbl.slice(t, start_idx, end_idx) + if type(t) ~= "table" then error("tbl.slice: first argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.slice: first argument must be an array", 2) end + if type(start_idx) ~= "number" or start_idx ~= math.floor(start_idx) then + error("tbl.slice: start index must be an integer", 2) + end + if end_idx ~= nil and (type(end_idx) ~= "number" or end_idx ~= math.floor(end_idx)) then + error("tbl.slice: end index must be an integer", 2) + end + + local len = #t + if start_idx < 0 then start_idx = len + start_idx + 1 end + if end_idx and end_idx < 0 then end_idx = len + end_idx + 1 end + + start_idx = math.max(1, math.min(start_idx, len)) + end_idx = end_idx and math.max(1, math.min(end_idx, len)) or len + + local result = {} + for i = start_idx, end_idx do + table.insert(result, t[i]) + end + + return result +end + +function tbl.splice(t, start_idx, delete_count, ...) + if type(t) ~= "table" then error("tbl.splice: first argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.splice: first argument must be an array", 2) end + if type(start_idx) ~= "number" or start_idx ~= math.floor(start_idx) then + error("tbl.splice: start index must be an integer", 2) + end + if delete_count ~= nil and (type(delete_count) ~= "number" or delete_count ~= math.floor(delete_count) or delete_count < 0) then + error("tbl.splice: delete count must be a non-negative integer", 2) + end + + local len = #t + if start_idx < 0 then start_idx = len + start_idx + 1 end + start_idx = math.max(1, math.min(start_idx, len + 1)) + + delete_count = delete_count or (len - start_idx + 1) + delete_count = math.max(0, math.min(delete_count, len - start_idx + 1)) + + local deleted = {} + for i = 1, delete_count do + deleted[i] = t[start_idx + i - 1] + end + + local insert_items = {...} + local insert_count = #insert_items + local move_count = len - start_idx - delete_count + 1 + + -- Shift elements + if insert_count > delete_count then + -- Moving right + for i = len, start_idx + delete_count, -1 do + t[i + insert_count - delete_count] = t[i] + end + elseif insert_count < delete_count then + -- Moving left + for i = start_idx + delete_count, len do + t[i + insert_count - delete_count] = t[i] + end + -- Clear the end + for i = len + insert_count - delete_count + 1, len do + t[i] = nil + end + end + + -- Insert new items + for i = 1, insert_count do + t[start_idx + i - 1] = insert_items[i] + end + + return deleted +end + +-- ====================================================================== +-- SORTING HELPERS +-- ====================================================================== + +function tbl.sort_by(t, key_func) + if type(t) ~= "table" then error("tbl.sort_by: first argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.sort_by: first argument must be an array", 2) end + if type(key_func) ~= "function" then error("tbl.sort_by: second argument must be a function", 2) end + + local result = tbl.clone(t) + table.sort(result, function(a, b) + return key_func(a) < key_func(b) + end) + return result +end + +function tbl.is_sorted(t, comp) + if type(t) ~= "table" then error("tbl.is_sorted: first argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.is_sorted: first argument must be an array", 2) end + if comp ~= nil and type(comp) ~= "function" then error("tbl.is_sorted: comparator must be a function", 2) end + + comp = comp or function(a, b) return a < b end + + for i = 2, #t do + if comp(t[i], t[i-1]) then + return false + end + end + return true +end + +-- ====================================================================== +-- UTILITY FUNCTIONS +-- ====================================================================== + +function tbl.keys(t) + if type(t) ~= "table" then error("tbl.keys: argument must be a table", 2) end + + local result = {} + for k, _ in pairs(t) do + table.insert(result, k) + end + return result +end + +function tbl.values(t) + if type(t) ~= "table" then error("tbl.values: argument must be a table", 2) end + + local result = {} + for _, v in pairs(t) do + table.insert(result, v) + end + return result +end + +function tbl.pairs(t) + if type(t) ~= "table" then error("tbl.pairs: argument must be a table", 2) end + + local result = {} + for k, v in pairs(t) do + table.insert(result, {k, v}) + end + return result +end + +function tbl.merge(...) + local tables = {...} + if #tables == 0 then return {} end + + for i, t in ipairs(tables) do + if type(t) ~= "table" then + error("tbl.merge: argument " .. i .. " must be a table", 2) + end + end + + local result = {} + for _, t in ipairs(tables) do + for k, v in pairs(t) do + result[k] = v + end + end + return result +end + +function tbl.extend(t1, ...) + if type(t1) ~= "table" then error("tbl.extend: first argument must be a table", 2) end + + local tables = {...} + for i, t in ipairs(tables) do + if type(t) ~= "table" then + error("tbl.extend: argument " .. (i + 1) .. " must be a table", 2) + end + end + + for _, t in ipairs(tables) do + for k, v in pairs(t) do + t1[k] = v + end + end + return t1 +end + +function tbl.invert(t) + if type(t) ~= "table" then error("tbl.invert: argument must be a table", 2) end + + local result = {} + for k, v in pairs(t) do + result[v] = k + end + return result +end + +function tbl.pick(t, ...) + if type(t) ~= "table" then error("tbl.pick: first argument must be a table", 2) end + + local keys = {...} + local result = {} + + for _, key in ipairs(keys) do + if t[key] ~= nil then + result[key] = t[key] + end + end + + return result +end + +function tbl.omit(t, ...) + if type(t) ~= "table" then error("tbl.omit: first argument must be a table", 2) end + + local omit_keys = {} + for _, key in ipairs({...}) do + omit_keys[key] = true + end + + local result = {} + for k, v in pairs(t) do + if not omit_keys[k] then + result[k] = v + end + end + + return result +end + +-- ====================================================================== +-- DEEP OPERATIONS +-- ====================================================================== + +function tbl.deep_equals(t1, t2) + if type(t1) ~= "table" then error("tbl.deep_equals: first argument must be a table", 2) end + if type(t2) ~= "table" then error("tbl.deep_equals: second argument must be a table", 2) end + + local function equals_recursive(a, b, seen) + if a == b then return true end + if type(a) ~= type(b) then return false end + if type(a) ~= "table" then return false end + + local key_pair = tostring(a) .. ":" .. tostring(b) + if seen[key_pair] then return true end + seen[key_pair] = true + + -- Check if they have the same keys + local keys_a, keys_b = {}, {} + for k in pairs(a) do keys_a[k] = true end + for k in pairs(b) do keys_b[k] = true end + + for k in pairs(keys_a) do + if not keys_b[k] then return false end + end + for k in pairs(keys_b) do + if not keys_a[k] then return false end + end + + -- Check if all values are equal + for k in pairs(keys_a) do + if not equals_recursive(a[k], b[k], seen) then + return false + end + end + + return true + end + + return equals_recursive(t1, t2, {}) +end + +function tbl.flatten(t, depth) + if type(t) ~= "table" then error("tbl.flatten: first argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.flatten: first argument must be an array", 2) end + if depth ~= nil and (type(depth) ~= "number" or depth ~= math.floor(depth) or depth < 1) then + error("tbl.flatten: depth must be a positive integer", 2) + end + + depth = depth or math.huge + + local function flatten_recursive(arr, current_depth) + local result = {} + for _, v in ipairs(arr) do + if type(v) == "table" and tbl.is_array(v) and current_depth > 0 then + local flattened = flatten_recursive(v, current_depth - 1) + for _, item in ipairs(flattened) do + table.insert(result, item) + end + else + table.insert(result, v) + end + end + return result + end + + return flatten_recursive(t, depth) +end + +function tbl.deep_merge(...) + local tables = {...} + if #tables == 0 then return {} end + + for i, t in ipairs(tables) do + if type(t) ~= "table" then + error("tbl.deep_merge: argument " .. i .. " must be a table", 2) + end + end + + local function merge_recursive(target, source) + for k, v in pairs(source) do + if type(v) == "table" and type(target[k]) == "table" then + target[k] = merge_recursive(target[k], v) + else + target[k] = tbl.deep_copy(v) + end + end + return target + end + + local result = tbl.deep_copy(tables[1]) + for i = 2, #tables do + result = merge_recursive(result, tables[i]) + end + + return result +end + +-- ====================================================================== +-- ADVANCED OPERATIONS +-- ====================================================================== + +function tbl.chunk(t, size) + if type(t) ~= "table" then error("tbl.chunk: first argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.chunk: first argument must be an array", 2) end + if type(size) ~= "number" or size ~= math.floor(size) or size <= 0 then + error("tbl.chunk: size must be a positive integer", 2) + end + + local result = {} + local len = #t + + for i = 1, len, size do + local chunk = {} + for j = i, math.min(i + size - 1, len) do + table.insert(chunk, t[j]) + end + table.insert(result, chunk) + end + + return result +end + +function tbl.partition(t, predicate) + if type(t) ~= "table" then error("tbl.partition: first argument must be a table", 2) end + if type(predicate) ~= "function" then error("tbl.partition: second argument must be a function", 2) end + + local truthy, falsy = {}, {} + + if tbl.is_array(t) then + for i, v in ipairs(t) do + if predicate(v, i, t) then + table.insert(truthy, v) + else + table.insert(falsy, v) + end + end + else + for k, v in pairs(t) do + if predicate(v, k, t) then + truthy[k] = v + else + falsy[k] = v + end + end + end + + return truthy, falsy +end + +function tbl.group_by(t, key_func) + if type(t) ~= "table" then error("tbl.group_by: first argument must be a table", 2) end + if type(key_func) ~= "function" then error("tbl.group_by: second argument must be a function", 2) end + + local result = {} + + for k, v in pairs(t) do + local group_key = key_func(v, k, t) + if result[group_key] == nil then + result[group_key] = {} + end + + if tbl.is_array(t) then + table.insert(result[group_key], v) + else + result[group_key][k] = v + end + end + + return result +end + +function tbl.zip(...) + local arrays = {...} + if #arrays == 0 then error("tbl.zip: at least one argument required", 2) end + + for i, arr in ipairs(arrays) do + if type(arr) ~= "table" then + error("tbl.zip: argument " .. i .. " must be a table", 2) + end + if not tbl.is_array(arr) then + error("tbl.zip: argument " .. i .. " must be an array", 2) + end + end + + local min_length = #arrays[1] + for i = 2, #arrays do + min_length = math.min(min_length, #arrays[i]) + end + + local result = {} + for i = 1, min_length do + local tuple = {} + for j = 1, #arrays do + table.insert(tuple, arrays[j][i]) + end + table.insert(result, tuple) + end + + return result +end + +function tbl.compact(t) + if type(t) ~= "table" then error("tbl.compact: argument must be a table", 2) end + + return tbl.filter(t, function(v) return v ~= nil and v ~= false end) +end + +function tbl.sample(t, n) + if type(t) ~= "table" then error("tbl.sample: first argument must be a table", 2) end + if not tbl.is_array(t) then error("tbl.sample: first argument must be an array", 2) end + if n ~= nil and (type(n) ~= "number" or n ~= math.floor(n) or n < 0) then + error("tbl.sample: sample size must be a non-negative integer", 2) + end + + n = n or 1 + local len = #t + if n >= len then return tbl.clone(t) end + + local shuffled = tbl.shuffle(t) + return tbl.slice(shuffled, 1, n) +end + +return tbl