rewrite of the string module

This commit is contained in:
Sky Johnson 2025-07-24 16:45:12 -05:00
parent 8a53fea511
commit 41cba2f049
10 changed files with 1691 additions and 1455 deletions

View File

@ -1,6 +1,6 @@
local http = {} local str = require("string")
local json = require("json") local json = require("json")
local string = require("string") local http = {}
-- Global routing tables -- Global routing tables
_G._http_routes = _G._http_routes or {} _G._http_routes = _G._http_routes or {}
@ -19,29 +19,29 @@ Response.__index = Response
local function parse_cookies(cookie_header) local function parse_cookies(cookie_header)
local cookies = {} local cookies = {}
if string.is_empty(cookie_header) then if str.is_empty(cookie_header) then
return cookies return cookies
end end
-- Split by semicolon and parse each cookie -- Split by semicolon and parse each cookie
local cookie_pairs = string.split(cookie_header, ";") local cookie_pairs = str.split(cookie_header, ";")
for _, cookie_pair in ipairs(cookie_pairs) do for _, cookie_pair in ipairs(cookie_pairs) do
local trimmed = string.trim(cookie_pair) local trimmed = str.trim(cookie_pair)
if not string.is_empty(trimmed) then if not str.is_empty(trimmed) then
local parts = string.split(trimmed, "=") local parts = str.split(trimmed, "=")
if #parts >= 2 then if #parts >= 2 then
local name = string.trim(parts[1]) local name = str.trim(parts[1])
local value = string.trim(parts[2]) local value = str.trim(parts[2])
-- URL decode the value -- URL decode the value
local success, decoded = pcall(function() local success, decoded = pcall(function()
return string.url_decode(value) return str.url_decode(value)
end) end)
cookies[name] = success and decoded or value cookies[name] = success and decoded or value
elseif #parts == 1 then elseif #parts == 1 then
-- Cookie without value -- Cookie without value
cookies[string.trim(parts[1])] = "" cookies[str.trim(parts[1])] = ""
end end
end end
end end
@ -54,17 +54,17 @@ end
-- ====================================================================== -- ======================================================================
local function split_path(path) local function split_path(path)
if string.is_empty(path) or path == "/" then if str.is_empty(path) or path == "/" then
return {} return {}
end end
-- Remove leading/trailing slashes and split -- Remove leading/trailing slashes and split
local clean_path = string.trim(path, "/") local clean_path = str.trim(path, "/")
if string.is_empty(clean_path) then if str.is_empty(clean_path) then
return {} return {}
end end
return string.split(clean_path, "/") return str.split(clean_path, "/")
end end
local function match_route(method, path) local function match_route(method, path)
@ -86,12 +86,12 @@ local function match_route(method, path)
for j = i, #path_segments do for j = i, #path_segments do
table.insert(remaining, path_segments[j]) table.insert(remaining, path_segments[j])
end end
params["*"] = string.join(remaining, "/") params["*"] = str.join(remaining, "/")
break break
elseif string.starts_with(route_seg, ":") then elseif str.starts_with(route_seg, ":") then
-- Parameter segment -- Parameter segment
if i <= #path_segments then if i <= #path_segments then
local param_name = string.slice(route_seg, 2, -1) local param_name = str.slice(route_seg, 2, -1)
params[param_name] = path_segments[i] params[param_name] = path_segments[i]
else else
match = false match = false
@ -135,7 +135,7 @@ function _http_handle_request(req_table, res_table)
end end
local mw = _G._http_middleware[index] local mw = _G._http_middleware[index]
if mw.path == nil or string.starts_with(req.path, mw.path) then if mw.path == nil or str.starts_with(req.path, mw.path) then
mw.handler(req, res, function() mw.handler(req, res, function()
run_middleware(index + 1) run_middleware(index + 1)
end) end)
@ -203,7 +203,7 @@ end
function Server:_add_route(method, path, handler) function Server:_add_route(method, path, handler)
-- Ensure path starts with / -- Ensure path starts with /
if not string.starts_with(path, "/") then if not str.starts_with(path, "/") then
path = "/" .. path path = "/" .. path
end end
@ -321,7 +321,7 @@ function Request.new(req_table)
end end
function Request:get(header_name) function Request:get(header_name)
local lower_name = string.lower(header_name) local lower_name = str.lower(header_name)
return self.headers[header_name] or self.headers[lower_name] return self.headers[header_name] or self.headers[lower_name]
end end
@ -359,7 +359,7 @@ function Request:cookie_matches(name, pattern)
if not cookie_value then if not cookie_value then
return false return false
end end
return string.match(pattern, cookie_value) return str.match(pattern, cookie_value)
end end
function Request:get_cookies_by_names(names) function Request:get_cookies_by_names(names)
@ -381,7 +381,7 @@ function Request:has_auth_cookies()
end end
function Request:json() function Request:json()
if string.is_empty(self.body) then if str.is_empty(self.body) then
return nil return nil
end end
@ -398,27 +398,27 @@ end
function Request:is_json() function Request:is_json()
local content_type = self:get("content-type") or "" local content_type = self:get("content-type") or ""
return string.contains(content_type, "application/json") return str.contains(content_type, "application/json")
end end
function Request:is_form() function Request:is_form()
local content_type = self:get("content-type") or "" local content_type = self:get("content-type") or ""
return string.contains(content_type, "application/x-www-form-urlencoded") return str.contains(content_type, "application/x-www-form-urlencoded")
end end
function Request:is_multipart() function Request:is_multipart()
local content_type = self:get("content-type") or "" local content_type = self:get("content-type") or ""
return string.contains(content_type, "multipart/form-data") return str.contains(content_type, "multipart/form-data")
end end
function Request:is_xml() function Request:is_xml()
local content_type = self:get("content-type") or "" local content_type = self:get("content-type") or ""
return string.contains(content_type, "application/xml") or string.contains(content_type, "text/xml") return str.contains(content_type, "application/xml") or str.contains(content_type, "text/xml")
end end
function Request:accepts(mime_type) function Request:accepts(mime_type)
local accept_header = self:get("accept") or "" local accept_header = self:get("accept") or ""
return string.contains(accept_header, mime_type) or string.contains(accept_header, "*/*") return str.contains(accept_header, mime_type) or str.contains(accept_header, "*/*")
end end
function Request:user_agent() function Request:user_agent()
@ -431,7 +431,7 @@ end
function Request:is_secure() function Request:is_secure()
local proto = self:get("x-forwarded-proto") local proto = self:get("x-forwarded-proto")
return proto == "https" or string.starts_with(self:get("host") or "", "https://") return proto == "https" or str.starts_with(self:get("host") or "", "https://")
end end
-- ====================================================================== -- ======================================================================
@ -565,8 +565,8 @@ function Response:cookie(name, value, options)
local cookie_value = tostring(value) local cookie_value = tostring(value)
-- URL encode the cookie value if it contains special characters -- URL encode the cookie value if it contains special characters
if string.match("[;,\\s]", cookie_value) then if str.match("[;,\\s]", cookie_value) then
cookie_value = string.url_encode(cookie_value) cookie_value = str.url_encode(cookie_value)
end end
local cookie = name .. "=" .. cookie_value local cookie = name .. "=" .. cookie_value
@ -638,13 +638,13 @@ function Response:download(data, filename, content_type)
self:type(content_type) self:type(content_type)
elseif filename then elseif filename then
-- Try to guess content type from extension -- Try to guess content type from extension
if string.ends_with(filename, ".pdf") then if str.ends_with(filename, ".pdf") then
self:type("application/pdf") self:type("application/pdf")
elseif string.ends_with(filename, ".zip") then elseif str.ends_with(filename, ".zip") then
self:type("application/zip") self:type("application/zip")
elseif string.ends_with(filename, ".json") then elseif str.ends_with(filename, ".json") then
self:type("application/json") self:type("application/json")
elseif string.ends_with(filename, ".csv") then elseif str.ends_with(filename, ".csv") then
self:type("text/csv") self:type("text/csv")
else else
self:type("application/octet-stream") self:type("application/octet-stream")
@ -675,7 +675,7 @@ function http.cors(options)
res:header("Access-Control-Allow-Credentials", "true") res:header("Access-Control-Allow-Credentials", "true")
end end
if string.iequals(req.method, "OPTIONS") then if str.iequals(req.method, "OPTIONS") then
res:status(204):send("") res:status(204):send("")
else else
next() next()
@ -687,7 +687,7 @@ function http.static(root_path, url_prefix)
url_prefix = url_prefix or "/" url_prefix = url_prefix or "/"
-- Ensure prefix starts with / -- Ensure prefix starts with /
if not string.starts_with(url_prefix, "/") then if not str.starts_with(url_prefix, "/") then
url_prefix = "/" .. url_prefix url_prefix = "/" .. url_prefix
end end
@ -706,7 +706,7 @@ end
function http.json_parser() function http.json_parser()
return function(req, res, next) return function(req, res, next)
if req:is_json() and not string.is_empty(req.body) then if req:is_json() and not str.is_empty(req.body) then
local success, data = pcall(function() local success, data = pcall(function()
return req:json() return req:json()
end) end)
@ -733,11 +733,11 @@ function http.logger(format)
local duration = (os.clock() - start_time) * 1000 local duration = (os.clock() - start_time) * 1000
local status = res._table.status or 200 local status = res._table.status or 200
local log_message = string.template(format, { local log_message = str.template(format, {
method = req.method, method = req.method,
path = req.path, path = req.path,
status = status, status = status,
["response-time"] = string.format("%.2f", duration), ["response-time"] = str.format("%.2f", duration),
["user-agent"] = req:user_agent(), ["user-agent"] = req:user_agent(),
ip = req:ip() ip = req:ip()
}) })
@ -749,9 +749,9 @@ end
function http.compression() function http.compression()
return function(req, res, next) return function(req, res, next)
local accept_encoding = req:get("accept-encoding") or "" local accept_encoding = req:get("accept-encoding") or ""
if string.contains(accept_encoding, "gzip") then if str.contains(accept_encoding, "gzip") then
res:header("Content-Encoding", "gzip") res:header("Content-Encoding", "gzip")
elseif string.contains(accept_encoding, "deflate") then elseif str.contains(accept_encoding, "deflate") then
res:header("Content-Encoding", "deflate") res:header("Content-Encoding", "deflate")
end end
next() next()

View File

@ -58,7 +58,10 @@ func kv_open(s *luajit.State) int {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
if _, exists := stores[name]; exists { if store, exists := stores[name]; exists {
if filename != "" && store.filename != filename {
store.filename = filename
}
s.PushBoolean(true) s.PushBoolean(true)
return 1 return 1
} }

View File

@ -4,6 +4,7 @@ import (
"embed" "embed"
"fmt" "fmt"
"maps" "maps"
"strings"
"Moonshark/modules/crypto" "Moonshark/modules/crypto"
"Moonshark/modules/fs" "Moonshark/modules/fs"
@ -11,31 +12,29 @@ import (
"Moonshark/modules/kv" "Moonshark/modules/kv"
"Moonshark/modules/math" "Moonshark/modules/math"
"Moonshark/modules/sql" "Moonshark/modules/sql"
lua_string "Moonshark/modules/string" lua_string "Moonshark/modules/string+"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go" luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
) )
// Global registry instance
var Global *Registry var Global *Registry
//go:embed **/*.lua //go:embed **/*.lua
var embeddedModules embed.FS var embeddedModules embed.FS
// Registry manages all Lua modules and Go functions
type Registry struct { type Registry struct {
modules map[string]string modules map[string]string
goFuncs map[string]luajit.GoFunction globalModules map[string]string // globalName -> moduleSource
goFuncs map[string]luajit.GoFunction
} }
// New creates a new registry with all modules loaded
func New() *Registry { func New() *Registry {
r := &Registry{ r := &Registry{
modules: make(map[string]string), modules: make(map[string]string),
goFuncs: make(map[string]luajit.GoFunction), globalModules: make(map[string]string),
goFuncs: make(map[string]luajit.GoFunction),
} }
// Load all Go functions
maps.Copy(r.goFuncs, lua_string.GetFunctionList()) maps.Copy(r.goFuncs, lua_string.GetFunctionList())
maps.Copy(r.goFuncs, math.GetFunctionList()) maps.Copy(r.goFuncs, math.GetFunctionList())
maps.Copy(r.goFuncs, crypto.GetFunctionList()) maps.Copy(r.goFuncs, crypto.GetFunctionList())
@ -48,9 +47,7 @@ func New() *Registry {
return r return r
} }
// loadEmbeddedModules discovers and loads all .lua files
func (r *Registry) loadEmbeddedModules() { func (r *Registry) loadEmbeddedModules() {
// Discover all directories from embed
dirs, _ := embeddedModules.ReadDir(".") dirs, _ := embeddedModules.ReadDir(".")
for _, dir := range dirs { for _, dir := range dirs {
@ -58,15 +55,27 @@ func (r *Registry) loadEmbeddedModules() {
continue continue
} }
// Assume one module file per directory: dirname/dirname.lua dirName := dir.Name()
modulePath := fmt.Sprintf("%s/%s.lua", dir.Name(), dir.Name()) isGlobal := strings.HasSuffix(dirName, "+")
var moduleName, globalName string
if isGlobal {
moduleName = strings.TrimSuffix(dirName, "+")
globalName = moduleName
} else {
moduleName = dirName
}
modulePath := fmt.Sprintf("%s/%s.lua", dirName, moduleName)
if source, err := embeddedModules.ReadFile(modulePath); err == nil { if source, err := embeddedModules.ReadFile(modulePath); err == nil {
r.modules[dir.Name()] = string(source) r.modules[moduleName] = string(source)
if isGlobal {
r.globalModules[globalName] = string(source)
}
} }
} }
} }
// InstallInState sets up the complete module system in a Lua state
func (r *Registry) InstallInState(state *luajit.State) error { func (r *Registry) InstallInState(state *luajit.State) error {
// Create moonshark global table with Go functions // Create moonshark global table with Go functions
state.NewTable() state.NewTable()
@ -78,6 +87,13 @@ func (r *Registry) InstallInState(state *luajit.State) error {
} }
state.SetGlobal("moonshark") state.SetGlobal("moonshark")
// Auto-enhance all global modules
for globalName, source := range r.globalModules {
if err := r.enhanceGlobal(state, globalName, source); err != nil {
return fmt.Errorf("failed to enhance %s global: %w", globalName, err)
}
}
// Backup original require and install custom one // Backup original require and install custom one
state.GetGlobal("require") state.GetGlobal("require")
state.SetGlobal("_require_original") state.SetGlobal("_require_original")
@ -92,7 +108,13 @@ func (r *Registry) InstallInState(state *luajit.State) error {
return s.PushError("require: module name must be a string") return s.PushError("require: module name must be a string")
} }
// Check built-in modules first // Return global if this module enhances a global
if _, isGlobal := r.globalModules[moduleName]; isGlobal {
s.GetGlobal(moduleName)
return 1
}
// Check built-in modules
if source, exists := r.modules[moduleName]; exists { if source, exists := r.modules[moduleName]; exists {
if err := s.LoadString(source); err != nil { if err := s.LoadString(source); err != nil {
return s.PushError("require: failed to load module '%s': %v", moduleName, err) return s.PushError("require: failed to load module '%s': %v", moduleName, err)
@ -117,7 +139,18 @@ func (r *Registry) InstallInState(state *luajit.State) error {
}) })
} }
// Initialize sets up the global registry func (r *Registry) enhanceGlobal(state *luajit.State, globalName, source string) error {
// Execute the module - it directly modifies the global
if err := state.LoadString(source); err != nil {
return fmt.Errorf("failed to load %s module: %w", globalName, err)
}
if err := state.Call(0, 0); err != nil { // 0 results expected
return fmt.Errorf("failed to execute %s module: %w", globalName, err)
}
return nil
}
func Initialize() error { func Initialize() error {
Global = New() Global = New()
return nil return nil

666
modules/string+/string.lua Normal file
View File

@ -0,0 +1,666 @@
local _orig_find = string.find
local _orig_match = string.match
local REVERSE_THRESHOLD = 100
local LENGTH_THRESHOLD = 1000
function string.split(s, delimiter)
if type(s) ~= "string" then error("string.split: first argument must be a string", 2) end
if type(delimiter) ~= "string" then error("string.split: second argument must be a string", 2) end
if delimiter == "" then
local result = {}
for i = 1, #s do
result[i] = s:sub(i, i)
end
return result
end
local result = {}
local start = 1
local delimiter_len = #delimiter
while true do
local pos = _orig_find(s, delimiter, start, true) -- Use original find
if not pos then
table.insert(result, s:sub(start))
break
end
table.insert(result, s:sub(start, pos - 1))
start = pos + delimiter_len
end
return result
end
getmetatable("").__index.split = string.split
function string.join(arr, separator)
if type(arr) ~= "table" then error("string.join: first argument must be a table", 2) end
if type(separator) ~= "string" then error("string.join: second argument must be a string", 2) end
return table.concat(arr, separator)
end
function string.trim(s, cutset)
if type(s) ~= "string" then error("string.trim: first argument must be a string", 2) end
if cutset then
if type(cutset) ~= "string" then error("string.trim: second argument must be a string", 2) end
local escaped = cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
local pattern = "^[" .. escaped .. "]*(.-)[" .. escaped .. "]*$"
return s:match(pattern)
else
return s:match("^%s*(.-)%s*$")
end
end
getmetatable("").__index.trim = string.trim
function string.trim_left(s, cutset)
if type(s) ~= "string" then error("string.trim_left: first argument must be a string", 2) end
if cutset then
if type(cutset) ~= "string" then error("string.trim_left: second argument must be a string", 2) end
local pattern = "^[" .. cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") .. "]*"
return s:gsub(pattern, "")
else
return s:match("^%s*(.*)")
end
end
getmetatable("").__index.trim_left = string.trim_left
function string.trim_right(s, cutset)
if type(s) ~= "string" then error("string.trim_right: first argument must be a string", 2) end
if cutset then
if type(cutset) ~= "string" then error("string.trim_right: second argument must be a string", 2) end
local escaped = cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
local pattern = "[" .. escaped .. "]*$"
return s:gsub(pattern, "")
else
return s:match("(.-)%s*$")
end
end
function string.title(s)
if type(s) ~= "string" then error("string.title: argument must be a string", 2) end
return s:gsub("(%w)([%w]*)", function(first, rest)
return first:upper() .. rest:lower()
end)
end
getmetatable("").__index.title = string.title
function string.contains(s, substr)
if type(s) ~= "string" then error("string.contains: first argument must be a string", 2) end
if type(substr) ~= "string" then error("string.contains: second argument must be a string", 2) end
return _orig_find(s, substr, 1, true) ~= nil
end
getmetatable("").__index.contains = string.contains
function string.starts_with(s, prefix)
if type(s) ~= "string" then error("string.starts_with: first argument must be a string", 2) end
if type(prefix) ~= "string" then error("string.starts_with: second argument must be a string", 2) end
return s:sub(1, #prefix) == prefix
end
getmetatable("").__index.starts_with = string.starts_with
function string.ends_with(s, suffix)
if type(s) ~= "string" then error("string.ends_with: first argument must be a string", 2) end
if type(suffix) ~= "string" then error("string.ends_with: second argument must be a string", 2) end
if #suffix == 0 then return true end
return s:sub(-#suffix) == suffix
end
getmetatable("").__index.ends_with = string.ends_with
function string.replace(s, old, new)
if type(s) ~= "string" then error("string.replace: first argument must be a string", 2) end
if type(old) ~= "string" then error("string.replace: second argument must be a string", 2) end
if type(new) ~= "string" then error("string.replace: third argument must be a string", 2) end
if old == "" then error("string.replace: cannot replace empty string", 2) end
return s:gsub(old:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"), new)
end
getmetatable("").__index.replace = string.replace
function string.replace_n(s, old, new, n)
if type(s) ~= "string" then error("string.replace_n: first argument must be a string", 2) end
if type(old) ~= "string" then error("string.replace_n: second argument must be a string", 2) end
if type(new) ~= "string" then error("string.replace_n: third argument must be a string", 2) end
if type(n) ~= "number" or n < 0 or n ~= math.floor(n) then
error("string.replace_n: fourth argument must be a non-negative integer", 2)
end
if old == "" then error("string.replace_n: cannot replace empty string", 2) end
local escaped = old:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
return (s:gsub(escaped, new, n))
end
getmetatable("").__index.replace_n = string.replace_n
function string.index(s, substr)
if type(s) ~= "string" then error("string.index: first argument must be a string", 2) end
if type(substr) ~= "string" then error("string.index: second argument must be a string", 2) end
local pos = _orig_find(s, substr, 1, true)
return pos
end
getmetatable("").__index.index = string.index
function string.last_index(s, substr)
if type(s) ~= "string" then error("string.last_index: first argument must be a string", 2) end
if type(substr) ~= "string" then error("string.last_index: second argument must be a string", 2) end
local last_pos = nil
local pos = 1
while true do
local found = _orig_find(s, substr, pos, true)
if not found then break end
last_pos = found
pos = found + 1
end
return last_pos
end
getmetatable("").__index.last_index = string.last_index
function string.count(s, substr)
if type(s) ~= "string" then error("string.count: first argument must be a string", 2) end
if type(substr) ~= "string" then error("string.count: second argument must be a string", 2) end
if substr == "" then return #s + 1 end
local count = 0
local pos = 1
while true do
local found = _orig_find(s, substr, pos, true)
if not found then break end
count = count + 1
pos = found + #substr
end
return count
end
getmetatable("").__index.count = string.count
function string.repeat_(s, n)
if type(s) ~= "string" then error("string.repeat_: first argument must be a string", 2) end
if type(n) ~= "number" or n < 0 or n ~= math.floor(n) then
error("string.repeat_: second argument must be a non-negative integer", 2)
end
return string.rep(s, n)
end
function string.reverse(s)
if type(s) ~= "string" then error("string.reverse: argument must be a string", 2) end
if #s > REVERSE_THRESHOLD then
local result, err = moonshark.string_reverse(s)
if not result then error("string.reverse: " .. err, 2) end
return result
else
local result = {}
for i = #s, 1, -1 do
result[#result + 1] = s:sub(i, i)
end
return table.concat(result)
end
end
getmetatable("").__index.reverse = string.reverse
function string.length(s)
if type(s) ~= "string" then error("string.length: argument must be a string", 2) end
return moonshark.string_length(s)
end
getmetatable("").__index.length = string.length
function string.byte_length(s)
if type(s) ~= "string" then error("string.byte_length: argument must be a string", 2) end
return moonshark.string_byte_length(s)
end
getmetatable("").__index.byte_length = string.byte_length
function string.lines(s)
if type(s) ~= "string" then error("string.lines: argument must be a string", 2) end
if s == "" then return {""} end
s = s:gsub("\r\n", "\n"):gsub("\r", "\n")
local lines = {}
for line in (s .. "\n"):gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
if #lines > 0 and lines[#lines] == "" then
table.remove(lines)
end
return lines
end
getmetatable("").__index.lines = string.lines
function string.words(s)
if type(s) ~= "string" then error("string.words: argument must be a string", 2) end
local words = {}
for word in s:gmatch("%S+") do
table.insert(words, word)
end
return words
end
getmetatable("").__index.words = string.words
function string.pad_left(s, width, pad_char)
if type(s) ~= "string" then error("string.pad_left: first argument must be a string", 2) end
if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then
error("string.pad_left: second argument must be a non-negative integer", 2)
end
pad_char = pad_char or " "
if type(pad_char) ~= "string" then error("string.pad_left: third argument must be a string", 2) end
if #pad_char == 0 then pad_char = " " else pad_char = pad_char:sub(1,1) end
local current_len = string.length(s)
if current_len >= width then return s end
return string.rep(pad_char, width - current_len) .. s
end
getmetatable("").__index.pad_left = string.pad_left
function string.pad_right(s, width, pad_char)
if type(s) ~= "string" then error("string.pad_right: first argument must be a string", 2) end
if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then
error("string.pad_right: second argument must be a non-negative integer", 2)
end
pad_char = pad_char or " "
if type(pad_char) ~= "string" then error("string.pad_right: third argument must be a string", 2) end
if #pad_char == 0 then pad_char = " " else pad_char = pad_char:sub(1,1) end
local current_len = string.length(s)
if current_len >= width then return s end
return s .. string.rep(pad_char, width - current_len)
end
getmetatable("").__index.pad_right = string.pad_right
function string.slice(s, start, end_pos)
if type(s) ~= "string" then error("string.slice: first argument must be a string", 2) end
if type(start) ~= "number" or start ~= math.floor(start) then
error("string.slice: second argument must be an integer", 2)
end
if end_pos ~= nil and (type(end_pos) ~= "number" or end_pos ~= math.floor(end_pos)) then
error("string.slice: third argument must be an integer", 2)
end
local result, err = moonshark.string_slice(s, start, end_pos)
if not result then error("string.slice: " .. err, 2) end
return result
end
getmetatable("").__index.slice = string.slice
-- Custom find that returns matched substring instead of position
function string.find(s, pattern, init, plain)
if type(s) ~= "string" then error("string.find: first argument must be a string", 2) end
if type(pattern) ~= "string" then error("string.find: second argument must be a string", 2) end
local start_pos, end_pos = _orig_find(s, pattern, init, plain)
if start_pos then
return s:sub(start_pos, end_pos)
end
return nil
end
getmetatable("").__index.find = string.find
function string.find_all(s, pattern)
if type(s) ~= "string" then error("string.find_all: first argument must be a string", 2) end
if type(pattern) ~= "string" then error("string.find_all: second argument must be a string", 2) end
local matches = {}
for match in s:gmatch(pattern) do
table.insert(matches, match)
end
return matches
end
getmetatable("").__index.find_all = string.find_all
function string.to_number(s)
if type(s) ~= "string" then error("string.to_number: argument must be a string", 2) end
s = string.trim(s)
return tonumber(s)
end
getmetatable("").__index.to_number = string.to_number
function string.is_numeric(s)
if type(s) ~= "string" then error("string.is_numeric: argument must be a string", 2) end
s = string.trim(s)
return tonumber(s) ~= nil
end
getmetatable("").__index.is_numeric = string.is_numeric
function string.is_alpha(s)
if type(s) ~= "string" then error("string.is_alpha: argument must be a string", 2) end
if #s == 0 then return false end
return s:match("^%a+$") ~= nil
end
getmetatable("").__index.is_alpha = string.is_alpha
function string.is_alphanumeric(s)
if type(s) ~= "string" then error("string.is_alphanumeric: argument must be a string", 2) end
if #s == 0 then return false end
return s:match("^%w+$") ~= nil
end
getmetatable("").__index.is_alphanumeric = string.is_alphanumeric
function string.is_utf8(s)
if type(s) ~= "string" then error("string.is_utf8: argument must be a string", 2) end
return moonshark.string_is_valid_utf8(s)
end
getmetatable("").__index.is_utf8 = string.is_utf8
function string.is_empty(s)
return s == nil or s == ""
end
getmetatable("").__index.is_empty = string.is_empty
function string.is_blank(s)
return s == nil or s == "" or string.trim(s) == ""
end
getmetatable("").__index.is_blank = string.is_blank
function string.capitalize(s)
if type(s) ~= "string" then error("string.capitalize: argument must be a string", 2) end
return s:gsub("(%a)([%w_']*)", function(first, rest)
return first:upper() .. rest:lower()
end)
end
getmetatable("").__index.capitalize = string.capitalize
function string.camel_case(s)
if type(s) ~= "string" then error("string.camel_case: argument must be a string", 2) end
local words = string.words(s)
if #words == 0 then return s end
local result = words[1]:lower()
for i = 2, #words do
result = result .. words[i]:sub(1,1):upper() .. words[i]:sub(2):lower()
end
return result
end
getmetatable("").__index.camel_case = string.camel_case
function string.pascal_case(s)
if type(s) ~= "string" then error("string.pascal_case: argument must be a string", 2) end
local words = string.words(s)
local result = ""
for _, word in ipairs(words) do
result = result .. word:sub(1,1):upper() .. word:sub(2):lower()
end
return result
end
getmetatable("").__index.pascal_case = string.pascal_case
function string.snake_case(s)
if type(s) ~= "string" then error("string.snake_case: argument must be a string", 2) end
local words = string.words(s)
local result = {}
for _, word in ipairs(words) do
table.insert(result, word:lower())
end
return table.concat(result, "_")
end
getmetatable("").__index.snake_case = string.snake_case
function string.kebab_case(s)
if type(s) ~= "string" then error("string.kebab_case: argument must be a string", 2) end
local words = string.words(s)
local result = {}
for _, word in ipairs(words) do
table.insert(result, word:lower())
end
return table.concat(result, "-")
end
getmetatable("").__index.kebab_case = string.kebab_case
function string.screaming_snake_case(s)
if type(s) ~= "string" then error("string.screaming_snake_case: argument must be a string", 2) end
return string.snake_case(s):upper()
end
getmetatable("").__index.screaming_snake_case = string.screaming_snake_case
function string.center(s, width, fill_char)
if type(s) ~= "string" then error("string.center: first argument must be a string", 2) end
if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then
error("string.center: second argument must be a non-negative integer", 2)
end
fill_char = fill_char or " "
if type(fill_char) ~= "string" or #fill_char == 0 then
error("string.center: fill character must be a non-empty string", 2)
end
fill_char = fill_char:sub(1,1)
local len = string.length(s)
if len >= width then return s end
local pad_total = width - len
local pad_left = math.floor(pad_total / 2)
local pad_right = pad_total - pad_left
return string.rep(fill_char, pad_left) .. s .. string.rep(fill_char, pad_right)
end
getmetatable("").__index.center = string.center
function string.truncate(s, max_length, suffix)
if type(s) ~= "string" then error("string.truncate: first argument must be a string", 2) end
if type(max_length) ~= "number" or max_length < 0 or max_length ~= math.floor(max_length) then
error("string.truncate: second argument must be a non-negative integer", 2)
end
suffix = suffix or "..."
if type(suffix) ~= "string" then error("string.truncate: third argument must be a string", 2) end
local len = string.length(s)
if len <= max_length then return s end
local suffix_len = string.length(suffix)
if max_length <= suffix_len then
return string.slice(suffix, 1, max_length)
end
local main_part = string.slice(s, 1, max_length - suffix_len)
main_part = string.trim_right(main_part)
return main_part .. suffix
end
getmetatable("").__index.truncate = string.truncate
function string.wrap(s, width)
if type(s) ~= "string" then error("string.wrap: first argument must be a string", 2) end
if type(width) ~= "number" or width <= 0 or width ~= math.floor(width) then
error("string.wrap: second argument must be a positive integer", 2)
end
if s == "" then return {""} end
local words = string.words(s)
if #words == 0 then return {""} end
local lines = {}
local current_line = ""
for _, word in ipairs(words) do
if string.length(word) > width then
if current_line ~= "" then
table.insert(lines, current_line)
current_line = ""
end
table.insert(lines, word)
elseif current_line == "" then
current_line = word
elseif string.length(current_line) + 1 + string.length(word) <= width then
current_line = current_line .. " " .. word
else
table.insert(lines, current_line)
current_line = word
end
end
if current_line ~= "" then
table.insert(lines, current_line)
end
return lines
end
getmetatable("").__index.wrap = string.wrap
function string.dedent(s)
if type(s) ~= "string" then error("string.dedent: argument must be a string", 2) end
local lines = string.lines(s)
if #lines == 0 then return s end
local min_indent = math.huge
for _, line in ipairs(lines) do
if string.trim(line) ~= "" then
local indent = 0
for i = 1, #line do
if line:sub(i,i) == " " then
indent = indent + 1
else
break
end
end
min_indent = math.min(min_indent, indent)
end
end
if min_indent == math.huge then return s end
local result = {}
for _, line in ipairs(lines) do
if string.trim(line) == "" then
table.insert(result, "")
else
table.insert(result, line:sub(min_indent + 1))
end
end
return table.concat(result, "\n")
end
getmetatable("").__index.dedent = string.dedent
function string.escape(s)
if type(s) ~= "string" then error("string.escape: argument must be a string", 2) end
return (s:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"))
end
getmetatable("").__index.escape = string.escape
function string.shell_quote(s)
if type(s) ~= "string" then error("string.shell_quote: argument must be a string", 2) end
if s:match("^[%w%.%-_/]+$") then
return s
end
return "'" .. s:gsub("'", "'\"'\"'") .. "'"
end
getmetatable("").__index.shell_quote = string.shell_quote
function string.url_encode(s)
if type(s) ~= "string" then error("string.url_encode: argument must be a string", 2) end
return s:gsub("([^%w%-%.%_%~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
getmetatable("").__index.url_encode = string.url_encode
function string.url_decode(s)
if type(s) ~= "string" then error("string.url_decode: argument must be a string", 2) end
s = s:gsub("+", " ")
return s:gsub("%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end)
end
getmetatable("").__index.url_decode = string.url_decode
function string.slug(s)
if type(s) ~= "string" then error("string.slug: argument must be a string", 2) end
if s == "" then return "" end
local result = s:lower()
-- Remove accents first
result = string.remove_accents(result)
-- Keep only alphanumeric, spaces, and hyphens
result = result:gsub("[^%w%s%-]", "")
-- Replace spaces with hyphens
result = result:gsub("%s+", "-")
-- Remove duplicate hyphens
result = result:gsub("%-+", "-")
-- Remove leading/trailing hyphens
result = result:gsub("^%-", "")
result = result:gsub("%-$", "")
return result
end
getmetatable("").__index.slug = string.slug
function string.iequals(a, b)
if type(a) ~= "string" then error("string.iequals: first argument must be a string", 2) end
if type(b) ~= "string" then error("string.iequals: second argument must be a string", 2) end
return string.lower(a) == string.lower(b)
end
getmetatable("").__index.iequals = string.iequals
function string.is_whitespace(s)
if type(s) ~= "string" then error("string.is_whitespace: argument must be a string", 2) end
return s:match("^%s*$") ~= nil
end
getmetatable("").__index.is_whitespace = string.is_whitespace
function string.strip_whitespace(s)
if type(s) ~= "string" then error("string.strip_whitespace: argument must be a string", 2) end
return s:gsub("%s", "")
end
getmetatable("").__index.strip_whitespace = string.strip_whitespace
function string.normalize_whitespace(s)
if type(s) ~= "string" then error("string.normalize_whitespace: argument must be a string", 2) end
return string.trim((s:gsub("%s+", " ")))
end
getmetatable("").__index.normalize_whitespace = string.normalize_whitespace
function string.extract_numbers(s)
if type(s) ~= "string" then error("string.extract_numbers: argument must be a string", 2) end
local numbers = {}
for num in s:gmatch("%-?%d+%.?%d*") do
local n = tonumber(num)
if n then table.insert(numbers, n) end
end
return numbers
end
getmetatable("").__index.extract_numbers = string.extract_numbers
function string.remove_accents(s)
if type(s) ~= "string" then error("string.remove_accents: argument must be a string", 2) end
local accents = {
["á"] = "a", ["à"] = "a", ["ä"] = "a", ["â"] = "a", ["ã"] = "a", ["å"] = "a",
["Á"] = "A", ["À"] = "A", ["Ä"] = "A", ["Â"] = "A", ["Ã"] = "A", ["Å"] = "A",
["é"] = "e", ["è"] = "e", ["ë"] = "e", ["ê"] = "e",
["É"] = "E", ["È"] = "E", ["Ë"] = "E", ["Ê"] = "E",
["í"] = "i", ["ì"] = "i", ["ï"] = "i", ["î"] = "i",
["Í"] = "I", ["Ì"] = "I", ["Ï"] = "I", ["Î"] = "I",
["ó"] = "o", ["ò"] = "o", ["ö"] = "o", ["ô"] = "o", ["õ"] = "o",
["Ó"] = "O", ["Ò"] = "O", ["Ö"] = "O", ["Ô"] = "O", ["Õ"] = "O",
["ú"] = "u", ["ù"] = "u", ["ü"] = "u", ["û"] = "u",
["Ú"] = "U", ["Ù"] = "U", ["Ü"] = "U", ["Û"] = "U",
["ñ"] = "n", ["Ñ"] = "N",
["ç"] = "c", ["Ç"] = "C"
}
local result = s
for accented, plain in pairs(accents) do
result = result:gsub(accented, plain)
end
return result
end
getmetatable("").__index.remove_accents = string.remove_accents
function string.template(template_str, vars)
if type(template_str) ~= "string" then error("string.template: first argument must be a string", 2) end
if type(vars) ~= "table" then error("string.template: second argument must be a table", 2) end
return template_str:gsub("%${([%w_%.]+)}", function(path)
local value = vars
-- Handle simple variables (no dots)
if not path:match("%.") then
return tostring(value[path] or "")
end
-- Handle nested properties
for key in path:gmatch("[^%.]+") do
if type(value) == "table" and value[key] ~= nil then
value = value[key]
else
return ""
end
end
return tostring(value)
end)
end
getmetatable("").__index.template = string.template
function string.random(length, charset)
local result, err = moonshark.random_string(length, charset)
if not result then
error(err)
end
return result
end

View File

@ -1,715 +0,0 @@
local str = {}
-- Performance thresholds based on benchmark results
local REVERSE_THRESHOLD = 100 -- Use Go for strings longer than this
local LENGTH_THRESHOLD = 1000 -- Use Go for ASCII strings longer than this
-- ======================================================================
-- BASIC STRING OPERATIONS (Optimized Lua/Go hybrid)
-- ======================================================================
function str.split(s, delimiter)
if type(s) ~= "string" then error("str.split: first argument must be a string", 2) end
if type(delimiter) ~= "string" then error("str.split: second argument must be a string", 2) end
if delimiter == "" then
local result = {}
for i = 1, #s do
result[i] = s:sub(i, i)
end
return result
end
local result = {}
local start = 1
local delimiter_len = #delimiter
while true do
local pos = s:find(delimiter, start, true)
if not pos then
table.insert(result, s:sub(start))
break
end
table.insert(result, s:sub(start, pos - 1))
start = pos + delimiter_len
end
return result
end
function str.join(arr, separator)
if type(arr) ~= "table" then error("str.join: first argument must be a table", 2) end
if type(separator) ~= "string" then error("str.join: second argument must be a string", 2) end
return table.concat(arr, separator)
end
function str.trim(s)
if type(s) ~= "string" then error("str.trim: argument must be a string", 2) end
return s:match("^%s*(.-)%s*$")
end
function str.trim_left(s, cutset)
if type(s) ~= "string" then error("str.trim_left: first argument must be a string", 2) end
if cutset then
if type(cutset) ~= "string" then error("str.trim_left: second argument must be a string", 2) end
local pattern = "^[" .. cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") .. "]*"
return s:gsub(pattern, "")
else
return s:match("^%s*(.*)")
end
end
function str.trim_right(s, cutset)
if type(s) ~= "string" then error("str.trim_right: first argument must be a string", 2) end
if cutset then
if type(cutset) ~= "string" then error("str.trim_right: second argument must be a string", 2) end
local pattern = "[" .. cutset:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1") .. "]*$"
return s:gsub(pattern, "")
else
return s:match("(.-)%s*$")
end
end
function str.upper(s)
if type(s) ~= "string" then error("str.upper: argument must be a string", 2) end
return s:upper()
end
function str.lower(s)
if type(s) ~= "string" then error("str.lower: argument must be a string", 2) end
return s:lower()
end
function str.title(s)
if type(s) ~= "string" then error("str.title: argument must be a string", 2) end
return s:gsub("(%a)([%w_']*)", function(first, rest)
return first:upper() .. rest:lower()
end)
end
function str.contains(s, substr)
if type(s) ~= "string" then error("str.contains: first argument must be a string", 2) end
if type(substr) ~= "string" then error("str.contains: second argument must be a string", 2) end
return s:find(substr, 1, true) ~= nil
end
function str.starts_with(s, prefix)
if type(s) ~= "string" then error("str.starts_with: first argument must be a string", 2) end
if type(prefix) ~= "string" then error("str.starts_with: second argument must be a string", 2) end
return s:sub(1, #prefix) == prefix
end
function str.ends_with(s, suffix)
if type(s) ~= "string" then error("str.ends_with: first argument must be a string", 2) end
if type(suffix) ~= "string" then error("str.ends_with: second argument must be a string", 2) end
return s:sub(-#suffix) == suffix
end
function str.replace(s, old, new)
if type(s) ~= "string" then error("str.replace: first argument must be a string", 2) end
if type(old) ~= "string" then error("str.replace: second argument must be a string", 2) end
if type(new) ~= "string" then error("str.replace: third argument must be a string", 2) end
if old == "" then error("str.replace: cannot replace empty string", 2) end
return s:gsub(old:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"), new)
end
function str.replace_n(s, old, new, n)
if type(s) ~= "string" then error("str.replace_n: first argument must be a string", 2) end
if type(old) ~= "string" then error("str.replace_n: second argument must be a string", 2) end
if type(new) ~= "string" then error("str.replace_n: third argument must be a string", 2) end
if type(n) ~= "number" or n < 0 or n ~= math.floor(n) then
error("str.replace_n: fourth argument must be a non-negative integer", 2)
end
if old == "" then error("str.replace_n: cannot replace empty string", 2) end
local escaped = old:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
return (s:gsub(escaped, new, n))
end
function str.index(s, substr)
if type(s) ~= "string" then error("str.index: first argument must be a string", 2) end
if type(substr) ~= "string" then error("str.index: second argument must be a string", 2) end
local pos = s:find(substr, 1, true)
return pos
end
function str.last_index(s, substr)
if type(s) ~= "string" then error("str.last_index: first argument must be a string", 2) end
if type(substr) ~= "string" then error("str.last_index: second argument must be a string", 2) end
local last_pos = nil
local pos = 1
while true do
local found = s:find(substr, pos, true)
if not found then break end
last_pos = found
pos = found + 1
end
return last_pos
end
function str.count(s, substr)
if type(s) ~= "string" then error("str.count: first argument must be a string", 2) end
if type(substr) ~= "string" then error("str.count: second argument must be a string", 2) end
if substr == "" then return #s + 1 end
local count = 0
local pos = 1
while true do
local found = s:find(substr, pos, true)
if not found then break end
count = count + 1
pos = found + #substr
end
return count
end
function str.repeat_(s, n)
if type(s) ~= "string" then error("str.repeat_: first argument must be a string", 2) end
if type(n) ~= "number" or n < 0 or n ~= math.floor(n) then
error("str.repeat_: second argument must be a non-negative integer", 2)
end
return string.rep(s, n)
end
function str.reverse(s)
if type(s) ~= "string" then error("str.reverse: argument must be a string", 2) end
if #s > REVERSE_THRESHOLD then
local result, err = moonshark.string_reverse(s)
if not result then error("str.reverse: " .. err, 2) end
return result
else
local result = {}
for i = #s, 1, -1 do
result[#result + 1] = s:sub(i, i)
end
return table.concat(result)
end
end
function str.length(s)
if type(s) ~= "string" then error("str.length: argument must be a string", 2) end
-- For long ASCII strings, Go is faster. For unicode or short strings, use Go consistently
-- since UTF-8 handling is more reliable in Go
return moonshark.string_length(s)
end
function str.byte_length(s)
if type(s) ~= "string" then error("str.byte_length: argument must be a string", 2) end
return #s
end
function str.lines(s)
if type(s) ~= "string" then error("str.lines: argument must be a string", 2) end
if s == "" then return {""} end
s = s:gsub("\r\n", "\n"):gsub("\r", "\n")
local lines = {}
for line in (s .. "\n"):gmatch("([^\n]*)\n") do
table.insert(lines, line)
end
if #lines > 0 and lines[#lines] == "" then
table.remove(lines)
end
return lines
end
function str.words(s)
if type(s) ~= "string" then error("str.words: argument must be a string", 2) end
local words = {}
for word in s:gmatch("%S+") do
table.insert(words, word)
end
return words
end
function str.pad_left(s, width, pad_char)
if type(s) ~= "string" then error("str.pad_left: first argument must be a string", 2) end
if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then
error("str.pad_left: second argument must be a non-negative integer", 2)
end
pad_char = pad_char or " "
if type(pad_char) ~= "string" then error("str.pad_left: third argument must be a string", 2) end
if #pad_char == 0 then pad_char = " " else pad_char = pad_char:sub(1,1) end
local current_len = str.length(s)
if current_len >= width then return s end
return string.rep(pad_char, width - current_len) .. s
end
function str.pad_right(s, width, pad_char)
if type(s) ~= "string" then error("str.pad_right: first argument must be a string", 2) end
if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then
error("str.pad_right: second argument must be a non-negative integer", 2)
end
pad_char = pad_char or " "
if type(pad_char) ~= "string" then error("str.pad_right: third argument must be a string", 2) end
if #pad_char == 0 then pad_char = " " else pad_char = pad_char:sub(1,1) end
local current_len = str.length(s)
if current_len >= width then return s end
return s .. string.rep(pad_char, width - current_len)
end
function str.slice(s, start, end_pos)
if type(s) ~= "string" then error("str.slice: first argument must be a string", 2) end
if type(start) ~= "number" or start ~= math.floor(start) then
error("str.slice: second argument must be an integer", 2)
end
if end_pos ~= nil and (type(end_pos) ~= "number" or end_pos ~= math.floor(end_pos)) then
error("str.slice: third argument must be an integer", 2)
end
local result, err = moonshark.string_slice(s, start, end_pos)
if not result then error("str.slice: " .. err, 2) end
return result
end
-- ======================================================================
-- REGULAR EXPRESSIONS (Optimized Lua patterns)
-- ======================================================================
function str.match(pattern, s)
if type(pattern) ~= "string" then error("str.match: first argument must be a string", 2) end
if type(s) ~= "string" then error("str.match: second argument must be a string", 2) end
local lua_pattern = pattern:gsub("\\d", "%%d"):gsub("\\w", "%%w"):gsub("\\s", "%%s")
return s:match(lua_pattern) ~= nil
end
function str.find(pattern, s)
if type(pattern) ~= "string" then error("str.find: first argument must be a string", 2) end
if type(s) ~= "string" then error("str.find: second argument must be a string", 2) end
local lua_pattern = pattern:gsub("\\d", "%%d"):gsub("\\w", "%%w"):gsub("\\s", "%%s")
return s:match(lua_pattern)
end
function str.find_all(pattern, s)
if type(pattern) ~= "string" then error("str.find_all: first argument must be a string", 2) end
if type(s) ~= "string" then error("str.find_all: second argument must be a string", 2) end
local lua_pattern = pattern:gsub("\\d", "%%d"):gsub("\\w", "%%w"):gsub("\\s", "%%s")
local matches = {}
for match in s:gmatch(lua_pattern) do
table.insert(matches, match)
end
return matches
end
function str.gsub(pattern, s, replacement)
if type(pattern) ~= "string" then error("str.gsub: first argument must be a string", 2) end
if type(s) ~= "string" then error("str.gsub: second argument must be a string", 2) end
if type(replacement) ~= "string" then error("str.gsub: third argument must be a string", 2) end
-- Use Go for complex regex, Lua for simple patterns
if pattern:match("[%[%]%(%)%{%}%|%\\%^%$]") then
-- Complex pattern, use Go
return moonshark.regex_replace(pattern, s, replacement)
else
-- Simple pattern, use Lua
local lua_pattern = pattern:gsub("\\d", "%%d"):gsub("\\w", "%%w"):gsub("\\s", "%%s")
return s:gsub(lua_pattern, replacement)
end
end
-- ======================================================================
-- TYPE CONVERSION & VALIDATION
-- ======================================================================
function str.to_number(s)
if type(s) ~= "string" then error("str.to_number: argument must be a string", 2) end
s = str.trim(s)
return tonumber(s)
end
function str.is_numeric(s)
if type(s) ~= "string" then error("str.is_numeric: argument must be a string", 2) end
s = str.trim(s)
return tonumber(s) ~= nil
end
function str.is_alpha(s)
if type(s) ~= "string" then error("str.is_alpha: argument must be a string", 2) end
if #s == 0 then return false end
return s:match("^%a+$") ~= nil
end
function str.is_alphanumeric(s)
if type(s) ~= "string" then error("str.is_alphanumeric: argument must be a string", 2) end
if #s == 0 then return false end
return s:match("^%w+$") ~= nil
end
function str.is_empty(s)
return s == nil or s == ""
end
function str.is_blank(s)
return str.is_empty(s) or str.trim(s) == ""
end
function str.is_utf8(s)
if type(s) ~= "string" then error("str.is_utf8: argument must be a string", 2) end
return moonshark.string_is_valid_utf8(s)
end
-- ======================================================================
-- ADVANCED STRING OPERATIONS (Pure Lua)
-- ======================================================================
function str.capitalize(s)
if type(s) ~= "string" then error("str.capitalize: argument must be a string", 2) end
return s:gsub("(%a)([%w_']*)", function(first, rest)
return first:upper() .. rest:lower()
end)
end
function str.camel_case(s)
if type(s) ~= "string" then error("str.camel_case: argument must be a string", 2) end
local words = str.words(s)
if #words == 0 then return s end
local result = words[1]:lower()
for i = 2, #words do
result = result .. words[i]:sub(1,1):upper() .. words[i]:sub(2):lower()
end
return result
end
function str.pascal_case(s)
if type(s) ~= "string" then error("str.pascal_case: argument must be a string", 2) end
local words = str.words(s)
local result = ""
for _, word in ipairs(words) do
result = result .. word:sub(1,1):upper() .. word:sub(2):lower()
end
return result
end
function str.snake_case(s)
if type(s) ~= "string" then error("str.snake_case: argument must be a string", 2) end
local words = str.words(s)
local result = {}
for _, word in ipairs(words) do
table.insert(result, word:lower())
end
return table.concat(result, "_")
end
function str.kebab_case(s)
if type(s) ~= "string" then error("str.kebab_case: argument must be a string", 2) end
local words = str.words(s)
local result = {}
for _, word in ipairs(words) do
table.insert(result, word:lower())
end
return table.concat(result, "-")
end
function str.center(s, width, fill_char)
if type(s) ~= "string" then error("str.center: first argument must be a string", 2) end
if type(width) ~= "number" or width < 0 or width ~= math.floor(width) then
error("str.center: second argument must be a non-negative integer", 2)
end
fill_char = fill_char or " "
if type(fill_char) ~= "string" or #fill_char == 0 then
error("str.center: fill character must be a non-empty string", 2)
end
fill_char = fill_char:sub(1,1)
local len = str.length(s)
if len >= width then return s end
local pad_total = width - len
local pad_left = math.floor(pad_total / 2)
local pad_right = pad_total - pad_left
return string.rep(fill_char, pad_left) .. s .. string.rep(fill_char, pad_right)
end
function str.truncate(s, max_length, suffix)
if type(s) ~= "string" then error("str.truncate: first argument must be a string", 2) end
if type(max_length) ~= "number" or max_length < 0 or max_length ~= math.floor(max_length) then
error("str.truncate: second argument must be a non-negative integer", 2)
end
suffix = suffix or "..."
if type(suffix) ~= "string" then error("str.truncate: third argument must be a string", 2) end
local len = str.length(s)
if len <= max_length then return s end
local suffix_len = str.length(suffix)
if max_length <= suffix_len then
return str.slice(suffix, 1, max_length)
end
local main_part = str.slice(s, 1, max_length - suffix_len)
main_part = str.trim_right(main_part)
return main_part .. suffix
end
function str.escape_regex(s)
if type(s) ~= "string" then error("str.escape_regex: argument must be a string", 2) end
return s:gsub("([%.%+%*%?%[%]%^%$%(%)%{%}%|%\\])", "\\%1")
end
function str.url_encode(s)
if type(s) ~= "string" then error("str.url_encode: argument must be a string", 2) end
return s:gsub("([^%w%-%.%_%~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
function str.url_decode(s)
if type(s) ~= "string" then error("str.url_decode: argument must be a string", 2) end
local result = s:gsub("%%(%x%x)", function(hex)
local byte = tonumber(hex, 16)
return byte and string.char(byte) or ("%" .. hex)
end):gsub("+", " ")
if not str.is_utf8(result) then
error("str.url_decode: result is not valid UTF-8", 2)
end
return result
end
function str.distance(a, b)
if type(a) ~= "string" then error("str.distance: first argument must be a string", 2) end
if type(b) ~= "string" then error("str.distance: second argument must be a string", 2) end
local len_a, len_b = str.length(a), str.length(b)
if len_a == 0 then return len_b end
if len_b == 0 then return len_a end
if len_a > 1000 or len_b > 1000 then
error("str.distance: strings too long for distance calculation", 2)
end
local matrix = {}
for i = 0, len_a do
matrix[i] = {[0] = i}
end
for j = 0, len_b do
matrix[0][j] = j
end
for i = 1, len_a do
for j = 1, len_b do
local cost = (str.slice(a, i, i) == str.slice(b, j, j)) and 0 or 1
matrix[i][j] = math.min(
matrix[i-1][j] + 1,
matrix[i][j-1] + 1,
matrix[i-1][j-1] + cost
)
end
end
return matrix[len_a][len_b]
end
function str.similarity(a, b)
if type(a) ~= "string" then error("str.similarity: first argument must be a string", 2) end
if type(b) ~= "string" then error("str.similarity: second argument must be a string", 2) end
local max_len = math.max(str.length(a), str.length(b))
if max_len == 0 then return 1.0 end
local dist = str.distance(a, b)
return 1.0 - (dist / max_len)
end
function str.template(template, vars)
if type(template) ~= "string" then error("str.template: first argument must be a string", 2) end
vars = vars or {}
if type(vars) ~= "table" then error("str.template: second argument must be a table", 2) end
return template:gsub("%${([%w_]+)}", function(var)
local value = vars[var]
return value ~= nil and tostring(value) or ""
end)
end
function str.random(length, charset)
if type(length) ~= "number" or length < 0 or length ~= math.floor(length) then
error("str.random: first argument must be a non-negative integer", 2)
end
if charset ~= nil and type(charset) ~= "string" then
error("str.random: second argument must be a string", 2)
end
charset = charset or "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
local result = {}
math.randomseed(os.time() + os.clock() * 1000000)
for i = 1, length do
local rand_index = math.random(1, #charset)
result[i] = charset:sub(rand_index, rand_index)
end
return table.concat(result)
end
function str.slug(s)
if type(s) ~= "string" then error("str.slug: argument must be a string", 2) end
local result = str.remove_accents(s):lower()
result = result:gsub("[^%w%s]", "")
result = result:gsub("%s+", "-")
result = result:gsub("^%-+", ""):gsub("%-+$", "")
return result
end
-- Add these functions to the end of string.lua, before the return statement
function str.screaming_snake_case(s)
if type(s) ~= "string" then error("str.screaming_snake_case: argument must be a string", 2) end
return str.snake_case(s):upper()
end
function str.wrap(s, width)
if type(s) ~= "string" then error("str.wrap: first argument must be a string", 2) end
if type(width) ~= "number" or width <= 0 then error("str.wrap: width must be positive number", 2) end
local words = str.words(s)
local lines = {}
local current_line = ""
for _, word in ipairs(words) do
if current_line == "" then
current_line = word
elseif str.length(current_line .. " " .. word) <= width then
current_line = current_line .. " " .. word
else
table.insert(lines, current_line)
current_line = word
end
end
if current_line ~= "" then
table.insert(lines, current_line)
end
return lines
end
function str.dedent(s)
if type(s) ~= "string" then error("str.dedent: argument must be a string", 2) end
local lines = str.lines(s)
if #lines == 0 then return "" end
-- Find minimum indentation
local min_indent = math.huge
for _, line in ipairs(lines) do
if line:match("%S") then -- Non-empty line
local indent = line:match("^(%s*)")
min_indent = math.min(min_indent, #indent)
end
end
if min_indent == math.huge then min_indent = 0 end
-- Remove common indentation
local result = {}
for _, line in ipairs(lines) do
table.insert(result, line:sub(min_indent + 1))
end
return table.concat(result, "\n")
end
function str.shell_quote(s)
if type(s) ~= "string" then error("str.shell_quote: argument must be a string", 2) end
if s:match("^[%w%-%./]+$") then
return s -- No quoting needed
end
-- Replace single quotes with '"'"'
local quoted = s:gsub("'", "'\"'\"'")
return "'" .. quoted .. "'"
end
function str.iequals(a, b)
if type(a) ~= "string" then error("str.iequals: first argument must be a string", 2) end
if type(b) ~= "string" then error("str.iequals: second argument must be a string", 2) end
return str.lower(a) == str.lower(b)
end
function str.template_advanced(template, context)
if type(template) ~= "string" then error("str.template_advanced: first argument must be a string", 2) end
context = context or {}
if type(context) ~= "table" then error("str.template_advanced: second argument must be a table", 2) end
return template:gsub("%${([%w_.]+)}", function(path)
local keys = str.split(path, ".")
local value = context
for _, key in ipairs(keys) do
if type(value) == "table" and value[key] ~= nil then
value = value[key]
else
return ""
end
end
return tostring(value)
end)
end
function str.is_whitespace(s)
if type(s) ~= "string" then error("str.is_whitespace: argument must be a string", 2) end
return s:match("^%s*$") ~= nil
end
function str.strip_whitespace(s)
if type(s) ~= "string" then error("str.strip_whitespace: argument must be a string", 2) end
return s:gsub("%s", "")
end
function str.normalize_whitespace(s)
if type(s) ~= "string" then error("str.normalize_whitespace: argument must be a string", 2) end
return str.trim(s:gsub("%s+", " "))
end
function str.extract_numbers(s)
if type(s) ~= "string" then error("str.extract_numbers: argument must be a string", 2) end
local numbers = {}
for match in s:gmatch("%-?%d+%.?%d*") do
local num = tonumber(match)
if num then
table.insert(numbers, num)
end
end
return numbers
end
function str.remove_accents(s)
if type(s) ~= "string" then error("str.remove_accents: argument must be a string", 2) end
local accents = {
["à"] = "a", ["á"] = "a", ["â"] = "a", ["ã"] = "a", ["ä"] = "a", ["å"] = "a",
["è"] = "e", ["é"] = "e", ["ê"] = "e", ["ë"] = "e",
["ì"] = "i", ["í"] = "i", ["î"] = "i", ["ï"] = "i",
["ò"] = "o", ["ó"] = "o", ["ô"] = "o", ["õ"] = "o", ["ö"] = "o",
["ù"] = "u", ["ú"] = "u", ["û"] = "u", ["ü"] = "u",
["ñ"] = "n", ["ç"] = "c", ["ÿ"] = "y",
["À"] = "A", ["Á"] = "A", ["Â"] = "A", ["Ã"] = "A", ["Ä"] = "A", ["Å"] = "A",
["È"] = "E", ["É"] = "E", ["Ê"] = "E", ["Ë"] = "E",
["Ì"] = "I", ["Í"] = "I", ["Î"] = "I", ["Ï"] = "I",
["Ò"] = "O", ["Ó"] = "O", ["Ô"] = "O", ["Õ"] = "O", ["Ö"] = "O",
["Ù"] = "U", ["Ú"] = "U", ["Û"] = "U", ["Ü"] = "U",
["Ñ"] = "N", ["Ç"] = "C", ["Ÿ"] = "Y"
}
local result = s
for accented, plain in pairs(accents) do
result = result:gsub(accented, plain)
end
return result
end
return str

View File

@ -1,61 +1,56 @@
local tbl = {} local orig_insert = table.insert
local orig_remove = table.remove
local orig_concat = table.concat
local orig_sort = table.sort
-- ====================================================================== function table.insert(t, pos, value)
-- BUILT-IN TABLE FUNCTIONS (Lua 5.1 wrappers for consistency) if type(t) ~= "table" then error("table.insert: first argument must be a table", 2) end
-- ======================================================================
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 if value == nil then
-- table.insert(t, value) form -- table.insert(t, value) form
table.insert(t, pos) orig_insert(t, pos)
else else
-- table.insert(t, pos, value) form -- table.insert(t, pos, value) form
if type(pos) ~= "number" or pos ~= math.floor(pos) then if type(pos) ~= "number" or pos ~= math.floor(pos) then
error("tbl.insert: position must be an integer", 2) error("table.insert: position must be an integer", 2)
end end
table.insert(t, pos, value) orig_insert(t, pos, value)
end end
end end
function tbl.remove(t, pos) function table.remove(t, pos)
if type(t) ~= "table" then error("tbl.remove: first argument must be a table", 2) end if type(t) ~= "table" then error("table.remove: first argument must be a table", 2) end
if pos ~= nil and (type(pos) ~= "number" or pos ~= math.floor(pos)) then if pos ~= nil and (type(pos) ~= "number" or pos ~= math.floor(pos)) then
error("tbl.remove: position must be an integer", 2) error("table.remove: position must be an integer", 2)
end end
return table.remove(t, pos) return orig_remove(t, pos)
end end
function tbl.concat(t, sep, start_idx, end_idx) function table.concat(t, sep, start_idx, end_idx)
if type(t) ~= "table" then error("tbl.concat: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 sep ~= nil and type(sep) ~= "string" then error("table.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 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) error("table.concat: start index must be an integer", 2)
end end
if end_idx ~= nil and (type(end_idx) ~= "number" or end_idx ~= math.floor(end_idx)) then 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) error("table.concat: end index must be an integer", 2)
end end
return table.concat(t, sep, start_idx, end_idx) return orig_concat(t, sep, start_idx, end_idx)
end end
function tbl.sort(t, comp) function table.sort(t, comp)
if type(t) ~= "table" then error("tbl.sort: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if comp ~= nil and type(comp) ~= "function" then error("table.sort: comparator must be a function", 2) end
table.sort(t, comp) orig_sort(t, comp)
end end
-- ====================================================================== function table.length(t)
-- BASIC TABLE OPERATIONS if type(t) ~= "table" then error("table.length: argument must be a table", 2) end
-- ======================================================================
function tbl.length(t)
if type(t) ~= "table" then error("tbl.length: argument must be a table", 2) end
return #t return #t
end end
function tbl.size(t) function table.size(t)
if type(t) ~= "table" then error("tbl.size: argument must be a table", 2) end if type(t) ~= "table" then error("table.size: argument must be a table", 2) end
local count = 0 local count = 0
for _ in pairs(t) do for _ in pairs(t) do
count = count + 1 count = count + 1
@ -63,14 +58,14 @@ function tbl.size(t)
return count return count
end end
function tbl.is_empty(t) function table.is_empty(t)
if type(t) ~= "table" then error("tbl.is_empty: argument must be a table", 2) end if type(t) ~= "table" then error("table.is_empty: argument must be a table", 2) end
return next(t) == nil return next(t) == nil
end end
function tbl.is_array(t) function table.is_array(t)
if type(t) ~= "table" then error("tbl.is_array: argument must be a table", 2) end if type(t) ~= "table" then error("table.is_array: argument must be a table", 2) end
if tbl.is_empty(t) then return true end if table.is_empty(t) then return true end
local max_index = 0 local max_index = 0
local count = 0 local count = 0
@ -84,15 +79,15 @@ function tbl.is_array(t)
return max_index == count return max_index == count
end end
function tbl.clear(t) function table.clear(t)
if type(t) ~= "table" then error("tbl.clear: argument must be a table", 2) end if type(t) ~= "table" then error("table.clear: argument must be a table", 2) end
for k in pairs(t) do for k in pairs(t) do
t[k] = nil t[k] = nil
end end
end end
function tbl.clone(t) function table.clone(t)
if type(t) ~= "table" then error("tbl.clone: argument must be a table", 2) end if type(t) ~= "table" then error("table.clone: argument must be a table", 2) end
local result = {} local result = {}
for k, v in pairs(t) do for k, v in pairs(t) do
result[k] = v result[k] = v
@ -100,8 +95,8 @@ function tbl.clone(t)
return result return result
end end
function tbl.deep_copy(t) function table.deep_copy(t)
if type(t) ~= "table" then error("tbl.deep_copy: argument must be a table", 2) end if type(t) ~= "table" then error("table.deep_copy: argument must be a table", 2) end
local function copy_recursive(obj, seen) local function copy_recursive(obj, seen)
if type(obj) ~= "table" then return obj end if type(obj) ~= "table" then return obj end
@ -120,29 +115,25 @@ function tbl.deep_copy(t)
return copy_recursive(t, {}) return copy_recursive(t, {})
end end
-- ====================================================================== function table.contains(t, value)
-- SEARCHING AND FINDING if type(t) ~= "table" then error("table.contains: first argument must be a table", 2) end
-- ======================================================================
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 for _, v in pairs(t) do
if v == value then return true end if v == value then return true end
end end
return false return false
end end
function tbl.index_of(t, value) function table.index_of(t, value)
if type(t) ~= "table" then error("tbl.index_of: first argument must be a table", 2) end if type(t) ~= "table" then error("table.index_of: first argument must be a table", 2) end
for k, v in pairs(t) do for k, v in pairs(t) do
if v == value then return k end if v == value then return k end
end end
return nil return nil
end end
function tbl.find(t, predicate) function table.find(t, predicate)
if type(t) ~= "table" then error("tbl.find: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if type(predicate) ~= "function" then error("table.find: second argument must be a function", 2) end
for k, v in pairs(t) do for k, v in pairs(t) do
if predicate(v, k, t) then return v, k end if predicate(v, k, t) then return v, k end
@ -150,9 +141,9 @@ function tbl.find(t, predicate)
return nil return nil
end end
function tbl.find_index(t, predicate) function table.find_index(t, predicate)
if type(t) ~= "table" then error("tbl.find_index: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if type(predicate) ~= "function" then error("table.find_index: second argument must be a function", 2) end
for k, v in pairs(t) do for k, v in pairs(t) do
if predicate(v, k, t) then return k end if predicate(v, k, t) then return k end
@ -160,8 +151,8 @@ function tbl.find_index(t, predicate)
return nil return nil
end end
function tbl.count(t, value_or_predicate) function table.count(t, value_or_predicate)
if type(t) ~= "table" then error("tbl.count: first argument must be a table", 2) end if type(t) ~= "table" then error("table.count: first argument must be a table", 2) end
local count = 0 local count = 0
if type(value_or_predicate) == "function" then if type(value_or_predicate) == "function" then
@ -176,16 +167,12 @@ function tbl.count(t, value_or_predicate)
return count return count
end end
-- ====================================================================== function table.filter(t, predicate)
-- FILTERING AND MAPPING if type(t) ~= "table" then error("table.filter: first argument must be a table", 2) end
-- ====================================================================== if type(predicate) ~= "function" then error("table.filter: second argument must be a function", 2) end
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 = {} local result = {}
if tbl.is_array(t) then if table.is_array(t) then
local max_index = 0 local max_index = 0
for k in pairs(t) do for k in pairs(t) do
if type(k) == "number" and k > max_index then if type(k) == "number" and k > max_index then
@ -195,7 +182,7 @@ function tbl.filter(t, predicate)
for i = 1, max_index do for i = 1, max_index do
local v = t[i] local v = t[i]
if v ~= nil and predicate(v, i, t) then if v ~= nil and predicate(v, i, t) then
table.insert(result, v) orig_insert(result, v)
end end
end end
else else
@ -208,16 +195,16 @@ function tbl.filter(t, predicate)
return result return result
end end
function tbl.reject(t, predicate) function table.reject(t, predicate)
if type(t) ~= "table" then error("tbl.reject: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if type(predicate) ~= "function" then error("table.reject: second argument must be a function", 2) end
return tbl.filter(t, function(v, k, tbl) return not predicate(v, k, tbl) end) return table.filter(t, function(v, k, tbl) return not predicate(v, k, tbl) end)
end end
function tbl.map(t, transformer) function table.map(t, transformer)
if type(t) ~= "table" then error("tbl.map: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if type(transformer) ~= "function" then error("table.map: second argument must be a function", 2) end
local result = {} local result = {}
for k, v in pairs(t) do for k, v in pairs(t) do
@ -226,9 +213,9 @@ function tbl.map(t, transformer)
return result return result
end end
function tbl.map_values(t, transformer) function table.map_values(t, transformer)
if type(t) ~= "table" then error("tbl.map_values: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if type(transformer) ~= "function" then error("table.map_values: second argument must be a function", 2) end
local result = {} local result = {}
for k, v in pairs(t) do for k, v in pairs(t) do
@ -237,9 +224,9 @@ function tbl.map_values(t, transformer)
return result return result
end end
function tbl.map_keys(t, transformer) function table.map_keys(t, transformer)
if type(t) ~= "table" then error("tbl.map_keys: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if type(transformer) ~= "function" then error("table.map_keys: second argument must be a function", 2) end
local result = {} local result = {}
for k, v in pairs(t) do for k, v in pairs(t) do
@ -249,13 +236,9 @@ function tbl.map_keys(t, transformer)
return result return result
end end
-- ====================================================================== function table.reduce(t, reducer, initial)
-- REDUCING AND AGGREGATING if type(t) ~= "table" then error("table.reduce: first argument must be a table", 2) end
-- ====================================================================== if type(reducer) ~= "function" then error("table.reduce: second argument must be a function", 2) end
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 accumulator = initial
local started = initial ~= nil local started = initial ~= nil
@ -270,39 +253,39 @@ function tbl.reduce(t, reducer, initial)
end end
if not started then if not started then
error("tbl.reduce: empty table with no initial value", 2) error("table.reduce: empty table with no initial value", 2)
end end
return accumulator return accumulator
end end
function tbl.sum(t) function table.sum(t)
if type(t) ~= "table" then error("tbl.sum: argument must be a table", 2) end if type(t) ~= "table" then error("table.sum: argument must be a table", 2) end
local total = 0 local total = 0
for _, v in pairs(t) do for _, v in pairs(t) do
if type(v) ~= "number" then error("tbl.sum: all values must be numbers", 2) end if type(v) ~= "number" then error("table.sum: all values must be numbers", 2) end
total = total + v total = total + v
end end
return total return total
end end
function tbl.product(t) function table.product(t)
if type(t) ~= "table" then error("tbl.product: argument must be a table", 2) end if type(t) ~= "table" then error("table.product: argument must be a table", 2) end
local result = 1 local result = 1
for _, v in pairs(t) do for _, v in pairs(t) do
if type(v) ~= "number" then error("tbl.product: all values must be numbers", 2) end if type(v) ~= "number" then error("table.product: all values must be numbers", 2) end
result = result * v result = result * v
end end
return result return result
end end
function tbl.min(t) function table.min(t)
if type(t) ~= "table" then error("tbl.min: argument must be a table", 2) end if type(t) ~= "table" then error("table.min: argument must be a table", 2) end
if tbl.is_empty(t) then error("tbl.min: table is empty", 2) end if table.is_empty(t) then error("table.min: table is empty", 2) end
local min_val = nil local min_val = nil
for _, v in pairs(t) do for _, v in pairs(t) do
if type(v) ~= "number" then error("tbl.min: all values must be numbers", 2) end if type(v) ~= "number" then error("table.min: all values must be numbers", 2) end
if min_val == nil or v < min_val then if min_val == nil or v < min_val then
min_val = v min_val = v
end end
@ -310,13 +293,13 @@ function tbl.min(t)
return min_val return min_val
end end
function tbl.max(t) function table.max(t)
if type(t) ~= "table" then error("tbl.max: argument must be a table", 2) end if type(t) ~= "table" then error("table.max: argument must be a table", 2) end
if tbl.is_empty(t) then error("tbl.max: table is empty", 2) end if table.is_empty(t) then error("table.max: table is empty", 2) end
local max_val = nil local max_val = nil
for _, v in pairs(t) do for _, v in pairs(t) do
if type(v) ~= "number" then error("tbl.max: all values must be numbers", 2) end if type(v) ~= "number" then error("table.max: all values must be numbers", 2) end
if max_val == nil or v > max_val then if max_val == nil or v > max_val then
max_val = v max_val = v
end end
@ -324,21 +307,17 @@ function tbl.max(t)
return max_val return max_val
end end
function tbl.average(t) function table.average(t)
if type(t) ~= "table" then error("tbl.average: argument must be a table", 2) end if type(t) ~= "table" then error("table.average: argument must be a table", 2) end
if tbl.is_empty(t) then error("tbl.average: table is empty", 2) end if table.is_empty(t) then error("table.average: table is empty", 2) end
return tbl.sum(t) / tbl.size(t) return table.sum(t) / table.size(t)
end end
-- ====================================================================== function table.all(t, predicate)
-- BOOLEAN OPERATIONS if type(t) ~= "table" then error("table.all: first argument must be a table", 2) end
-- ======================================================================
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 predicate then
if type(predicate) ~= "function" then error("tbl.all: second argument must be a function", 2) end if type(predicate) ~= "function" then error("table.all: second argument must be a function", 2) end
for k, v in pairs(t) do for k, v in pairs(t) do
if not predicate(v, k, t) then return false end if not predicate(v, k, t) then return false end
end end
@ -350,11 +329,11 @@ function tbl.all(t, predicate)
return true return true
end end
function tbl.any(t, predicate) function table.any(t, predicate)
if type(t) ~= "table" then error("tbl.any: first argument must be a table", 2) end if type(t) ~= "table" then error("table.any: first argument must be a table", 2) end
if predicate then if predicate then
if type(predicate) ~= "function" then error("tbl.any: second argument must be a function", 2) end if type(predicate) ~= "function" then error("table.any: second argument must be a function", 2) end
for k, v in pairs(t) do for k, v in pairs(t) do
if predicate(v, k, t) then return true end if predicate(v, k, t) then return true end
end end
@ -366,26 +345,22 @@ function tbl.any(t, predicate)
return false return false
end end
function tbl.none(t, predicate) function table.none(t, predicate)
if type(t) ~= "table" then error("tbl.none: first argument must be a table", 2) end if type(t) ~= "table" then error("table.none: first argument must be a table", 2) end
return not tbl.any(t, predicate) return not table.any(t, predicate)
end end
-- ====================================================================== function table.unique(t)
-- SET OPERATIONS if type(t) ~= "table" then error("table.unique: argument must be a table", 2) end
-- ======================================================================
function tbl.unique(t)
if type(t) ~= "table" then error("tbl.unique: argument must be a table", 2) end
local seen = {} local seen = {}
local result = {} local result = {}
if tbl.is_array(t) then if table.is_array(t) then
for _, v in ipairs(t) do for _, v in ipairs(t) do
if not seen[v] then if not seen[v] then
seen[v] = true seen[v] = true
table.insert(result, v) orig_insert(result, v)
end end
end end
else else
@ -400,9 +375,9 @@ function tbl.unique(t)
return result return result
end end
function tbl.intersection(t1, t2) function table.intersection(t1, t2)
if type(t1) ~= "table" then error("tbl.intersection: first argument must be a table", 2) end if type(t1) ~= "table" then error("table.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 if type(t2) ~= "table" then error("table.intersection: second argument must be a table", 2) end
local set2 = {} local set2 = {}
for _, v in pairs(t2) do for _, v in pairs(t2) do
@ -410,10 +385,10 @@ function tbl.intersection(t1, t2)
end end
local result = {} local result = {}
if tbl.is_array(t1) then if table.is_array(t1) then
for _, v in ipairs(t1) do for _, v in ipairs(t1) do
if set2[v] then if set2[v] then
table.insert(result, v) orig_insert(result, v)
end end
end end
else else
@ -427,16 +402,16 @@ function tbl.intersection(t1, t2)
return result return result
end end
function tbl.union(t1, t2) function table.union(t1, t2)
if type(t1) ~= "table" then error("tbl.union: first argument must be a table", 2) end if type(t1) ~= "table" then error("table.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 if type(t2) ~= "table" then error("table.union: second argument must be a table", 2) end
local result = tbl.clone(t1) local result = table.clone(t1)
if tbl.is_array(t1) and tbl.is_array(t2) then if table.is_array(t1) and table.is_array(t2) then
for _, v in ipairs(t2) do for _, v in ipairs(t2) do
if not tbl.contains(result, v) then if not table.contains(result, v) then
table.insert(result, v) orig_insert(result, v)
end end
end end
else else
@ -450,25 +425,21 @@ function tbl.union(t1, t2)
return result return result
end end
function tbl.difference(t1, t2) function table.difference(t1, t2)
if type(t1) ~= "table" then error("tbl.difference: first argument must be a table", 2) end if type(t1) ~= "table" then error("table.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 if type(t2) ~= "table" then error("table.difference: second argument must be a table", 2) end
local set2 = {} local set2 = {}
for _, v in pairs(t2) do for _, v in pairs(t2) do
set2[v] = true set2[v] = true
end end
return tbl.filter(t1, function(v) return not set2[v] end) return table.filter(t1, function(v) return not set2[v] end)
end end
-- ====================================================================== function table.reverse(t)
-- ARRAY OPERATIONS if type(t) ~= "table" then error("table.reverse: argument must be a table", 2) end
-- ====================================================================== if not table.is_array(t) then error("table.reverse: argument must be an array", 2) end
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 result = {}
local len = #t local len = #t
@ -478,11 +449,11 @@ function tbl.reverse(t)
return result return result
end end
function tbl.shuffle(t) function table.shuffle(t)
if type(t) ~= "table" then error("tbl.shuffle: argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if not table.is_array(t) then error("table.shuffle: argument must be an array", 2) end
local result = tbl.clone(t) local result = table.clone(t)
local len = #result local len = #result
math.randomseed(os.time() + os.clock() * 1000000) math.randomseed(os.time() + os.clock() * 1000000)
@ -495,18 +466,18 @@ function tbl.shuffle(t)
return result return result
end end
function tbl.rotate(t, positions) function table.rotate(t, positions)
if type(t) ~= "table" then error("tbl.rotate: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 not table.is_array(t) then error("table.rotate: first argument must be an array", 2) end
if type(positions) ~= "number" or positions ~= math.floor(positions) then if type(positions) ~= "number" or positions ~= math.floor(positions) then
error("tbl.rotate: second argument must be an integer", 2) error("table.rotate: second argument must be an integer", 2)
end end
local len = #t local len = #t
if len == 0 then return {} end if len == 0 then return {} end
positions = positions % len positions = positions % len
if positions == 0 then return tbl.clone(t) end if positions == 0 then return table.clone(t) end
local result = {} local result = {}
for i = 1, len do for i = 1, len do
@ -517,14 +488,14 @@ function tbl.rotate(t, positions)
return result return result
end end
function tbl.slice(t, start_idx, end_idx) function table.slice(t, start_idx, end_idx)
if type(t) ~= "table" then error("tbl.slice: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 not table.is_array(t) then error("table.slice: first argument must be an array", 2) end
if type(start_idx) ~= "number" or start_idx ~= math.floor(start_idx) then if type(start_idx) ~= "number" or start_idx ~= math.floor(start_idx) then
error("tbl.slice: start index must be an integer", 2) error("table.slice: start index must be an integer", 2)
end end
if end_idx ~= nil and (type(end_idx) ~= "number" or end_idx ~= math.floor(end_idx)) then 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) error("table.slice: end index must be an integer", 2)
end end
local len = #t local len = #t
@ -536,20 +507,20 @@ function tbl.slice(t, start_idx, end_idx)
local result = {} local result = {}
for i = start_idx, end_idx do for i = start_idx, end_idx do
table.insert(result, t[i]) orig_insert(result, t[i])
end end
return result return result
end end
function tbl.splice(t, start_idx, delete_count, ...) function table.splice(t, start_idx, delete_count, ...)
if type(t) ~= "table" then error("tbl.splice: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 not table.is_array(t) then error("table.splice: first argument must be an array", 2) end
if type(start_idx) ~= "number" or start_idx ~= math.floor(start_idx) then if type(start_idx) ~= "number" or start_idx ~= math.floor(start_idx) then
error("tbl.splice: start index must be an integer", 2) error("table.splice: start index must be an integer", 2)
end end
if delete_count ~= nil and (type(delete_count) ~= "number" or delete_count ~= math.floor(delete_count) or delete_count < 0) then 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) error("table.splice: delete count must be a non-negative integer", 2)
end end
local len = #t local len = #t
@ -593,26 +564,22 @@ function tbl.splice(t, start_idx, delete_count, ...)
return deleted return deleted
end end
-- ====================================================================== function table.sort_by(t, key_func)
-- SORTING HELPERS if type(t) ~= "table" then error("table.sort_by: first argument must be a table", 2) end
-- ====================================================================== if not table.is_array(t) then error("table.sort_by: first argument must be an array", 2) end
if type(key_func) ~= "function" then error("table.sort_by: second argument must be a function", 2) end
function tbl.sort_by(t, key_func) local result = table.clone(t)
if type(t) ~= "table" then error("tbl.sort_by: first argument must be a table", 2) end orig_sort(result, function(a, b)
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) return key_func(a) < key_func(b)
end) end)
return result return result
end end
function tbl.is_sorted(t, comp) function table.is_sorted(t, comp)
if type(t) ~= "table" then error("tbl.is_sorted: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 not table.is_array(t) then error("table.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 if comp ~= nil and type(comp) ~= "function" then error("table.is_sorted: comparator must be a function", 2) end
comp = comp or function(a, b) return a < b end comp = comp or function(a, b) return a < b end
@ -624,47 +591,43 @@ function tbl.is_sorted(t, comp)
return true return true
end end
-- ====================================================================== function table.keys(t)
-- UTILITY FUNCTIONS if type(t) ~= "table" then error("table.keys: argument must be a table", 2) end
-- ======================================================================
function tbl.keys(t)
if type(t) ~= "table" then error("tbl.keys: argument must be a table", 2) end
local result = {} local result = {}
for k, _ in pairs(t) do for k, _ in pairs(t) do
table.insert(result, k) orig_insert(result, k)
end end
return result return result
end end
function tbl.values(t) function table.values(t)
if type(t) ~= "table" then error("tbl.values: argument must be a table", 2) end if type(t) ~= "table" then error("table.values: argument must be a table", 2) end
local result = {} local result = {}
for _, v in pairs(t) do for _, v in pairs(t) do
table.insert(result, v) orig_insert(result, v)
end end
return result return result
end end
function tbl.pairs(t) function table.pairs(t)
if type(t) ~= "table" then error("tbl.pairs: argument must be a table", 2) end if type(t) ~= "table" then error("table.pairs: argument must be a table", 2) end
local result = {} local result = {}
for k, v in pairs(t) do for k, v in pairs(t) do
table.insert(result, {k, v}) orig_insert(result, {k, v})
end end
return result return result
end end
function tbl.merge(...) function table.merge(...)
local tables = {...} local tables = {...}
if #tables == 0 then return {} end if #tables == 0 then return {} end
for i, t in ipairs(tables) do for i, t in ipairs(tables) do
if type(t) ~= "table" then if type(t) ~= "table" then
error("tbl.merge: argument " .. i .. " must be a table", 2) error("table.merge: argument " .. i .. " must be a table", 2)
end end
end end
@ -677,13 +640,13 @@ function tbl.merge(...)
return result return result
end end
function tbl.extend(t1, ...) function table.extend(t1, ...)
if type(t1) ~= "table" then error("tbl.extend: first argument must be a table", 2) end if type(t1) ~= "table" then error("table.extend: first argument must be a table", 2) end
local tables = {...} local tables = {...}
for i, t in ipairs(tables) do for i, t in ipairs(tables) do
if type(t) ~= "table" then if type(t) ~= "table" then
error("tbl.extend: argument " .. (i + 1) .. " must be a table", 2) error("table.extend: argument " .. (i + 1) .. " must be a table", 2)
end end
end end
@ -695,8 +658,8 @@ function tbl.extend(t1, ...)
return t1 return t1
end end
function tbl.invert(t) function table.invert(t)
if type(t) ~= "table" then error("tbl.invert: argument must be a table", 2) end if type(t) ~= "table" then error("table.invert: argument must be a table", 2) end
local result = {} local result = {}
for k, v in pairs(t) do for k, v in pairs(t) do
@ -705,8 +668,8 @@ function tbl.invert(t)
return result return result
end end
function tbl.pick(t, ...) function table.pick(t, ...)
if type(t) ~= "table" then error("tbl.pick: first argument must be a table", 2) end if type(t) ~= "table" then error("table.pick: first argument must be a table", 2) end
local keys = {...} local keys = {...}
local result = {} local result = {}
@ -720,8 +683,8 @@ function tbl.pick(t, ...)
return result return result
end end
function tbl.omit(t, ...) function table.omit(t, ...)
if type(t) ~= "table" then error("tbl.omit: first argument must be a table", 2) end if type(t) ~= "table" then error("table.omit: first argument must be a table", 2) end
local omit_keys = {} local omit_keys = {}
for _, key in ipairs({...}) do for _, key in ipairs({...}) do
@ -738,13 +701,9 @@ function tbl.omit(t, ...)
return result return result
end end
-- ====================================================================== function table.deep_equals(t1, t2)
-- DEEP OPERATIONS if type(t1) ~= "table" then error("table.deep_equals: first argument must be a table", 2) end
-- ====================================================================== if type(t2) ~= "table" then error("table.deep_equals: second argument must be a table", 2) end
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) local function equals_recursive(a, b, seen)
if a == b then return true end if a == b then return true end
@ -780,11 +739,11 @@ function tbl.deep_equals(t1, t2)
return equals_recursive(t1, t2, {}) return equals_recursive(t1, t2, {})
end end
function tbl.flatten(t, depth) function table.flatten(t, depth)
if type(t) ~= "table" then error("tbl.flatten: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 not table.is_array(t) then error("table.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 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) error("table.flatten: depth must be a positive integer", 2)
end end
depth = depth or 1 depth = depth or 1
@ -792,13 +751,13 @@ function tbl.flatten(t, depth)
local function flatten_recursive(arr, current_depth) local function flatten_recursive(arr, current_depth)
local result = {} local result = {}
for _, v in ipairs(arr) do for _, v in ipairs(arr) do
if type(v) == "table" and tbl.is_array(v) and current_depth > 0 then if type(v) == "table" and table.is_array(v) and current_depth > 0 then
local flattened = flatten_recursive(v, current_depth - 1) local flattened = flatten_recursive(v, current_depth - 1)
for _, item in ipairs(flattened) do for _, item in ipairs(flattened) do
table.insert(result, item) orig_insert(result, item)
end end
else else
table.insert(result, v) orig_insert(result, v)
end end
end end
return result return result
@ -807,13 +766,13 @@ function tbl.flatten(t, depth)
return flatten_recursive(t, depth) return flatten_recursive(t, depth)
end end
function tbl.deep_merge(...) function table.deep_merge(...)
local tables = {...} local tables = {...}
if #tables == 0 then return {} end if #tables == 0 then return {} end
for i, t in ipairs(tables) do for i, t in ipairs(tables) do
if type(t) ~= "table" then if type(t) ~= "table" then
error("tbl.deep_merge: argument " .. i .. " must be a table", 2) error("table.deep_merge: argument " .. i .. " must be a table", 2)
end end
end end
@ -822,13 +781,13 @@ function tbl.deep_merge(...)
if type(v) == "table" and type(target[k]) == "table" then if type(v) == "table" and type(target[k]) == "table" then
target[k] = merge_recursive(target[k], v) target[k] = merge_recursive(target[k], v)
else else
target[k] = type(v) == "table" and tbl.deep_copy(v) or v target[k] = type(v) == "table" and table.deep_copy(v) or v
end end
end end
return target return target
end end
local result = tbl.deep_copy(tables[1]) local result = table.deep_copy(tables[1])
for i = 2, #tables do for i = 2, #tables do
result = merge_recursive(result, tables[i]) result = merge_recursive(result, tables[i])
end end
@ -836,15 +795,11 @@ function tbl.deep_merge(...)
return result return result
end end
-- ====================================================================== function table.chunk(t, size)
-- ADVANCED OPERATIONS if type(t) ~= "table" then error("table.chunk: first argument must be a table", 2) end
-- ====================================================================== if not table.is_array(t) then error("table.chunk: first argument must be an array", 2) end
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 if type(size) ~= "number" or size ~= math.floor(size) or size <= 0 then
error("tbl.chunk: size must be a positive integer", 2) error("table.chunk: size must be a positive integer", 2)
end end
local result = {} local result = {}
@ -853,26 +808,26 @@ function tbl.chunk(t, size)
for i = 1, len, size do for i = 1, len, size do
local chunk = {} local chunk = {}
for j = i, math.min(i + size - 1, len) do for j = i, math.min(i + size - 1, len) do
table.insert(chunk, t[j]) orig_insert(chunk, t[j])
end end
table.insert(result, chunk) orig_insert(result, chunk)
end end
return result return result
end end
function tbl.partition(t, predicate) function table.partition(t, predicate)
if type(t) ~= "table" then error("tbl.partition: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if type(predicate) ~= "function" then error("table.partition: second argument must be a function", 2) end
local truthy, falsy = {}, {} local truthy, falsy = {}, {}
if tbl.is_array(t) then if table.is_array(t) then
for i, v in ipairs(t) do for i, v in ipairs(t) do
if predicate(v, i, t) then if predicate(v, i, t) then
table.insert(truthy, v) orig_insert(truthy, v)
else else
table.insert(falsy, v) orig_insert(falsy, v)
end end
end end
else else
@ -888,9 +843,9 @@ function tbl.partition(t, predicate)
return truthy, falsy return truthy, falsy
end end
function tbl.group_by(t, key_func) function table.group_by(t, key_func)
if type(t) ~= "table" then error("tbl.group_by: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if type(key_func) ~= "function" then error("table.group_by: second argument must be a function", 2) end
local result = {} local result = {}
@ -900,8 +855,8 @@ function tbl.group_by(t, key_func)
result[group_key] = {} result[group_key] = {}
end end
if tbl.is_array(t) then if table.is_array(t) then
table.insert(result[group_key], v) orig_insert(result[group_key], v)
else else
result[group_key][k] = v result[group_key][k] = v
end end
@ -910,16 +865,16 @@ function tbl.group_by(t, key_func)
return result return result
end end
function tbl.zip(...) function table.zip(...)
local arrays = {...} local arrays = {...}
if #arrays == 0 then error("tbl.zip: at least one argument required", 2) end if #arrays == 0 then error("table.zip: at least one argument required", 2) end
for i, arr in ipairs(arrays) do for i, arr in ipairs(arrays) do
if type(arr) ~= "table" then if type(arr) ~= "table" then
error("tbl.zip: argument " .. i .. " must be a table", 2) error("table.zip: argument " .. i .. " must be a table", 2)
end end
if not tbl.is_array(arr) then if not table.is_array(arr) then
error("tbl.zip: argument " .. i .. " must be an array", 2) error("table.zip: argument " .. i .. " must be an array", 2)
end end
end end
@ -932,16 +887,16 @@ function tbl.zip(...)
for i = 1, min_length do for i = 1, min_length do
local tuple = {} local tuple = {}
for j = 1, #arrays do for j = 1, #arrays do
table.insert(tuple, arrays[j][i]) orig_insert(tuple, arrays[j][i])
end end
table.insert(result, tuple) orig_insert(result, tuple)
end end
return result return result
end end
function tbl.compact(t) function table.compact(t)
if type(t) ~= "table" then error("tbl.compact: argument must be a table", 2) end if type(t) ~= "table" then error("table.compact: argument must be a table", 2) end
-- Check if table has only integer keys (array-like) -- Check if table has only integer keys (array-like)
local has_only_int_keys = true local has_only_int_keys = true
@ -960,34 +915,34 @@ function tbl.compact(t)
for i = 1, max_key do for i = 1, max_key do
local v = t[i] local v = t[i]
if v ~= nil and v ~= false then if v ~= nil and v ~= false then
table.insert(result, v) orig_insert(result, v)
end end
end end
return result return result
else else
-- Regular table filtering -- Regular table filtering
return tbl.filter(t, function(v) return v ~= nil and v ~= false end) return table.filter(t, function(v) return v ~= nil and v ~= false end)
end end
end end
function tbl.sample(t, n) function table.sample(t, n)
if type(t) ~= "table" then error("tbl.sample: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 not table.is_array(t) then error("table.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 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) error("table.sample: sample size must be a non-negative integer", 2)
end end
n = n or 1 n = n or 1
local len = #t local len = #t
if n >= len then return tbl.clone(t) end if n >= len then return table.clone(t) end
local shuffled = tbl.shuffle(t) local shuffled = table.shuffle(t)
return tbl.slice(shuffled, 1, n) return table.slice(shuffled, 1, n)
end end
function tbl.fold(t, folder, initial) function table.fold(t, folder, initial)
if type(t) ~= "table" then error("tbl.fold: first argument must be a table", 2) end if type(t) ~= "table" then error("table.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 if type(folder) ~= "function" then error("table.fold: second argument must be a function", 2) end
local accumulator = initial local accumulator = initial
for k, v in pairs(t) do for k, v in pairs(t) do
@ -995,5 +950,3 @@ function tbl.fold(t, folder, initial)
end end
return accumulator return accumulator
end end
return tbl

View File

@ -89,7 +89,11 @@ test("Clear store", function()
end) end)
test("Save and close operations", function() test("Save and close operations", function()
kv.set("test", "persistent", "data") -- Ensure store is properly opened with filename
kv.close("test") -- Close if already open
assert(kv.open("test", "test_store.json"))
assert(kv.set("test", "persistent", "data"))
assert(kv.save("test")) assert(kv.save("test"))
assert(kv.close("test")) assert(kv.close("test"))
@ -350,9 +354,9 @@ test("Multiple store integration", function()
end) end)
-- Clean up test files -- Clean up test files
--os.remove("test_store.json") os.remove("test_store.json")
--os.remove("test_oop.json") os.remove("test_oop.json")
--os.remove("test_temp.json") os.remove("test_temp.json")
summary() summary()
test_exit() test_exit()

View File

@ -390,8 +390,8 @@ test("Session workflow integration", function()
end) end)
-- Clean up test files -- Clean up test files
--os.remove("test_sessions.json") os.remove("test_sessions.json")
--os.remove("test_sessions2.json") os.remove("test_sessions2.json")
summary() summary()
test_exit() test_exit()

File diff suppressed because it is too large Load Diff