rewrite of the string module
This commit is contained in:
parent
8a53fea511
commit
41cba2f049
@ -1,6 +1,6 @@
|
||||
local http = {}
|
||||
local str = require("string")
|
||||
local json = require("json")
|
||||
local string = require("string")
|
||||
local http = {}
|
||||
|
||||
-- Global routing tables
|
||||
_G._http_routes = _G._http_routes or {}
|
||||
@ -19,29 +19,29 @@ Response.__index = Response
|
||||
|
||||
local function parse_cookies(cookie_header)
|
||||
local cookies = {}
|
||||
if string.is_empty(cookie_header) then
|
||||
if str.is_empty(cookie_header) then
|
||||
return cookies
|
||||
end
|
||||
|
||||
-- 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
|
||||
local trimmed = string.trim(cookie_pair)
|
||||
if not string.is_empty(trimmed) then
|
||||
local parts = string.split(trimmed, "=")
|
||||
local trimmed = str.trim(cookie_pair)
|
||||
if not str.is_empty(trimmed) then
|
||||
local parts = str.split(trimmed, "=")
|
||||
if #parts >= 2 then
|
||||
local name = string.trim(parts[1])
|
||||
local value = string.trim(parts[2])
|
||||
local name = str.trim(parts[1])
|
||||
local value = str.trim(parts[2])
|
||||
|
||||
-- URL decode the value
|
||||
local success, decoded = pcall(function()
|
||||
return string.url_decode(value)
|
||||
return str.url_decode(value)
|
||||
end)
|
||||
|
||||
cookies[name] = success and decoded or value
|
||||
elseif #parts == 1 then
|
||||
-- Cookie without value
|
||||
cookies[string.trim(parts[1])] = ""
|
||||
cookies[str.trim(parts[1])] = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -54,17 +54,17 @@ end
|
||||
-- ======================================================================
|
||||
|
||||
local function split_path(path)
|
||||
if string.is_empty(path) or path == "/" then
|
||||
if str.is_empty(path) or path == "/" then
|
||||
return {}
|
||||
end
|
||||
|
||||
-- Remove leading/trailing slashes and split
|
||||
local clean_path = string.trim(path, "/")
|
||||
if string.is_empty(clean_path) then
|
||||
local clean_path = str.trim(path, "/")
|
||||
if str.is_empty(clean_path) then
|
||||
return {}
|
||||
end
|
||||
|
||||
return string.split(clean_path, "/")
|
||||
return str.split(clean_path, "/")
|
||||
end
|
||||
|
||||
local function match_route(method, path)
|
||||
@ -86,12 +86,12 @@ local function match_route(method, path)
|
||||
for j = i, #path_segments do
|
||||
table.insert(remaining, path_segments[j])
|
||||
end
|
||||
params["*"] = string.join(remaining, "/")
|
||||
params["*"] = str.join(remaining, "/")
|
||||
break
|
||||
elseif string.starts_with(route_seg, ":") then
|
||||
elseif str.starts_with(route_seg, ":") then
|
||||
-- Parameter segment
|
||||
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]
|
||||
else
|
||||
match = false
|
||||
@ -135,7 +135,7 @@ function _http_handle_request(req_table, res_table)
|
||||
end
|
||||
|
||||
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()
|
||||
run_middleware(index + 1)
|
||||
end)
|
||||
@ -203,7 +203,7 @@ end
|
||||
|
||||
function Server:_add_route(method, path, handler)
|
||||
-- Ensure path starts with /
|
||||
if not string.starts_with(path, "/") then
|
||||
if not str.starts_with(path, "/") then
|
||||
path = "/" .. path
|
||||
end
|
||||
|
||||
@ -321,7 +321,7 @@ function Request.new(req_table)
|
||||
end
|
||||
|
||||
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]
|
||||
end
|
||||
|
||||
@ -359,7 +359,7 @@ function Request:cookie_matches(name, pattern)
|
||||
if not cookie_value then
|
||||
return false
|
||||
end
|
||||
return string.match(pattern, cookie_value)
|
||||
return str.match(pattern, cookie_value)
|
||||
end
|
||||
|
||||
function Request:get_cookies_by_names(names)
|
||||
@ -381,7 +381,7 @@ function Request:has_auth_cookies()
|
||||
end
|
||||
|
||||
function Request:json()
|
||||
if string.is_empty(self.body) then
|
||||
if str.is_empty(self.body) then
|
||||
return nil
|
||||
end
|
||||
|
||||
@ -398,27 +398,27 @@ end
|
||||
|
||||
function Request:is_json()
|
||||
local content_type = self:get("content-type") or ""
|
||||
return string.contains(content_type, "application/json")
|
||||
return str.contains(content_type, "application/json")
|
||||
end
|
||||
|
||||
function Request:is_form()
|
||||
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
|
||||
|
||||
function Request:is_multipart()
|
||||
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
|
||||
|
||||
function Request:is_xml()
|
||||
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
|
||||
|
||||
function Request:accepts(mime_type)
|
||||
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
|
||||
|
||||
function Request:user_agent()
|
||||
@ -431,7 +431,7 @@ end
|
||||
|
||||
function Request:is_secure()
|
||||
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
|
||||
|
||||
-- ======================================================================
|
||||
@ -565,8 +565,8 @@ function Response:cookie(name, value, options)
|
||||
local cookie_value = tostring(value)
|
||||
|
||||
-- URL encode the cookie value if it contains special characters
|
||||
if string.match("[;,\\s]", cookie_value) then
|
||||
cookie_value = string.url_encode(cookie_value)
|
||||
if str.match("[;,\\s]", cookie_value) then
|
||||
cookie_value = str.url_encode(cookie_value)
|
||||
end
|
||||
|
||||
local cookie = name .. "=" .. cookie_value
|
||||
@ -638,13 +638,13 @@ function Response:download(data, filename, content_type)
|
||||
self:type(content_type)
|
||||
elseif filename then
|
||||
-- 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")
|
||||
elseif string.ends_with(filename, ".zip") then
|
||||
elseif str.ends_with(filename, ".zip") then
|
||||
self:type("application/zip")
|
||||
elseif string.ends_with(filename, ".json") then
|
||||
elseif str.ends_with(filename, ".json") then
|
||||
self:type("application/json")
|
||||
elseif string.ends_with(filename, ".csv") then
|
||||
elseif str.ends_with(filename, ".csv") then
|
||||
self:type("text/csv")
|
||||
else
|
||||
self:type("application/octet-stream")
|
||||
@ -675,7 +675,7 @@ function http.cors(options)
|
||||
res:header("Access-Control-Allow-Credentials", "true")
|
||||
end
|
||||
|
||||
if string.iequals(req.method, "OPTIONS") then
|
||||
if str.iequals(req.method, "OPTIONS") then
|
||||
res:status(204):send("")
|
||||
else
|
||||
next()
|
||||
@ -687,7 +687,7 @@ function http.static(root_path, url_prefix)
|
||||
url_prefix = url_prefix or "/"
|
||||
|
||||
-- Ensure prefix starts with /
|
||||
if not string.starts_with(url_prefix, "/") then
|
||||
if not str.starts_with(url_prefix, "/") then
|
||||
url_prefix = "/" .. url_prefix
|
||||
end
|
||||
|
||||
@ -706,7 +706,7 @@ end
|
||||
|
||||
function http.json_parser()
|
||||
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()
|
||||
return req:json()
|
||||
end)
|
||||
@ -733,11 +733,11 @@ function http.logger(format)
|
||||
local duration = (os.clock() - start_time) * 1000
|
||||
local status = res._table.status or 200
|
||||
|
||||
local log_message = string.template(format, {
|
||||
local log_message = str.template(format, {
|
||||
method = req.method,
|
||||
path = req.path,
|
||||
status = status,
|
||||
["response-time"] = string.format("%.2f", duration),
|
||||
["response-time"] = str.format("%.2f", duration),
|
||||
["user-agent"] = req:user_agent(),
|
||||
ip = req:ip()
|
||||
})
|
||||
@ -749,9 +749,9 @@ end
|
||||
function http.compression()
|
||||
return function(req, res, next)
|
||||
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")
|
||||
elseif string.contains(accept_encoding, "deflate") then
|
||||
elseif str.contains(accept_encoding, "deflate") then
|
||||
res:header("Content-Encoding", "deflate")
|
||||
end
|
||||
next()
|
||||
|
@ -58,7 +58,10 @@ func kv_open(s *luajit.State) int {
|
||||
mutex.Lock()
|
||||
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)
|
||||
return 1
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"Moonshark/modules/crypto"
|
||||
"Moonshark/modules/fs"
|
||||
@ -11,31 +12,29 @@ import (
|
||||
"Moonshark/modules/kv"
|
||||
"Moonshark/modules/math"
|
||||
"Moonshark/modules/sql"
|
||||
lua_string "Moonshark/modules/string"
|
||||
lua_string "Moonshark/modules/string+"
|
||||
|
||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
||||
)
|
||||
|
||||
// Global registry instance
|
||||
var Global *Registry
|
||||
|
||||
//go:embed **/*.lua
|
||||
var embeddedModules embed.FS
|
||||
|
||||
// Registry manages all Lua modules and Go functions
|
||||
type Registry struct {
|
||||
modules map[string]string
|
||||
globalModules map[string]string // globalName -> moduleSource
|
||||
goFuncs map[string]luajit.GoFunction
|
||||
}
|
||||
|
||||
// New creates a new registry with all modules loaded
|
||||
func New() *Registry {
|
||||
r := &Registry{
|
||||
modules: make(map[string]string),
|
||||
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, math.GetFunctionList())
|
||||
maps.Copy(r.goFuncs, crypto.GetFunctionList())
|
||||
@ -48,9 +47,7 @@ func New() *Registry {
|
||||
return r
|
||||
}
|
||||
|
||||
// loadEmbeddedModules discovers and loads all .lua files
|
||||
func (r *Registry) loadEmbeddedModules() {
|
||||
// Discover all directories from embed
|
||||
dirs, _ := embeddedModules.ReadDir(".")
|
||||
|
||||
for _, dir := range dirs {
|
||||
@ -58,15 +55,27 @@ func (r *Registry) loadEmbeddedModules() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Assume one module file per directory: dirname/dirname.lua
|
||||
modulePath := fmt.Sprintf("%s/%s.lua", dir.Name(), dir.Name())
|
||||
dirName := 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 {
|
||||
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 {
|
||||
// Create moonshark global table with Go functions
|
||||
state.NewTable()
|
||||
@ -78,6 +87,13 @@ func (r *Registry) InstallInState(state *luajit.State) error {
|
||||
}
|
||||
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
|
||||
state.GetGlobal("require")
|
||||
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")
|
||||
}
|
||||
|
||||
// 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 err := s.LoadString(source); err != nil {
|
||||
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 {
|
||||
Global = New()
|
||||
return nil
|
||||
|
666
modules/string+/string.lua
Normal file
666
modules/string+/string.lua
Normal 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
|
@ -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
|
@ -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
|
||||
|
||||
-- ======================================================================
|
||||
-- BUILT-IN TABLE FUNCTIONS (Lua 5.1 wrappers for consistency)
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.insert(t, pos, value)
|
||||
if type(t) ~= "table" then error("tbl.insert: first argument must be a table", 2) end
|
||||
function table.insert(t, pos, value)
|
||||
if type(t) ~= "table" then error("table.insert: first argument must be a table", 2) end
|
||||
|
||||
if value == nil then
|
||||
-- table.insert(t, value) form
|
||||
table.insert(t, pos)
|
||||
orig_insert(t, pos)
|
||||
else
|
||||
-- table.insert(t, pos, value) form
|
||||
if type(pos) ~= "number" or pos ~= math.floor(pos) then
|
||||
error("tbl.insert: position must be an integer", 2)
|
||||
error("table.insert: position must be an integer", 2)
|
||||
end
|
||||
table.insert(t, pos, value)
|
||||
orig_insert(t, pos, value)
|
||||
end
|
||||
end
|
||||
|
||||
function tbl.remove(t, pos)
|
||||
if type(t) ~= "table" then error("tbl.remove: first argument must be a table", 2) end
|
||||
function table.remove(t, pos)
|
||||
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
|
||||
error("tbl.remove: position must be an integer", 2)
|
||||
error("table.remove: position must be an integer", 2)
|
||||
end
|
||||
return table.remove(t, pos)
|
||||
return orig_remove(t, pos)
|
||||
end
|
||||
|
||||
function tbl.concat(t, sep, start_idx, end_idx)
|
||||
if type(t) ~= "table" then error("tbl.concat: first argument must be a table", 2) end
|
||||
if sep ~= nil and type(sep) ~= "string" then error("tbl.concat: separator must be a string", 2) end
|
||||
function table.concat(t, sep, start_idx, end_idx)
|
||||
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("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
|
||||
error("tbl.concat: start index must be an integer", 2)
|
||||
error("table.concat: start index must be an integer", 2)
|
||||
end
|
||||
if end_idx ~= nil and (type(end_idx) ~= "number" or end_idx ~= math.floor(end_idx)) then
|
||||
error("tbl.concat: end index must be an integer", 2)
|
||||
error("table.concat: end index must be an integer", 2)
|
||||
end
|
||||
return table.concat(t, sep, start_idx, end_idx)
|
||||
return orig_concat(t, sep, start_idx, end_idx)
|
||||
end
|
||||
|
||||
function tbl.sort(t, comp)
|
||||
if type(t) ~= "table" then error("tbl.sort: first argument must be a table", 2) end
|
||||
if comp ~= nil and type(comp) ~= "function" then error("tbl.sort: comparator must be a function", 2) end
|
||||
table.sort(t, comp)
|
||||
function table.sort(t, comp)
|
||||
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("table.sort: comparator must be a function", 2) end
|
||||
orig_sort(t, comp)
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- BASIC TABLE OPERATIONS
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.length(t)
|
||||
if type(t) ~= "table" then error("tbl.length: argument must be a table", 2) end
|
||||
function table.length(t)
|
||||
if type(t) ~= "table" then error("table.length: argument must be a table", 2) end
|
||||
return #t
|
||||
end
|
||||
|
||||
function tbl.size(t)
|
||||
if type(t) ~= "table" then error("tbl.size: argument must be a table", 2) end
|
||||
function table.size(t)
|
||||
if type(t) ~= "table" then error("table.size: argument must be a table", 2) end
|
||||
local count = 0
|
||||
for _ in pairs(t) do
|
||||
count = count + 1
|
||||
@ -63,14 +58,14 @@ function tbl.size(t)
|
||||
return count
|
||||
end
|
||||
|
||||
function tbl.is_empty(t)
|
||||
if type(t) ~= "table" then error("tbl.is_empty: argument must be a table", 2) end
|
||||
function table.is_empty(t)
|
||||
if type(t) ~= "table" then error("table.is_empty: argument must be a table", 2) end
|
||||
return next(t) == nil
|
||||
end
|
||||
|
||||
function tbl.is_array(t)
|
||||
if type(t) ~= "table" then error("tbl.is_array: argument must be a table", 2) end
|
||||
if tbl.is_empty(t) then return true end
|
||||
function table.is_array(t)
|
||||
if type(t) ~= "table" then error("table.is_array: argument must be a table", 2) end
|
||||
if table.is_empty(t) then return true end
|
||||
|
||||
local max_index = 0
|
||||
local count = 0
|
||||
@ -84,15 +79,15 @@ function tbl.is_array(t)
|
||||
return max_index == count
|
||||
end
|
||||
|
||||
function tbl.clear(t)
|
||||
if type(t) ~= "table" then error("tbl.clear: argument must be a table", 2) end
|
||||
function table.clear(t)
|
||||
if type(t) ~= "table" then error("table.clear: argument must be a table", 2) end
|
||||
for k in pairs(t) do
|
||||
t[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function tbl.clone(t)
|
||||
if type(t) ~= "table" then error("tbl.clone: argument must be a table", 2) end
|
||||
function table.clone(t)
|
||||
if type(t) ~= "table" then error("table.clone: argument must be a table", 2) end
|
||||
local result = {}
|
||||
for k, v in pairs(t) do
|
||||
result[k] = v
|
||||
@ -100,8 +95,8 @@ function tbl.clone(t)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.deep_copy(t)
|
||||
if type(t) ~= "table" then error("tbl.deep_copy: argument must be a table", 2) end
|
||||
function table.deep_copy(t)
|
||||
if type(t) ~= "table" then error("table.deep_copy: argument must be a table", 2) end
|
||||
|
||||
local function copy_recursive(obj, seen)
|
||||
if type(obj) ~= "table" then return obj end
|
||||
@ -120,29 +115,25 @@ function tbl.deep_copy(t)
|
||||
return copy_recursive(t, {})
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- SEARCHING AND FINDING
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.contains(t, value)
|
||||
if type(t) ~= "table" then error("tbl.contains: first argument must be a table", 2) end
|
||||
function table.contains(t, value)
|
||||
if type(t) ~= "table" then error("table.contains: first argument must be a table", 2) end
|
||||
for _, v in pairs(t) do
|
||||
if v == value then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function tbl.index_of(t, value)
|
||||
if type(t) ~= "table" then error("tbl.index_of: first argument must be a table", 2) end
|
||||
function table.index_of(t, value)
|
||||
if type(t) ~= "table" then error("table.index_of: first argument must be a table", 2) end
|
||||
for k, v in pairs(t) do
|
||||
if v == value then return k end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function tbl.find(t, predicate)
|
||||
if type(t) ~= "table" then error("tbl.find: first argument must be a table", 2) end
|
||||
if type(predicate) ~= "function" then error("tbl.find: second argument must be a function", 2) end
|
||||
function table.find(t, predicate)
|
||||
if type(t) ~= "table" then error("table.find: first argument must be a table", 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
|
||||
if predicate(v, k, t) then return v, k end
|
||||
@ -150,9 +141,9 @@ function tbl.find(t, predicate)
|
||||
return nil
|
||||
end
|
||||
|
||||
function tbl.find_index(t, predicate)
|
||||
if type(t) ~= "table" then error("tbl.find_index: first argument must be a table", 2) end
|
||||
if type(predicate) ~= "function" then error("tbl.find_index: second argument must be a function", 2) end
|
||||
function table.find_index(t, predicate)
|
||||
if type(t) ~= "table" then error("table.find_index: first argument must be a table", 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
|
||||
if predicate(v, k, t) then return k end
|
||||
@ -160,8 +151,8 @@ function tbl.find_index(t, predicate)
|
||||
return nil
|
||||
end
|
||||
|
||||
function tbl.count(t, value_or_predicate)
|
||||
if type(t) ~= "table" then error("tbl.count: first argument must be a table", 2) end
|
||||
function table.count(t, value_or_predicate)
|
||||
if type(t) ~= "table" then error("table.count: first argument must be a table", 2) end
|
||||
|
||||
local count = 0
|
||||
if type(value_or_predicate) == "function" then
|
||||
@ -176,16 +167,12 @@ function tbl.count(t, value_or_predicate)
|
||||
return count
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- FILTERING AND MAPPING
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.filter(t, predicate)
|
||||
if type(t) ~= "table" then error("tbl.filter: first argument must be a table", 2) end
|
||||
if type(predicate) ~= "function" then error("tbl.filter: second argument must be a function", 2) end
|
||||
function table.filter(t, predicate)
|
||||
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
|
||||
|
||||
local result = {}
|
||||
if tbl.is_array(t) then
|
||||
if table.is_array(t) then
|
||||
local max_index = 0
|
||||
for k in pairs(t) do
|
||||
if type(k) == "number" and k > max_index then
|
||||
@ -195,7 +182,7 @@ function tbl.filter(t, predicate)
|
||||
for i = 1, max_index do
|
||||
local v = t[i]
|
||||
if v ~= nil and predicate(v, i, t) then
|
||||
table.insert(result, v)
|
||||
orig_insert(result, v)
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -208,16 +195,16 @@ function tbl.filter(t, predicate)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.reject(t, predicate)
|
||||
if type(t) ~= "table" then error("tbl.reject: first argument must be a table", 2) end
|
||||
if type(predicate) ~= "function" then error("tbl.reject: second argument must be a function", 2) end
|
||||
function table.reject(t, predicate)
|
||||
if type(t) ~= "table" then error("table.reject: first argument must be a table", 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
|
||||
|
||||
function tbl.map(t, transformer)
|
||||
if type(t) ~= "table" then error("tbl.map: first argument must be a table", 2) end
|
||||
if type(transformer) ~= "function" then error("tbl.map: second argument must be a function", 2) end
|
||||
function table.map(t, transformer)
|
||||
if type(t) ~= "table" then error("table.map: first argument must be a table", 2) end
|
||||
if type(transformer) ~= "function" then error("table.map: second argument must be a function", 2) end
|
||||
|
||||
local result = {}
|
||||
for k, v in pairs(t) do
|
||||
@ -226,9 +213,9 @@ function tbl.map(t, transformer)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.map_values(t, transformer)
|
||||
if type(t) ~= "table" then error("tbl.map_values: first argument must be a table", 2) end
|
||||
if type(transformer) ~= "function" then error("tbl.map_values: second argument must be a function", 2) end
|
||||
function table.map_values(t, transformer)
|
||||
if type(t) ~= "table" then error("table.map_values: first argument must be a table", 2) end
|
||||
if type(transformer) ~= "function" then error("table.map_values: second argument must be a function", 2) end
|
||||
|
||||
local result = {}
|
||||
for k, v in pairs(t) do
|
||||
@ -237,9 +224,9 @@ function tbl.map_values(t, transformer)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.map_keys(t, transformer)
|
||||
if type(t) ~= "table" then error("tbl.map_keys: first argument must be a table", 2) end
|
||||
if type(transformer) ~= "function" then error("tbl.map_keys: second argument must be a function", 2) end
|
||||
function table.map_keys(t, transformer)
|
||||
if type(t) ~= "table" then error("table.map_keys: first argument must be a table", 2) end
|
||||
if type(transformer) ~= "function" then error("table.map_keys: second argument must be a function", 2) end
|
||||
|
||||
local result = {}
|
||||
for k, v in pairs(t) do
|
||||
@ -249,13 +236,9 @@ function tbl.map_keys(t, transformer)
|
||||
return result
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- REDUCING AND AGGREGATING
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.reduce(t, reducer, initial)
|
||||
if type(t) ~= "table" then error("tbl.reduce: first argument must be a table", 2) end
|
||||
if type(reducer) ~= "function" then error("tbl.reduce: second argument must be a function", 2) end
|
||||
function table.reduce(t, reducer, initial)
|
||||
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
|
||||
|
||||
local accumulator = initial
|
||||
local started = initial ~= nil
|
||||
@ -270,39 +253,39 @@ function tbl.reduce(t, reducer, initial)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
return accumulator
|
||||
end
|
||||
|
||||
function tbl.sum(t)
|
||||
if type(t) ~= "table" then error("tbl.sum: argument must be a table", 2) end
|
||||
function table.sum(t)
|
||||
if type(t) ~= "table" then error("table.sum: argument must be a table", 2) end
|
||||
local total = 0
|
||||
for _, v in pairs(t) do
|
||||
if type(v) ~= "number" then error("tbl.sum: all values must be numbers", 2) end
|
||||
if type(v) ~= "number" then error("table.sum: all values must be numbers", 2) end
|
||||
total = total + v
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
function tbl.product(t)
|
||||
if type(t) ~= "table" then error("tbl.product: argument must be a table", 2) end
|
||||
function table.product(t)
|
||||
if type(t) ~= "table" then error("table.product: argument must be a table", 2) end
|
||||
local result = 1
|
||||
for _, v in pairs(t) do
|
||||
if type(v) ~= "number" then error("tbl.product: all values must be numbers", 2) end
|
||||
if type(v) ~= "number" then error("table.product: all values must be numbers", 2) end
|
||||
result = result * v
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.min(t)
|
||||
if type(t) ~= "table" then error("tbl.min: argument must be a table", 2) end
|
||||
if tbl.is_empty(t) then error("tbl.min: table is empty", 2) end
|
||||
function table.min(t)
|
||||
if type(t) ~= "table" then error("table.min: argument must be a table", 2) end
|
||||
if table.is_empty(t) then error("table.min: table is empty", 2) end
|
||||
|
||||
local min_val = nil
|
||||
for _, v in pairs(t) do
|
||||
if type(v) ~= "number" then error("tbl.min: all values must be numbers", 2) end
|
||||
if type(v) ~= "number" then error("table.min: all values must be numbers", 2) end
|
||||
if min_val == nil or v < min_val then
|
||||
min_val = v
|
||||
end
|
||||
@ -310,13 +293,13 @@ function tbl.min(t)
|
||||
return min_val
|
||||
end
|
||||
|
||||
function tbl.max(t)
|
||||
if type(t) ~= "table" then error("tbl.max: argument must be a table", 2) end
|
||||
if tbl.is_empty(t) then error("tbl.max: table is empty", 2) end
|
||||
function table.max(t)
|
||||
if type(t) ~= "table" then error("table.max: argument must be a table", 2) end
|
||||
if table.is_empty(t) then error("table.max: table is empty", 2) end
|
||||
|
||||
local max_val = nil
|
||||
for _, v in pairs(t) do
|
||||
if type(v) ~= "number" then error("tbl.max: all values must be numbers", 2) end
|
||||
if type(v) ~= "number" then error("table.max: all values must be numbers", 2) end
|
||||
if max_val == nil or v > max_val then
|
||||
max_val = v
|
||||
end
|
||||
@ -324,21 +307,17 @@ function tbl.max(t)
|
||||
return max_val
|
||||
end
|
||||
|
||||
function tbl.average(t)
|
||||
if type(t) ~= "table" then error("tbl.average: argument must be a table", 2) end
|
||||
if tbl.is_empty(t) then error("tbl.average: table is empty", 2) end
|
||||
return tbl.sum(t) / tbl.size(t)
|
||||
function table.average(t)
|
||||
if type(t) ~= "table" then error("table.average: argument must be a table", 2) end
|
||||
if table.is_empty(t) then error("table.average: table is empty", 2) end
|
||||
return table.sum(t) / table.size(t)
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- BOOLEAN OPERATIONS
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.all(t, predicate)
|
||||
if type(t) ~= "table" then error("tbl.all: first argument must be a table", 2) end
|
||||
function table.all(t, predicate)
|
||||
if type(t) ~= "table" then error("table.all: first argument must be a table", 2) end
|
||||
|
||||
if predicate then
|
||||
if type(predicate) ~= "function" then error("tbl.all: second argument must be a function", 2) end
|
||||
if type(predicate) ~= "function" then error("table.all: second argument must be a function", 2) end
|
||||
for k, v in pairs(t) do
|
||||
if not predicate(v, k, t) then return false end
|
||||
end
|
||||
@ -350,11 +329,11 @@ function tbl.all(t, predicate)
|
||||
return true
|
||||
end
|
||||
|
||||
function tbl.any(t, predicate)
|
||||
if type(t) ~= "table" then error("tbl.any: first argument must be a table", 2) end
|
||||
function table.any(t, predicate)
|
||||
if type(t) ~= "table" then error("table.any: first argument must be a table", 2) end
|
||||
|
||||
if predicate then
|
||||
if type(predicate) ~= "function" then error("tbl.any: second argument must be a function", 2) end
|
||||
if type(predicate) ~= "function" then error("table.any: second argument must be a function", 2) end
|
||||
for k, v in pairs(t) do
|
||||
if predicate(v, k, t) then return true end
|
||||
end
|
||||
@ -366,26 +345,22 @@ function tbl.any(t, predicate)
|
||||
return false
|
||||
end
|
||||
|
||||
function tbl.none(t, predicate)
|
||||
if type(t) ~= "table" then error("tbl.none: first argument must be a table", 2) end
|
||||
return not tbl.any(t, predicate)
|
||||
function table.none(t, predicate)
|
||||
if type(t) ~= "table" then error("table.none: first argument must be a table", 2) end
|
||||
return not table.any(t, predicate)
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- SET OPERATIONS
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.unique(t)
|
||||
if type(t) ~= "table" then error("tbl.unique: argument must be a table", 2) end
|
||||
function table.unique(t)
|
||||
if type(t) ~= "table" then error("table.unique: argument must be a table", 2) end
|
||||
|
||||
local seen = {}
|
||||
local result = {}
|
||||
|
||||
if tbl.is_array(t) then
|
||||
if table.is_array(t) then
|
||||
for _, v in ipairs(t) do
|
||||
if not seen[v] then
|
||||
seen[v] = true
|
||||
table.insert(result, v)
|
||||
orig_insert(result, v)
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -400,9 +375,9 @@ function tbl.unique(t)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.intersection(t1, t2)
|
||||
if type(t1) ~= "table" then error("tbl.intersection: first argument must be a table", 2) end
|
||||
if type(t2) ~= "table" then error("tbl.intersection: second argument must be a table", 2) end
|
||||
function table.intersection(t1, t2)
|
||||
if type(t1) ~= "table" then error("table.intersection: first 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 = {}
|
||||
for _, v in pairs(t2) do
|
||||
@ -410,10 +385,10 @@ function tbl.intersection(t1, t2)
|
||||
end
|
||||
|
||||
local result = {}
|
||||
if tbl.is_array(t1) then
|
||||
if table.is_array(t1) then
|
||||
for _, v in ipairs(t1) do
|
||||
if set2[v] then
|
||||
table.insert(result, v)
|
||||
orig_insert(result, v)
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -427,16 +402,16 @@ function tbl.intersection(t1, t2)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.union(t1, t2)
|
||||
if type(t1) ~= "table" then error("tbl.union: first argument must be a table", 2) end
|
||||
if type(t2) ~= "table" then error("tbl.union: second argument must be a table", 2) end
|
||||
function table.union(t1, t2)
|
||||
if type(t1) ~= "table" then error("table.union: first 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
|
||||
if not tbl.contains(result, v) then
|
||||
table.insert(result, v)
|
||||
if not table.contains(result, v) then
|
||||
orig_insert(result, v)
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -450,25 +425,21 @@ function tbl.union(t1, t2)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.difference(t1, t2)
|
||||
if type(t1) ~= "table" then error("tbl.difference: first argument must be a table", 2) end
|
||||
if type(t2) ~= "table" then error("tbl.difference: second argument must be a table", 2) end
|
||||
function table.difference(t1, t2)
|
||||
if type(t1) ~= "table" then error("table.difference: first 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 = {}
|
||||
for _, v in pairs(t2) do
|
||||
set2[v] = true
|
||||
end
|
||||
|
||||
return tbl.filter(t1, function(v) return not set2[v] end)
|
||||
return table.filter(t1, function(v) return not set2[v] end)
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- ARRAY OPERATIONS
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.reverse(t)
|
||||
if type(t) ~= "table" then error("tbl.reverse: argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.reverse: argument must be an array", 2) end
|
||||
function table.reverse(t)
|
||||
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
|
||||
|
||||
local result = {}
|
||||
local len = #t
|
||||
@ -478,11 +449,11 @@ function tbl.reverse(t)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.shuffle(t)
|
||||
if type(t) ~= "table" then error("tbl.shuffle: argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.shuffle: argument must be an array", 2) end
|
||||
function table.shuffle(t)
|
||||
if type(t) ~= "table" then error("table.shuffle: argument must be a table", 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
|
||||
|
||||
math.randomseed(os.time() + os.clock() * 1000000)
|
||||
@ -495,18 +466,18 @@ function tbl.shuffle(t)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.rotate(t, positions)
|
||||
if type(t) ~= "table" then error("tbl.rotate: first argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.rotate: first argument must be an array", 2) end
|
||||
function table.rotate(t, positions)
|
||||
if type(t) ~= "table" then error("table.rotate: first argument must be a table", 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
|
||||
error("tbl.rotate: second argument must be an integer", 2)
|
||||
error("table.rotate: second argument must be an integer", 2)
|
||||
end
|
||||
|
||||
local len = #t
|
||||
if len == 0 then return {} end
|
||||
|
||||
positions = positions % len
|
||||
if positions == 0 then return tbl.clone(t) end
|
||||
if positions == 0 then return table.clone(t) end
|
||||
|
||||
local result = {}
|
||||
for i = 1, len do
|
||||
@ -517,14 +488,14 @@ function tbl.rotate(t, positions)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.slice(t, start_idx, end_idx)
|
||||
if type(t) ~= "table" then error("tbl.slice: first argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.slice: first argument must be an array", 2) end
|
||||
function table.slice(t, start_idx, end_idx)
|
||||
if type(t) ~= "table" then error("table.slice: first argument must be a table", 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
|
||||
error("tbl.slice: start index must be an integer", 2)
|
||||
error("table.slice: start index must be an integer", 2)
|
||||
end
|
||||
if end_idx ~= nil and (type(end_idx) ~= "number" or end_idx ~= math.floor(end_idx)) then
|
||||
error("tbl.slice: end index must be an integer", 2)
|
||||
error("table.slice: end index must be an integer", 2)
|
||||
end
|
||||
|
||||
local len = #t
|
||||
@ -536,20 +507,20 @@ function tbl.slice(t, start_idx, end_idx)
|
||||
|
||||
local result = {}
|
||||
for i = start_idx, end_idx do
|
||||
table.insert(result, t[i])
|
||||
orig_insert(result, t[i])
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.splice(t, start_idx, delete_count, ...)
|
||||
if type(t) ~= "table" then error("tbl.splice: first argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.splice: first argument must be an array", 2) end
|
||||
function table.splice(t, start_idx, delete_count, ...)
|
||||
if type(t) ~= "table" then error("table.splice: first argument must be a table", 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
|
||||
error("tbl.splice: start index must be an integer", 2)
|
||||
error("table.splice: start index must be an integer", 2)
|
||||
end
|
||||
if delete_count ~= nil and (type(delete_count) ~= "number" or delete_count ~= math.floor(delete_count) or delete_count < 0) then
|
||||
error("tbl.splice: delete count must be a non-negative integer", 2)
|
||||
error("table.splice: delete count must be a non-negative integer", 2)
|
||||
end
|
||||
|
||||
local len = #t
|
||||
@ -593,26 +564,22 @@ function tbl.splice(t, start_idx, delete_count, ...)
|
||||
return deleted
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- SORTING HELPERS
|
||||
-- ======================================================================
|
||||
function table.sort_by(t, key_func)
|
||||
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)
|
||||
if type(t) ~= "table" then error("tbl.sort_by: first argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.sort_by: first argument must be an array", 2) end
|
||||
if type(key_func) ~= "function" then error("tbl.sort_by: second argument must be a function", 2) end
|
||||
|
||||
local result = tbl.clone(t)
|
||||
table.sort(result, function(a, b)
|
||||
local result = table.clone(t)
|
||||
orig_sort(result, function(a, b)
|
||||
return key_func(a) < key_func(b)
|
||||
end)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.is_sorted(t, comp)
|
||||
if type(t) ~= "table" then error("tbl.is_sorted: first argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.is_sorted: first argument must be an array", 2) end
|
||||
if comp ~= nil and type(comp) ~= "function" then error("tbl.is_sorted: comparator must be a function", 2) end
|
||||
function table.is_sorted(t, comp)
|
||||
if type(t) ~= "table" then error("table.is_sorted: first argument must be a table", 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("table.is_sorted: comparator must be a function", 2) end
|
||||
|
||||
comp = comp or function(a, b) return a < b end
|
||||
|
||||
@ -624,47 +591,43 @@ function tbl.is_sorted(t, comp)
|
||||
return true
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- UTILITY FUNCTIONS
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.keys(t)
|
||||
if type(t) ~= "table" then error("tbl.keys: argument must be a table", 2) end
|
||||
function table.keys(t)
|
||||
if type(t) ~= "table" then error("table.keys: argument must be a table", 2) end
|
||||
|
||||
local result = {}
|
||||
for k, _ in pairs(t) do
|
||||
table.insert(result, k)
|
||||
orig_insert(result, k)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.values(t)
|
||||
if type(t) ~= "table" then error("tbl.values: argument must be a table", 2) end
|
||||
function table.values(t)
|
||||
if type(t) ~= "table" then error("table.values: argument must be a table", 2) end
|
||||
|
||||
local result = {}
|
||||
for _, v in pairs(t) do
|
||||
table.insert(result, v)
|
||||
orig_insert(result, v)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.pairs(t)
|
||||
if type(t) ~= "table" then error("tbl.pairs: argument must be a table", 2) end
|
||||
function table.pairs(t)
|
||||
if type(t) ~= "table" then error("table.pairs: argument must be a table", 2) end
|
||||
|
||||
local result = {}
|
||||
for k, v in pairs(t) do
|
||||
table.insert(result, {k, v})
|
||||
orig_insert(result, {k, v})
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.merge(...)
|
||||
function table.merge(...)
|
||||
local tables = {...}
|
||||
if #tables == 0 then return {} end
|
||||
|
||||
for i, t in ipairs(tables) do
|
||||
if type(t) ~= "table" then
|
||||
error("tbl.merge: argument " .. i .. " must be a table", 2)
|
||||
error("table.merge: argument " .. i .. " must be a table", 2)
|
||||
end
|
||||
end
|
||||
|
||||
@ -677,13 +640,13 @@ function tbl.merge(...)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.extend(t1, ...)
|
||||
if type(t1) ~= "table" then error("tbl.extend: first argument must be a table", 2) end
|
||||
function table.extend(t1, ...)
|
||||
if type(t1) ~= "table" then error("table.extend: first argument must be a table", 2) end
|
||||
|
||||
local tables = {...}
|
||||
for i, t in ipairs(tables) do
|
||||
if type(t) ~= "table" then
|
||||
error("tbl.extend: argument " .. (i + 1) .. " must be a table", 2)
|
||||
error("table.extend: argument " .. (i + 1) .. " must be a table", 2)
|
||||
end
|
||||
end
|
||||
|
||||
@ -695,8 +658,8 @@ function tbl.extend(t1, ...)
|
||||
return t1
|
||||
end
|
||||
|
||||
function tbl.invert(t)
|
||||
if type(t) ~= "table" then error("tbl.invert: argument must be a table", 2) end
|
||||
function table.invert(t)
|
||||
if type(t) ~= "table" then error("table.invert: argument must be a table", 2) end
|
||||
|
||||
local result = {}
|
||||
for k, v in pairs(t) do
|
||||
@ -705,8 +668,8 @@ function tbl.invert(t)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.pick(t, ...)
|
||||
if type(t) ~= "table" then error("tbl.pick: first argument must be a table", 2) end
|
||||
function table.pick(t, ...)
|
||||
if type(t) ~= "table" then error("table.pick: first argument must be a table", 2) end
|
||||
|
||||
local keys = {...}
|
||||
local result = {}
|
||||
@ -720,8 +683,8 @@ function tbl.pick(t, ...)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.omit(t, ...)
|
||||
if type(t) ~= "table" then error("tbl.omit: first argument must be a table", 2) end
|
||||
function table.omit(t, ...)
|
||||
if type(t) ~= "table" then error("table.omit: first argument must be a table", 2) end
|
||||
|
||||
local omit_keys = {}
|
||||
for _, key in ipairs({...}) do
|
||||
@ -738,13 +701,9 @@ function tbl.omit(t, ...)
|
||||
return result
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- DEEP OPERATIONS
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.deep_equals(t1, t2)
|
||||
if type(t1) ~= "table" then error("tbl.deep_equals: first argument must be a table", 2) end
|
||||
if type(t2) ~= "table" then error("tbl.deep_equals: second argument must be a table", 2) end
|
||||
function table.deep_equals(t1, t2)
|
||||
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
|
||||
|
||||
local function equals_recursive(a, b, seen)
|
||||
if a == b then return true end
|
||||
@ -780,11 +739,11 @@ function tbl.deep_equals(t1, t2)
|
||||
return equals_recursive(t1, t2, {})
|
||||
end
|
||||
|
||||
function tbl.flatten(t, depth)
|
||||
if type(t) ~= "table" then error("tbl.flatten: first argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.flatten: first argument must be an array", 2) end
|
||||
function table.flatten(t, depth)
|
||||
if type(t) ~= "table" then error("table.flatten: first argument must be a table", 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
|
||||
error("tbl.flatten: depth must be a positive integer", 2)
|
||||
error("table.flatten: depth must be a positive integer", 2)
|
||||
end
|
||||
|
||||
depth = depth or 1
|
||||
@ -792,13 +751,13 @@ function tbl.flatten(t, depth)
|
||||
local function flatten_recursive(arr, current_depth)
|
||||
local result = {}
|
||||
for _, v in ipairs(arr) do
|
||||
if type(v) == "table" and tbl.is_array(v) and current_depth > 0 then
|
||||
if type(v) == "table" and table.is_array(v) and current_depth > 0 then
|
||||
local flattened = flatten_recursive(v, current_depth - 1)
|
||||
for _, item in ipairs(flattened) do
|
||||
table.insert(result, item)
|
||||
orig_insert(result, item)
|
||||
end
|
||||
else
|
||||
table.insert(result, v)
|
||||
orig_insert(result, v)
|
||||
end
|
||||
end
|
||||
return result
|
||||
@ -807,13 +766,13 @@ function tbl.flatten(t, depth)
|
||||
return flatten_recursive(t, depth)
|
||||
end
|
||||
|
||||
function tbl.deep_merge(...)
|
||||
function table.deep_merge(...)
|
||||
local tables = {...}
|
||||
if #tables == 0 then return {} end
|
||||
|
||||
for i, t in ipairs(tables) do
|
||||
if type(t) ~= "table" then
|
||||
error("tbl.deep_merge: argument " .. i .. " must be a table", 2)
|
||||
error("table.deep_merge: argument " .. i .. " must be a table", 2)
|
||||
end
|
||||
end
|
||||
|
||||
@ -822,13 +781,13 @@ function tbl.deep_merge(...)
|
||||
if type(v) == "table" and type(target[k]) == "table" then
|
||||
target[k] = merge_recursive(target[k], v)
|
||||
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
|
||||
return target
|
||||
end
|
||||
|
||||
local result = tbl.deep_copy(tables[1])
|
||||
local result = table.deep_copy(tables[1])
|
||||
for i = 2, #tables do
|
||||
result = merge_recursive(result, tables[i])
|
||||
end
|
||||
@ -836,15 +795,11 @@ function tbl.deep_merge(...)
|
||||
return result
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- ADVANCED OPERATIONS
|
||||
-- ======================================================================
|
||||
|
||||
function tbl.chunk(t, size)
|
||||
if type(t) ~= "table" then error("tbl.chunk: first argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.chunk: first argument must be an array", 2) end
|
||||
function table.chunk(t, size)
|
||||
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
|
||||
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
|
||||
|
||||
local result = {}
|
||||
@ -853,26 +808,26 @@ function tbl.chunk(t, size)
|
||||
for i = 1, len, size do
|
||||
local chunk = {}
|
||||
for j = i, math.min(i + size - 1, len) do
|
||||
table.insert(chunk, t[j])
|
||||
orig_insert(chunk, t[j])
|
||||
end
|
||||
table.insert(result, chunk)
|
||||
orig_insert(result, chunk)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.partition(t, predicate)
|
||||
if type(t) ~= "table" then error("tbl.partition: first argument must be a table", 2) end
|
||||
if type(predicate) ~= "function" then error("tbl.partition: second argument must be a function", 2) end
|
||||
function table.partition(t, predicate)
|
||||
if type(t) ~= "table" then error("table.partition: first argument must be a table", 2) end
|
||||
if type(predicate) ~= "function" then error("table.partition: second argument must be a function", 2) end
|
||||
|
||||
local truthy, falsy = {}, {}
|
||||
|
||||
if tbl.is_array(t) then
|
||||
if table.is_array(t) then
|
||||
for i, v in ipairs(t) do
|
||||
if predicate(v, i, t) then
|
||||
table.insert(truthy, v)
|
||||
orig_insert(truthy, v)
|
||||
else
|
||||
table.insert(falsy, v)
|
||||
orig_insert(falsy, v)
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -888,9 +843,9 @@ function tbl.partition(t, predicate)
|
||||
return truthy, falsy
|
||||
end
|
||||
|
||||
function tbl.group_by(t, key_func)
|
||||
if type(t) ~= "table" then error("tbl.group_by: first argument must be a table", 2) end
|
||||
if type(key_func) ~= "function" then error("tbl.group_by: second argument must be a function", 2) end
|
||||
function table.group_by(t, key_func)
|
||||
if type(t) ~= "table" then error("table.group_by: first argument must be a table", 2) end
|
||||
if type(key_func) ~= "function" then error("table.group_by: second argument must be a function", 2) end
|
||||
|
||||
local result = {}
|
||||
|
||||
@ -900,8 +855,8 @@ function tbl.group_by(t, key_func)
|
||||
result[group_key] = {}
|
||||
end
|
||||
|
||||
if tbl.is_array(t) then
|
||||
table.insert(result[group_key], v)
|
||||
if table.is_array(t) then
|
||||
orig_insert(result[group_key], v)
|
||||
else
|
||||
result[group_key][k] = v
|
||||
end
|
||||
@ -910,16 +865,16 @@ function tbl.group_by(t, key_func)
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.zip(...)
|
||||
function table.zip(...)
|
||||
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
|
||||
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
|
||||
if not tbl.is_array(arr) then
|
||||
error("tbl.zip: argument " .. i .. " must be an array", 2)
|
||||
if not table.is_array(arr) then
|
||||
error("table.zip: argument " .. i .. " must be an array", 2)
|
||||
end
|
||||
end
|
||||
|
||||
@ -932,16 +887,16 @@ function tbl.zip(...)
|
||||
for i = 1, min_length do
|
||||
local tuple = {}
|
||||
for j = 1, #arrays do
|
||||
table.insert(tuple, arrays[j][i])
|
||||
orig_insert(tuple, arrays[j][i])
|
||||
end
|
||||
table.insert(result, tuple)
|
||||
orig_insert(result, tuple)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
function tbl.compact(t)
|
||||
if type(t) ~= "table" then error("tbl.compact: argument must be a table", 2) end
|
||||
function table.compact(t)
|
||||
if type(t) ~= "table" then error("table.compact: argument must be a table", 2) end
|
||||
|
||||
-- Check if table has only integer keys (array-like)
|
||||
local has_only_int_keys = true
|
||||
@ -960,34 +915,34 @@ function tbl.compact(t)
|
||||
for i = 1, max_key do
|
||||
local v = t[i]
|
||||
if v ~= nil and v ~= false then
|
||||
table.insert(result, v)
|
||||
orig_insert(result, v)
|
||||
end
|
||||
end
|
||||
return result
|
||||
else
|
||||
-- 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
|
||||
|
||||
function tbl.sample(t, n)
|
||||
if type(t) ~= "table" then error("tbl.sample: first argument must be a table", 2) end
|
||||
if not tbl.is_array(t) then error("tbl.sample: first argument must be an array", 2) end
|
||||
function table.sample(t, n)
|
||||
if type(t) ~= "table" then error("table.sample: first argument must be a table", 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
|
||||
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
|
||||
|
||||
n = n or 1
|
||||
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)
|
||||
return tbl.slice(shuffled, 1, n)
|
||||
local shuffled = table.shuffle(t)
|
||||
return table.slice(shuffled, 1, n)
|
||||
end
|
||||
|
||||
function tbl.fold(t, folder, initial)
|
||||
if type(t) ~= "table" then error("tbl.fold: first argument must be a table", 2) end
|
||||
if type(folder) ~= "function" then error("tbl.fold: second argument must be a function", 2) end
|
||||
function table.fold(t, folder, initial)
|
||||
if type(t) ~= "table" then error("table.fold: first argument must be a table", 2) end
|
||||
if type(folder) ~= "function" then error("table.fold: second argument must be a function", 2) end
|
||||
|
||||
local accumulator = initial
|
||||
for k, v in pairs(t) do
|
||||
@ -995,5 +950,3 @@ function tbl.fold(t, folder, initial)
|
||||
end
|
||||
return accumulator
|
||||
end
|
||||
|
||||
return tbl
|
||||
|
12
tests/kv.lua
12
tests/kv.lua
@ -89,7 +89,11 @@ test("Clear store", function()
|
||||
end)
|
||||
|
||||
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.close("test"))
|
||||
|
||||
@ -350,9 +354,9 @@ test("Multiple store integration", function()
|
||||
end)
|
||||
|
||||
-- Clean up test files
|
||||
--os.remove("test_store.json")
|
||||
--os.remove("test_oop.json")
|
||||
--os.remove("test_temp.json")
|
||||
os.remove("test_store.json")
|
||||
os.remove("test_oop.json")
|
||||
os.remove("test_temp.json")
|
||||
|
||||
summary()
|
||||
test_exit()
|
||||
|
@ -390,8 +390,8 @@ test("Session workflow integration", function()
|
||||
end)
|
||||
|
||||
-- Clean up test files
|
||||
--os.remove("test_sessions.json")
|
||||
--os.remove("test_sessions2.json")
|
||||
os.remove("test_sessions.json")
|
||||
os.remove("test_sessions2.json")
|
||||
|
||||
summary()
|
||||
test_exit()
|
||||
|
892
tests/string.lua
892
tests/string.lua
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user