diff --git a/modules/crypto/crypto.lua b/modules/crypto/crypto.lua index 35348c4..0178322 100644 --- a/modules/crypto/crypto.lua +++ b/modules/crypto/crypto.lua @@ -1,5 +1,4 @@ --- modules/crypto.lua - Comprehensive cryptographic utilities - +local json = require("json") local crypto = {} -- ====================================================================== @@ -93,16 +92,16 @@ end -- Hash file contents function crypto.hash_file(path, algorithm) algorithm = algorithm or "sha256" - + if not moonshark.file_exists(path) then error("File not found: " .. path) end - + local content = moonshark.file_read(path) if not content then error("Failed to read file: " .. path) end - + if algorithm == "md5" then return crypto.md5(content) elseif algorithm == "sha1" then @@ -189,11 +188,11 @@ end function crypto.random_password(length, include_symbols) length = length or 12 local charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - + if include_symbols then charset = charset .. "!@#$%^&*()_+-=[]{}|;:,.<>?" end - + return crypto.random_string(length, charset) end @@ -228,13 +227,13 @@ end function crypto.xor_encrypt(data, key) local result = {} local key_len = #key - + for i = 1, #data do local data_byte = string.byte(data, i) local key_byte = string.byte(key, ((i - 1) % key_len) + 1) table.insert(result, string.char(bit32 and bit32.bxor(data_byte, key_byte) or bit.bxor(data_byte, key_byte))) end - + return table.concat(result) end @@ -247,7 +246,7 @@ end function crypto.hash_chain(data, iterations, algorithm) iterations = iterations or 1000 algorithm = algorithm or "sha256" - + local result = data for i = 1, iterations do result = crypto[algorithm](result) @@ -260,12 +259,12 @@ function crypto.derive_key(password, salt, iterations, algorithm) iterations = iterations or 10000 algorithm = algorithm or "sha256" salt = salt or crypto.random_hex(16) - + local derived = password .. salt for i = 1, iterations do derived = crypto[algorithm](derived) end - + return derived, salt end @@ -296,7 +295,7 @@ end function crypto.encode_chain(data, formats) formats = formats or {"base64"} local result = data - + for _, format in ipairs(formats) do if format == "base64" then result = crypto.base64_encode(result) @@ -308,7 +307,7 @@ function crypto.encode_chain(data, formats) error("Unknown encoding format: " .. format) end end - + return result end @@ -316,7 +315,7 @@ end function crypto.decode_chain(data, formats) formats = formats or {"base64"} local result = data - + -- Reverse the formats for decoding for i = #formats, 1, -1 do local format = formats[i] @@ -330,7 +329,7 @@ function crypto.decode_chain(data, formats) error("Unknown decoding format: " .. format) end end - + return result end @@ -344,8 +343,7 @@ end -- Create fingerprint from table data function crypto.fingerprint(data, algorithm) algorithm = algorithm or "sha256" - local json = moonshark.json_encode(data) - return crypto[algorithm](json) + return crypto[algorithm](json.encode(data)) end -- Simple data integrity check @@ -363,9 +361,9 @@ function crypto.verify_integrity(check) if not check.data or not check.hash then return false end - + local expected = crypto.sha256(check.data) return crypto.secure_compare(expected, check.hash) end -return crypto \ No newline at end of file +return crypto diff --git a/modules/json/json.lua b/modules/json/json.lua index f69cac5..f60a127 100644 --- a/modules/json/json.lua +++ b/modules/json/json.lua @@ -1,5 +1,3 @@ --- modules/json.lua - High-performance JSON module - local json = {} function json.encode(value) @@ -599,4 +597,4 @@ function json.validate(data, schema) return validate_value(data, schema) end -return json \ No newline at end of file +return json diff --git a/modules/table/table.lua b/modules/table/table.lua index 8485b4a..b039a91 100644 --- a/modules/table/table.lua +++ b/modules/table/table.lua @@ -186,8 +186,15 @@ function tbl.filter(t, predicate) local result = {} if tbl.is_array(t) then - for i, v in ipairs(t) do - if predicate(v, i, t) then + local max_index = 0 + for k in pairs(t) do + if type(k) == "number" and k > max_index then + max_index = k + end + end + for i = 1, max_index do + local v = t[i] + if v ~= nil and predicate(v, i, t) then table.insert(result, v) end end @@ -780,7 +787,7 @@ function tbl.flatten(t, depth) error("tbl.flatten: depth must be a positive integer", 2) end - depth = depth or math.huge + depth = depth or 1 local function flatten_recursive(arr, current_depth) local result = {} @@ -815,7 +822,7 @@ function tbl.deep_merge(...) if type(v) == "table" and type(target[k]) == "table" then target[k] = merge_recursive(target[k], v) else - target[k] = tbl.deep_copy(v) + target[k] = type(v) == "table" and tbl.deep_copy(v) or v end end return target @@ -936,7 +943,31 @@ 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) + -- Check if table has only integer keys (array-like) + local has_only_int_keys = true + local max_key = 0 + for k in pairs(t) do + if type(k) ~= "number" or k ~= math.floor(k) or k <= 0 then + has_only_int_keys = false + break + end + max_key = math.max(max_key, k) + end + + if has_only_int_keys then + -- Treat as array-like and return clean array + local result = {} + for i = 1, max_key do + local v = t[i] + if v ~= nil and v ~= false then + table.insert(result, v) + end + end + return result + else + -- Regular table filtering + return tbl.filter(t, function(v) return v ~= nil and v ~= false end) + end end function tbl.sample(t, n) @@ -954,4 +985,15 @@ function tbl.sample(t, n) return tbl.slice(shuffled, 1, n) end +function tbl.fold(t, folder, initial) + if type(t) ~= "table" then error("tbl.fold: first argument must be a table", 2) end + if type(folder) ~= "function" then error("tbl.fold: second argument must be a function", 2) end + + local accumulator = initial + for k, v in pairs(t) do + accumulator = folder(accumulator, v, k, t) + end + return accumulator +end + return tbl diff --git a/tests/crypto.lua b/tests/crypto.lua index 7fc3c48..ae11693 100644 --- a/tests/crypto.lua +++ b/tests/crypto.lua @@ -13,7 +13,7 @@ test("Base64 Encoding/Decoding", function() local encoded = crypto.base64_encode(test_data) assert_equal(type(encoded), "string") assert(#encoded > 0, "encoded string should not be empty") - + local decoded = crypto.base64_decode(encoded) assert_equal(decoded, test_data) end) @@ -21,7 +21,7 @@ end) test("Base64 URL Encoding/Decoding", function() local encoded = crypto.base64_url_encode(test_data) assert_equal(type(encoded), "string") - + local decoded = crypto.base64_url_decode(encoded) assert_equal(decoded, test_data) end) @@ -30,7 +30,7 @@ test("Hex Encoding/Decoding", function() local encoded = crypto.hex_encode(test_data) assert_equal(type(encoded), "string") assert(encoded:match("^[0-9a-f]+$"), "hex should only contain hex characters") - + local decoded = crypto.hex_decode(encoded) assert_equal(decoded, test_data) end) @@ -50,7 +50,7 @@ test("MD5 Hash", function() assert_equal(type(hash), "string") assert_equal(#hash, 32) -- MD5 is 32 hex characters assert(hash:match("^[0-9a-f]+$"), "hash should be hex") - + -- Same input should produce same hash assert_equal(crypto.md5(test_data), hash) end) @@ -80,7 +80,7 @@ test("Hash Multiple Inputs", function() local hash1 = crypto.hash_multiple({"hello", "world"}) local hash2 = crypto.hash_multiple({"hello", "world"}) local hash3 = crypto.hash_multiple({"world", "hello"}) - + assert_equal(hash1, hash2) assert(hash1 ~= hash3, "different order should produce different hash") end) @@ -107,10 +107,10 @@ test("MAC Functions", function() local mac = crypto.mac(test_data, test_key) assert_equal(type(mac), "string") assert(#mac > 0, "mac should not be empty") - + local valid = crypto.verify_mac(test_data, test_key, mac) assert_equal(valid, true) - + local invalid = crypto.verify_mac("different data", test_key, mac) assert_equal(invalid, false) end) @@ -122,13 +122,13 @@ end) test("UUID Generation", function() local uuid1 = crypto.uuid() local uuid2 = crypto.uuid_v4() - + assert_equal(type(uuid1), "string") assert_equal(type(uuid2), "string") assert_equal(#uuid1, 36) -- Standard UUID length assert_equal(#uuid2, 36) assert(uuid1 ~= uuid2, "UUIDs should be unique") - + assert_equal(crypto.is_uuid(uuid1), true) assert_equal(crypto.is_uuid(uuid2), true) assert_equal(crypto.is_uuid("not-a-uuid"), false) @@ -141,7 +141,7 @@ end) test("Random Bytes", function() local bytes1 = crypto.random_bytes(16) local bytes2 = crypto.random_bytes(16) - + assert_equal(type(bytes1), "string") assert_equal(#bytes1, 16) assert(bytes1 ~= bytes2, "random bytes should be different") @@ -150,7 +150,7 @@ end) test("Random Hex", function() local hex1 = crypto.random_hex(8) local hex2 = crypto.random_hex(8) - + assert_equal(type(hex1), "string") assert_equal(#hex1, 16) -- 8 bytes = 16 hex characters assert(hex1:match("^[0-9a-f]+$"), "should be hex") @@ -160,11 +160,11 @@ end) test("Random String", function() local str1 = crypto.random_string(10) local str2 = crypto.random_string(10) - + assert_equal(type(str1), "string") assert_equal(#str1, 10) assert(str1 ~= str2, "random strings should be different") - + local custom = crypto.random_string(5, "abc") assert_equal(#custom, 5) assert(custom:match("^[abc]+$"), "should only contain specified characters") @@ -179,7 +179,7 @@ end) test("Random Password", function() local pass1 = crypto.random_password(12) local pass2 = crypto.random_password(12, true) -- with symbols - + assert_equal(#pass1, 12) assert_equal(#pass2, 12) assert(pass1 ~= pass2, "passwords should be different") @@ -188,7 +188,7 @@ end) test("Token Generation", function() local token1 = crypto.token(32) local token2 = crypto.token(32) - + assert_equal(#token1, 64) -- 32 bytes = 64 hex characters assert(token1:match("^[0-9a-f]+$"), "token should be hex") assert(token1 ~= token2, "tokens should be unique") @@ -197,7 +197,7 @@ end) test("Nonce Generation", function() local nonce1 = crypto.nonce() local nonce2 = crypto.nonce(32) - + assert_equal(#nonce1, 32) -- default 16 bytes = 32 hex assert_equal(#nonce2, 64) -- 32 bytes = 64 hex assert(nonce1 ~= nonce2, "nonces should be unique") @@ -211,7 +211,7 @@ test("Secure Compare", function() local str1 = "hello" local str2 = "hello" local str3 = "world" - + assert_equal(crypto.secure_compare(str1, str2), true) assert_equal(crypto.secure_compare(str1, str3), false) end) @@ -219,10 +219,10 @@ end) test("Checksum Functions", function() local checksum = crypto.checksum(test_data) assert_equal(type(checksum), "string") - + local valid = crypto.verify_checksum(test_data, checksum) assert_equal(valid, true) - + local invalid = crypto.verify_checksum("different", checksum) assert_equal(invalid, false) end) @@ -231,7 +231,7 @@ test("XOR Encryption", function() local key = "mykey" local encrypted = crypto.xor_encrypt(test_data, key) local decrypted = crypto.xor_decrypt(encrypted, key) - + assert_equal(decrypted, test_data) assert(encrypted ~= test_data, "encrypted should be different") end) @@ -240,7 +240,7 @@ test("Hash Chain", function() local chain1 = crypto.hash_chain(test_data, 100) local chain2 = crypto.hash_chain(test_data, 100) local chain3 = crypto.hash_chain(test_data, 101) - + assert_equal(chain1, chain2) assert(chain1 ~= chain3, "different iterations should produce different results") end) @@ -248,7 +248,7 @@ end) test("Key Derivation", function() local derived1, salt1 = crypto.derive_key("password", "salt", 1000) local derived2, salt2 = crypto.derive_key("password", salt1, 1000) - + assert_equal(derived1, derived2) assert_equal(salt1, salt2) assert_equal(type(derived1), "string") @@ -259,7 +259,7 @@ test("Fingerprint", function() local data = {name = "test", value = 42} local fp1 = crypto.fingerprint(data) local fp2 = crypto.fingerprint(data) - + assert_equal(fp1, fp2) assert_equal(type(fp1), "string") assert(#fp1 > 0, "fingerprint should not be empty") @@ -267,15 +267,15 @@ end) test("Integrity Check", function() local check = crypto.integrity_check(test_data) - + assert_equal(check.data, test_data) assert_equal(type(check.hash), "string") assert_equal(type(check.timestamp), "number") assert_equal(type(check.uuid), "string") - + local valid = crypto.verify_integrity(check) assert_equal(valid, true) - + -- Tamper with data check.data = "tampered" local invalid = crypto.verify_integrity(check) @@ -290,11 +290,11 @@ test("Error Handling", function() -- Invalid base64 local success, err = pcall(crypto.base64_decode, "invalid===base64") assert_equal(success, false) - + -- Invalid hex local success2, err2 = pcall(crypto.hex_decode, "invalid_hex") assert_equal(success2, false) - + -- Invalid UUID validation (returns boolean, doesn't throw) assert_equal(crypto.is_uuid("not-a-uuid"), false) assert_equal(crypto.is_uuid(""), false) @@ -307,26 +307,26 @@ end) test("Performance Test", function() local large_data = string.rep("test data for performance ", 1000) - + local start = os.clock() local hash = crypto.sha256(large_data) local hash_time = os.clock() - start - + start = os.clock() local encoded = crypto.base64_encode(large_data) local encode_time = os.clock() - start - + start = os.clock() local decoded = crypto.base64_decode(encoded) local decode_time = os.clock() - start - + print(string.format(" SHA256 of %d bytes: %.3fs", #large_data, hash_time)) print(string.format(" Base64 encode: %.3fs", encode_time)) print(string.format(" Base64 decode: %.3fs", decode_time)) - + assert_equal(decoded, large_data) assert_equal(type(hash), "string") end) summary() -test_exit() \ No newline at end of file +test_exit() diff --git a/tests/table.lua b/tests/table.lua new file mode 100644 index 0000000..5240d62 --- /dev/null +++ b/tests/table.lua @@ -0,0 +1,888 @@ +require("tests") +local tbl = require("table") + +-- Test data +local simple_array = {1, 2, 3, 4, 5} +local simple_table = {a = 1, b = 2, c = 3} +local mixed_table = {1, 2, a = "hello", b = "world"} +local nested_table = { + a = {x = 1, y = 2}, + b = {x = 3, y = 4}, + c = {1, 2, 3} +} + +-- ====================================================================== +-- BUILT-IN TABLE FUNCTIONS +-- ====================================================================== + +test("Table Insert Operations", function() + local t = {1, 2, 3} + + tbl.insert(t, 4) + assert_equal(4, #t) + assert_equal(4, t[4]) + + tbl.insert(t, 2, "inserted") + assert_equal(5, #t) + assert_equal("inserted", t[2]) + assert_equal(2, t[3]) +end) + +test("Table Remove Operations", function() + local t = {1, 2, 3, 4, 5} + + local removed = tbl.remove(t) + assert_equal(5, removed) + assert_equal(4, #t) + + removed = tbl.remove(t, 2) + assert_equal(2, removed) + assert_equal(3, #t) + assert_equal(3, t[2]) +end) + +test("Table Concat", function() + local t = {"hello", "world", "test"} + assert_equal("helloworldtest", tbl.concat(t)) + assert_equal("hello,world,test", tbl.concat(t, ",")) + assert_equal("world,test", tbl.concat(t, ",", 2)) + assert_equal("world", tbl.concat(t, ",", 2, 2)) +end) + +test("Table Sort", function() + local t = {3, 1, 4, 1, 5} + tbl.sort(t) + assert_table_equal({1, 1, 3, 4, 5}, t) + + local t2 = {"c", "a", "b"} + tbl.sort(t2) + assert_table_equal({"a", "b", "c"}, t2) + + local t3 = {3, 1, 4, 1, 5} + tbl.sort(t3, function(a, b) return a > b end) + assert_table_equal({5, 4, 3, 1, 1}, t3) +end) + +-- ====================================================================== +-- BASIC TABLE OPERATIONS +-- ====================================================================== + +test("Table Length and Size", function() + assert_equal(5, tbl.length(simple_array)) + assert_equal(0, tbl.length({})) + + assert_equal(3, tbl.size(simple_table)) + assert_equal(4, tbl.size(mixed_table)) + assert_equal(0, tbl.size({})) +end) + +test("Table Empty Check", function() + assert_equal(true, tbl.is_empty({})) + assert_equal(false, tbl.is_empty(simple_array)) + assert_equal(false, tbl.is_empty(simple_table)) +end) + +test("Table Array Check", function() + assert_equal(true, tbl.is_array(simple_array)) + assert_equal(true, tbl.is_array({})) + assert_equal(false, tbl.is_array(simple_table)) + assert_equal(false, tbl.is_array(mixed_table)) + + assert_equal(true, tbl.is_array({1, 2, 3})) + assert_equal(false, tbl.is_array({1, 2, nil, 4})) + assert_equal(false, tbl.is_array({[0] = 1, [1] = 2})) +end) + +test("Table Clear", function() + local t = tbl.clone(simple_table) + tbl.clear(t) + assert_equal(true, tbl.is_empty(t)) +end) + +test("Table Clone", function() + local cloned = tbl.clone(simple_table) + assert_table_equal(simple_table, cloned) + + -- Modify original shouldn't affect clone + simple_table.new_key = "test" + assert_equal(nil, cloned.new_key) + simple_table.new_key = nil +end) + +test("Table Deep Copy", function() + local copied = tbl.deep_copy(nested_table) + assert_table_equal(nested_table, copied) + + -- Modify nested part shouldn't affect copy + nested_table.a.z = 99 + assert_equal(nil, copied.a.z) + nested_table.a.z = nil +end) + +-- ====================================================================== +-- SEARCHING AND FINDING +-- ====================================================================== + +test("Table Contains", function() + assert_equal(true, tbl.contains(simple_array, 3)) + assert_equal(false, tbl.contains(simple_array, 6)) + assert_equal(true, tbl.contains(simple_table, 2)) + assert_equal(false, tbl.contains(simple_table, "hello")) +end) + +test("Table Index Of", function() + assert_equal(3, tbl.index_of(simple_array, 3)) + assert_equal(nil, tbl.index_of(simple_array, 6)) + assert_equal("b", tbl.index_of(simple_table, 2)) + assert_equal(nil, tbl.index_of(simple_table, "hello")) +end) + +test("Table Find", function() + local value, key = tbl.find(simple_array, function(v) return v > 3 end) + assert_equal(4, value) + assert_equal(4, key) + + local value2, key2 = tbl.find(simple_table, function(v, k) return k == "b" end) + assert_equal(2, value2) + assert_equal("b", key2) + + local value3 = tbl.find(simple_array, function(v) return v > 10 end) + assert_equal(nil, value3) +end) + +test("Table Find Index", function() + local idx = tbl.find_index(simple_array, function(v) return v > 3 end) + assert_equal(4, idx) + + local idx2 = tbl.find_index(simple_table, function(v, k) return k == "c" end) + assert_equal("c", idx2) + + local idx3 = tbl.find_index(simple_array, function(v) return v > 10 end) + assert_equal(nil, idx3) +end) + +test("Table Count", function() + local arr = {1, 2, 3, 2, 4, 2} + assert_equal(3, tbl.count(arr, 2)) + assert_equal(0, tbl.count(arr, 5)) + + assert_equal(2, tbl.count(arr, function(v) return v > 2 end)) + assert_equal(2, tbl.count(arr, function(v) return v == 1 or v == 4 end)) +end) + +-- ====================================================================== +-- FILTERING AND MAPPING +-- ====================================================================== + +test("Table Filter", function() + local evens = tbl.filter(simple_array, function(v) return v % 2 == 0 end) + assert_table_equal({2, 4}, evens) + + local filtered_table = tbl.filter(simple_table, function(v) return v > 1 end) + assert_equal(2, tbl.size(filtered_table)) + assert_equal(2, filtered_table.b) + assert_equal(3, filtered_table.c) +end) + +test("Table Reject", function() + local odds = tbl.reject(simple_array, function(v) return v % 2 == 0 end) + assert_table_equal({1, 3, 5}, odds) +end) + +test("Table Map", function() + local doubled = tbl.map(simple_array, function(v) return v * 2 end) + assert_table_equal({2, 4, 6, 8, 10}, doubled) + + local mapped_table = tbl.map(simple_table, function(v) return v + 10 end) + assert_equal(11, mapped_table.a) + assert_equal(12, mapped_table.b) + assert_equal(13, mapped_table.c) +end) + +test("Table Map Values", function() + local incremented = tbl.map_values(simple_table, function(v) return v + 1 end) + assert_equal(2, incremented.a) + assert_equal(3, incremented.b) + assert_equal(4, incremented.c) +end) + +test("Table Map Keys", function() + local prefixed = tbl.map_keys(simple_table, function(k) return "key_" .. k end) + assert_equal(1, prefixed.key_a) + assert_equal(2, prefixed.key_b) + assert_equal(3, prefixed.key_c) + assert_equal(nil, prefixed.a) +end) + +-- ====================================================================== +-- REDUCING AND AGGREGATING +-- ====================================================================== + +test("Table Reduce", function() + local sum = tbl.reduce(simple_array, function(acc, v) return acc + v end) + assert_equal(15, sum) + + local sum_with_initial = tbl.reduce(simple_array, function(acc, v) return acc + v end, 10) + assert_equal(25, sum_with_initial) + + local product = tbl.reduce({2, 3, 4}, function(acc, v) return acc * v end) + assert_equal(24, product) +end) + +test("Table Fold", function() + local sum = tbl.fold(simple_array, function(acc, v) return acc + v end, 0) + assert_equal(15, sum) + + local concatenated = tbl.fold({"a", "b", "c"}, function(acc, v) return acc .. v end, "") + assert_equal("abc", concatenated) +end) + +test("Table Math Operations", function() + assert_equal(15, tbl.sum(simple_array)) + assert_equal(120, tbl.product(simple_array)) + assert_equal(1, tbl.min(simple_array)) + assert_equal(5, tbl.max(simple_array)) + assert_equal(3, tbl.average(simple_array)) + + local floats = {1.5, 2.5, 3.0} + assert_close(7.0, tbl.sum(floats)) + assert_close(2.33333, tbl.average(floats), 0.001) +end) + +-- ====================================================================== +-- BOOLEAN OPERATIONS +-- ====================================================================== + +test("Table All", function() + assert_equal(true, tbl.all({true, true, true})) + assert_equal(false, tbl.all({true, false, true})) + assert_equal(true, tbl.all({})) + + assert_equal(true, tbl.all(simple_array, function(v) return v > 0 end)) + assert_equal(false, tbl.all(simple_array, function(v) return v > 3 end)) +end) + +test("Table Any", function() + assert_equal(true, tbl.any({false, true, false})) + assert_equal(false, tbl.any({false, false, false})) + assert_equal(false, tbl.any({})) + + assert_equal(true, tbl.any(simple_array, function(v) return v > 3 end)) + assert_equal(false, tbl.any(simple_array, function(v) return v > 10 end)) +end) + +test("Table None", function() + assert_equal(true, tbl.none({false, false, false})) + assert_equal(false, tbl.none({false, true, false})) + assert_equal(true, tbl.none({})) + + assert_equal(false, tbl.none(simple_array, function(v) return v > 3 end)) + assert_equal(true, tbl.none(simple_array, function(v) return v > 10 end)) +end) + +-- ====================================================================== +-- SET OPERATIONS +-- ====================================================================== + +test("Table Unique", function() + local duplicates = {1, 2, 2, 3, 3, 3, 4} + local unique = tbl.unique(duplicates) + assert_table_equal({1, 2, 3, 4}, unique) + + local empty_unique = tbl.unique({}) + assert_table_equal({}, empty_unique) +end) + +test("Table Intersection", function() + local arr1 = {1, 2, 3, 4} + local arr2 = {3, 4, 5, 6} + local intersect = tbl.intersection(arr1, arr2) + assert_equal(2, #intersect) + assert_equal(true, tbl.contains(intersect, 3)) + assert_equal(true, tbl.contains(intersect, 4)) +end) + +test("Table Union", function() + local arr1 = {1, 2, 3} + local arr2 = {3, 4, 5} + local union = tbl.union(arr1, arr2) + assert_equal(5, #union) + for i = 1, 5 do + assert_equal(true, tbl.contains(union, i)) + end +end) + +test("Table Difference", function() + local arr1 = {1, 2, 3, 4, 5} + local arr2 = {3, 4} + local diff = tbl.difference(arr1, arr2) + assert_table_equal({1, 2, 5}, diff) +end) + +-- ====================================================================== +-- ARRAY OPERATIONS +-- ====================================================================== + +test("Table Reverse", function() + local reversed = tbl.reverse(simple_array) + assert_table_equal({5, 4, 3, 2, 1}, reversed) + + local single = tbl.reverse({42}) + assert_table_equal({42}, single) + + local empty = tbl.reverse({}) + assert_table_equal({}, empty) +end) + +test("Table Shuffle", function() + local shuffled = tbl.shuffle(simple_array) + assert_equal(5, #shuffled) + + -- All original elements should still be present + for _, v in ipairs(simple_array) do + assert_equal(true, tbl.contains(shuffled, v)) + end + + -- Should be same length + assert_equal(#simple_array, #shuffled) +end) + +test("Table Rotate", function() + local arr = {1, 2, 3, 4, 5} + + local rotated_right = tbl.rotate(arr, 2) + assert_table_equal({4, 5, 1, 2, 3}, rotated_right) + + local rotated_left = tbl.rotate(arr, -2) + assert_table_equal({3, 4, 5, 1, 2}, rotated_left) + + local no_rotation = tbl.rotate(arr, 0) + assert_table_equal(arr, no_rotation) + + local full_rotation = tbl.rotate(arr, 5) + assert_table_equal(arr, full_rotation) +end) + +test("Table Slice", function() + local sliced = tbl.slice(simple_array, 2, 4) + assert_table_equal({2, 3, 4}, sliced) + + local from_start = tbl.slice(simple_array, 1, 3) + assert_table_equal({1, 2, 3}, from_start) + + local to_end = tbl.slice(simple_array, 3) + assert_table_equal({3, 4, 5}, to_end) + + local negative_indices = tbl.slice(simple_array, -3, -1) + assert_table_equal({3, 4, 5}, negative_indices) +end) + +test("Table Splice", function() + local arr = {1, 2, 3, 4, 5} + + -- Remove elements + local removed = tbl.splice(arr, 2, 2) + assert_table_equal({2, 3}, removed) + assert_table_equal({1, 4, 5}, arr) + + -- Insert elements + arr = {1, 2, 3, 4, 5} + removed = tbl.splice(arr, 3, 0, "a", "b") + assert_table_equal({}, removed) + assert_table_equal({1, 2, "a", "b", 3, 4, 5}, arr) + + -- Replace elements + arr = {1, 2, 3, 4, 5} + removed = tbl.splice(arr, 2, 2, "x", "y", "z") + assert_table_equal({2, 3}, removed) + assert_table_equal({1, "x", "y", "z", 4, 5}, arr) +end) + +-- ====================================================================== +-- SORTING HELPERS +-- ====================================================================== + +test("Table Sort By", function() + local people = { + {name = "Alice", age = 30}, + {name = "Bob", age = 25}, + {name = "Charlie", age = 35} + } + + local sorted_by_age = tbl.sort_by(people, function(p) return p.age end) + assert_equal("Bob", sorted_by_age[1].name) + assert_equal("Charlie", sorted_by_age[3].name) + + local sorted_by_name = tbl.sort_by(people, function(p) return p.name end) + assert_equal("Alice", sorted_by_name[1].name) + assert_equal("Charlie", sorted_by_name[3].name) +end) + +test("Table Is Sorted", function() + assert_equal(true, tbl.is_sorted({1, 2, 3, 4, 5})) + assert_equal(false, tbl.is_sorted({1, 3, 2, 4, 5})) + assert_equal(true, tbl.is_sorted({})) + assert_equal(true, tbl.is_sorted({42})) + + assert_equal(true, tbl.is_sorted({5, 4, 3, 2, 1}, function(a, b) return a > b end)) + assert_equal(false, tbl.is_sorted({1, 2, 3, 4, 5}, function(a, b) return a > b end)) +end) + +-- ====================================================================== +-- UTILITY FUNCTIONS +-- ====================================================================== + +test("Table Keys and Values", function() + local keys = tbl.keys(simple_table) + assert_equal(3, #keys) + assert_equal(true, tbl.contains(keys, "a")) + assert_equal(true, tbl.contains(keys, "b")) + assert_equal(true, tbl.contains(keys, "c")) + + local values = tbl.values(simple_table) + assert_equal(3, #values) + assert_equal(true, tbl.contains(values, 1)) + assert_equal(true, tbl.contains(values, 2)) + assert_equal(true, tbl.contains(values, 3)) +end) + +test("Table Pairs", function() + local pairs_list = tbl.pairs({a = 1, b = 2}) + assert_equal(2, #pairs_list) + + -- Should contain key-value pairs + local found_a, found_b = false, false + for _, pair in ipairs(pairs_list) do + if pair[1] == "a" and pair[2] == 1 then found_a = true end + if pair[1] == "b" and pair[2] == 2 then found_b = true end + end + assert_equal(true, found_a) + assert_equal(true, found_b) +end) + +test("Table Merge", function() + local t1 = {a = 1, b = 2} + local t2 = {c = 3, d = 4} + local t3 = {b = 20, e = 5} + + local merged = tbl.merge(t1, t2, t3) + assert_equal(5, tbl.size(merged)) + assert_equal(1, merged.a) + assert_equal(20, merged.b) -- Last one wins + assert_equal(3, merged.c) + assert_equal(4, merged.d) + assert_equal(5, merged.e) +end) + +test("Table Extend", function() + local t1 = {a = 1, b = 2} + local t2 = {c = 3, d = 4} + + local extended = tbl.extend(t1, t2) + assert_equal(t1, extended) -- Should return t1 + assert_equal(4, tbl.size(t1)) + assert_equal(3, t1.c) + assert_equal(4, t1.d) +end) + +test("Table Invert", function() + local inverted = tbl.invert(simple_table) + assert_equal("a", inverted[1]) + assert_equal("b", inverted[2]) + assert_equal("c", inverted[3]) +end) + +test("Table Pick and Omit", function() + local big_table = {a = 1, b = 2, c = 3, d = 4, e = 5} + + local picked = tbl.pick(big_table, "a", "c", "e") + assert_equal(3, tbl.size(picked)) + assert_equal(1, picked.a) + assert_equal(3, picked.c) + assert_equal(5, picked.e) + assert_equal(nil, picked.b) + assert_equal(nil, picked.d) + + local omitted = tbl.omit(big_table, "b", "d") + assert_equal(3, tbl.size(omitted)) + assert_equal(1, omitted.a) + assert_equal(3, omitted.c) + assert_equal(5, omitted.e) + assert_equal(nil, omitted.b) + assert_equal(nil, omitted.d) +end) + +-- ====================================================================== +-- DEEP OPERATIONS +-- ====================================================================== + +test("Table Deep Equals", function() + local t1 = {a = {x = 1, y = 2}, b = {1, 2, 3}} + local t2 = {a = {x = 1, y = 2}, b = {1, 2, 3}} + local t3 = {a = {x = 1, y = 3}, b = {1, 2, 3}} + + assert_equal(true, tbl.deep_equals(t1, t2)) + assert_equal(false, tbl.deep_equals(t1, t3)) + assert_equal(true, tbl.deep_equals({}, {})) + assert_equal(false, tbl.deep_equals({a = 1}, {a = 1, b = 2})) +end) + +test("Table Flatten", function() + local nested = {{1, 2}, {3, 4}, {5, {6, 7}}} + local flattened = tbl.flatten(nested) + assert_table_equal({1, 2, 3, 4, 5, {6, 7}}, flattened) + + local deep_flattened = tbl.flatten(nested, 2) + assert_table_equal({1, 2, 3, 4, 5, 6, 7}, deep_flattened) + + local already_flat = tbl.flatten({1, 2, 3}) + assert_table_equal({1, 2, 3}, already_flat) +end) + +test("Table Deep Merge", function() + local t1 = {a = {x = 1}, b = 2} + local t2 = {a = {y = 3}, c = 4} + + local merged = tbl.deep_merge(t1, t2) + assert_equal(1, merged.a.x) + assert_equal(3, merged.a.y) + assert_equal(2, merged.b) + assert_equal(4, merged.c) + + -- Original tables should be unchanged + assert_equal(nil, t1.a.y) + assert_equal(nil, t1.c) +end) + +-- ====================================================================== +-- ADVANCED OPERATIONS +-- ====================================================================== + +test("Table Chunk", function() + local chunks = tbl.chunk({1, 2, 3, 4, 5, 6, 7}, 3) + assert_equal(3, #chunks) + assert_table_equal({1, 2, 3}, chunks[1]) + assert_table_equal({4, 5, 6}, chunks[2]) + assert_table_equal({7}, chunks[3]) + + local exact_chunks = tbl.chunk({1, 2, 3, 4}, 2) + assert_equal(2, #exact_chunks) + assert_table_equal({1, 2}, exact_chunks[1]) + assert_table_equal({3, 4}, exact_chunks[2]) +end) + +test("Table Partition", function() + local evens, odds = tbl.partition(simple_array, function(v) return v % 2 == 0 end) + assert_table_equal({2, 4}, evens) + assert_table_equal({1, 3, 5}, odds) + + local empty_true, all_false = tbl.partition({1, 3, 5}, function(v) return v % 2 == 0 end) + assert_table_equal({}, empty_true) + assert_table_equal({1, 3, 5}, all_false) +end) + +test("Table Group By", function() + local people = { + {name = "Alice", department = "engineering"}, + {name = "Bob", department = "sales"}, + {name = "Charlie", department = "engineering"}, + {name = "David", department = "sales"} + } + + local by_dept = tbl.group_by(people, function(person) return person.department end) + assert_equal(2, tbl.size(by_dept)) + assert_equal(2, #by_dept.engineering) + assert_equal(2, #by_dept.sales) + assert_equal("Alice", by_dept.engineering[1].name) + assert_equal("Bob", by_dept.sales[1].name) +end) + +test("Table Zip", function() + local names = {"Alice", "Bob", "Charlie"} + local ages = {25, 30, 35} + local cities = {"NYC", "LA", "Chicago"} + + local zipped = tbl.zip(names, ages, cities) + assert_equal(3, #zipped) + assert_table_equal({"Alice", 25, "NYC"}, zipped[1]) + assert_table_equal({"Bob", 30, "LA"}, zipped[2]) + assert_table_equal({"Charlie", 35, "Chicago"}, zipped[3]) + + -- Different lengths + local short_zip = tbl.zip({1, 2, 3}, {"a", "b"}) + assert_equal(2, #short_zip) + assert_table_equal({1, "a"}, short_zip[1]) + assert_table_equal({2, "b"}, short_zip[2]) +end) + +test("Table Compact", function() + local messy = {1, nil, false, 2, nil, 3, false} + local compacted = tbl.compact(messy) + assert_table_equal({1, 2, 3}, compacted) + + local clean = {1, 2, 3} + local unchanged = tbl.compact(clean) + assert_table_equal(clean, unchanged) +end) + +test("Table Sample", function() + local sample1 = tbl.sample(simple_array, 3) + assert_equal(3, #sample1) + + -- All sampled elements should be from original + for _, v in ipairs(sample1) do + assert_equal(true, tbl.contains(simple_array, v)) + end + + local single_sample = tbl.sample(simple_array) + assert_equal(1, #single_sample) + assert_equal(true, tbl.contains(simple_array, single_sample[1])) + + local oversample = tbl.sample({1, 2}, 5) + assert_equal(2, #oversample) +end) + +-- ====================================================================== +-- EDGE CASES AND ERROR HANDLING +-- ====================================================================== + +test("Empty Table Handling", function() + local empty = {} + + assert_equal(true, tbl.is_empty(empty)) + assert_equal(0, tbl.length(empty)) + assert_equal(0, tbl.size(empty)) + assert_equal(true, tbl.is_array(empty)) + + assert_table_equal({}, tbl.filter(empty, function() return true end)) + assert_table_equal({}, tbl.map(empty, function(v) return v * 2 end)) + assert_table_equal({}, tbl.keys(empty)) + assert_table_equal({}, tbl.values(empty)) + + assert_equal(true, tbl.all(empty)) + assert_equal(false, tbl.any(empty)) + assert_equal(true, tbl.none(empty)) +end) + +test("Single Element Tables", function() + local single = {42} + + assert_equal(1, tbl.length(single)) + assert_equal(1, tbl.size(single)) + assert_equal(true, tbl.is_array(single)) + assert_equal(false, tbl.is_empty(single)) + + assert_equal(42, tbl.sum(single)) + assert_equal(42, tbl.product(single)) + assert_equal(42, tbl.min(single)) + assert_equal(42, tbl.max(single)) + assert_equal(42, tbl.average(single)) + + assert_table_equal({42}, tbl.reverse(single)) + assert_table_equal({84}, tbl.map(single, function(v) return v * 2 end)) +end) + +test("Circular Reference Handling", function() + local t1 = {a = 1} + local t2 = {b = 2} + t1.ref = t2 + t2.ref = t1 + + -- Deep copy should handle circular references + local copied = tbl.deep_copy(t1) + assert_equal(1, copied.a) + assert_equal(2, copied.ref.b) + assert_equal(copied, copied.ref.ref) -- Should maintain circular structure +end) + +test("Large Table Performance", function() + local large = {} + for i = 1, 10000 do + large[i] = i + end + + assert_equal(10000, tbl.length(large)) + assert_equal(true, tbl.is_array(large)) + assert_equal(50005000, tbl.sum(large)) -- Sum of 1 to 10000 + + local evens = tbl.filter(large, function(v) return v % 2 == 0 end) + assert_equal(5000, #evens) + + local doubled = tbl.map(large, function(v) return v * 2 end) + assert_equal(10000, #doubled) + assert_equal(2, doubled[1]) + assert_equal(20000, doubled[10000]) +end) + +test("Mixed Type Table Handling", function() + local mixed = {1, "hello", true, {a = 1}, function() end} + + assert_equal(5, tbl.length(mixed)) + assert_equal(true, tbl.is_array(mixed)) + assert_equal(true, tbl.contains(mixed, "hello")) + assert_equal(true, tbl.contains(mixed, true)) + + local strings_only = tbl.filter(mixed, function(v) return type(v) == "string" end) + assert_equal(1, #strings_only) + assert_equal("hello", strings_only[1]) +end) + +-- ====================================================================== +-- PERFORMANCE TESTS +-- ====================================================================== + +test("Performance Test", function() + local large_array = {} + for i = 1, 10000 do + large_array[i] = math.random(1, 1000) + end + + local start = os.clock() + local filtered = tbl.filter(large_array, function(v) return v > 500 end) + local filter_time = os.clock() - start + + start = os.clock() + local mapped = tbl.map(large_array, function(v) return v * 2 end) + local map_time = os.clock() - start + + start = os.clock() + local sum = tbl.sum(large_array) + local sum_time = os.clock() - start + + start = os.clock() + local sorted = tbl.sort_by(large_array, function(v) return v end) + local sort_time = os.clock() - start + + start = os.clock() + local unique = tbl.unique(large_array) + local unique_time = os.clock() - start + + print(string.format(" Filter %d elements: %.3fs", #filtered, filter_time)) + print(string.format(" Map %d elements: %.3fs", #mapped, map_time)) + print(string.format(" Sum %d elements: %.3fs", #large_array, sum_time)) + print(string.format(" Sort %d elements: %.3fs", #sorted, sort_time)) + print(string.format(" Unique from %d to %d: %.3fs", #large_array, #unique, unique_time)) + + assert(#filtered > 0, "should filter some elements") + assert_equal(#large_array, #mapped) + assert(sum > 0, "sum should be positive") + assert_equal(#large_array, #sorted) + assert(tbl.is_sorted(sorted), "should be sorted") +end) + +-- ====================================================================== +-- INTEGRATION TESTS +-- ====================================================================== + +test("Data Processing Pipeline", function() + local sales_data = { + {product = "laptop", price = 1000, quantity = 2, category = "electronics"}, + {product = "mouse", price = 25, quantity = 10, category = "electronics"}, + {product = "book", price = 15, quantity = 5, category = "books"}, + {product = "phone", price = 800, quantity = 3, category = "electronics"}, + {product = "magazine", price = 5, quantity = 20, category = "books"} + } + + -- Calculate total revenue per item + local with_revenue = tbl.map(sales_data, function(item) + local new_item = tbl.clone(item) + new_item.revenue = item.price * item.quantity + return new_item + end) + + -- Filter high-value items (revenue >= 100) + local high_value = tbl.filter(with_revenue, function(item) + return item.revenue >= 100 + end) + + -- Group by category + local by_category = tbl.group_by(high_value, function(item) + return item.category + end) + + -- Calculate total revenue by category + local category_totals = tbl.map_values(by_category, function(items) + return tbl.sum(tbl.map(items, function(item) return item.revenue end)) + end) + + assert_equal(2, tbl.size(category_totals)) + assert_equal(4650, category_totals.electronics) -- laptop: 2000, mouse: 250, phone: 2400 + assert_equal(100, category_totals.books) -- magazine: 100 +end) + +test("Complex Data Transformation", function() + local users = { + {id = 1, name = "Alice", age = 25, skills = {"lua", "python"}}, + {id = 2, name = "Bob", age = 30, skills = {"javascript", "lua"}}, + {id = 3, name = "Charlie", age = 35, skills = {"python", "java"}}, + {id = 4, name = "David", age = 28, skills = {"lua", "go"}} + } + + -- Find Lua developers + local lua_devs = tbl.filter(users, function(user) + return tbl.contains(user.skills, "lua") + end) + + -- Sort by age + local sorted_lua_devs = tbl.sort_by(lua_devs, function(user) return user.age end) + + -- Extract just names and ages + local simplified = tbl.map(sorted_lua_devs, function(user) + return {name = user.name, age = user.age} + end) + + assert_equal(3, #simplified) + assert_equal("Alice", simplified[1].name) -- Youngest + assert_equal("David", simplified[2].name) + assert_equal("Bob", simplified[3].name) -- Oldest + + -- Group all users by age ranges + local age_groups = tbl.group_by(users, function(user) + if user.age < 30 then return "young" + else return "experienced" end + end) + + assert_equal(2, #age_groups.young) -- Alice, David + assert_equal(2, #age_groups.experienced) -- Bob, Charlie +end) + +test("Statistical Analysis", function() + local test_scores = { + {student = "Alice", scores = {85, 92, 78, 95, 88}}, + {student = "Bob", scores = {72, 85, 90, 77, 82}}, + {student = "Charlie", scores = {95, 88, 92, 90, 85}}, + {student = "David", scores = {68, 75, 80, 72, 78}} + } + + -- Calculate average score for each student + local with_averages = tbl.map(test_scores, function(student) + local avg = tbl.average(student.scores) + return { + student = student.student, + scores = student.scores, + average = avg, + max_score = tbl.max(student.scores), + min_score = tbl.min(student.scores) + } + end) + + -- Find top performer + local top_student = tbl.reduce(with_averages, function(best, current) + return current.average > best.average and current or best + end) + + -- Students above class average + local all_averages = tbl.map(with_averages, function(s) return s.average end) + local class_average = tbl.average(all_averages) + local above_average = tbl.filter(with_averages, function(s) + return s.average > class_average + end) + + assert_equal("Charlie", top_student.student) + assert_equal(90, top_student.average) + assert_equal(2, #above_average) -- Charlie and Alice + assert_close(83.4, class_average, 0.1) +end) + +summary() +test_exit()