added logging support
This commit is contained in:
+355
@@ -0,0 +1,355 @@
|
||||
--[[
|
||||
fmt.lua - Go's fmt.Printf() style formatting for Lua
|
||||
|
||||
Supported verbs:
|
||||
%v - default format (tostring)
|
||||
%T - type of the value
|
||||
%t - boolean (true/false)
|
||||
%d - integer (decimal)
|
||||
%b - integer (binary)
|
||||
%o - integer (octal)
|
||||
%x - integer (hex, lowercase)
|
||||
%X - integer (hex, uppercase)
|
||||
%e - float (scientific, lowercase)
|
||||
%E - float (scientific, uppercase)
|
||||
%f - float (decimal point)
|
||||
%g - float (%e for large, %f otherwise)
|
||||
%G - float (%E for large, %f otherwise)
|
||||
%s - string
|
||||
%q - quoted string (Lua-escaped)
|
||||
%c - character (from integer codepoint)
|
||||
%% - literal percent sign
|
||||
|
||||
Width & precision:
|
||||
%8d - right-aligned, width 8
|
||||
%-8d - left-aligned, width 8
|
||||
%08d - zero-padded, width 8
|
||||
%8.2f - width 8, 2 decimal places
|
||||
%.5s - truncate string to 5 chars
|
||||
%*d - width from next argument
|
||||
%.*f - precision from next argument
|
||||
--]]
|
||||
|
||||
local fmt = {}
|
||||
|
||||
-- ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
local function to_int(v)
|
||||
local n = math.tointeger and math.tointeger(v) or (type(v) == "number" and math.floor(v) or nil)
|
||||
if n == nil then
|
||||
error(string.format("fmt: expected integer, got %s (%s)", type(v), tostring(v)), 3)
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
local function to_num(v)
|
||||
if type(v) ~= "number" then
|
||||
error(string.format("fmt: expected number, got %s (%s)", type(v), tostring(v)), 3)
|
||||
end
|
||||
return v
|
||||
end
|
||||
|
||||
-- Convert integer to binary string
|
||||
local function to_binary(n)
|
||||
n = to_int(n)
|
||||
if n == 0 then return "0" end
|
||||
local neg = n < 0
|
||||
if neg then n = -n end
|
||||
local bits = {}
|
||||
while n > 0 do
|
||||
table.insert(bits, 1, n % 2)
|
||||
n = math.floor(n / 2)
|
||||
end
|
||||
local s = table.concat(bits)
|
||||
return neg and "-" .. s or s
|
||||
end
|
||||
|
||||
-- Apply width/alignment padding
|
||||
local function apply_width(s, width, left_align, zero_pad, is_numeric)
|
||||
if not width or width == 0 or #s >= width then return s end
|
||||
local pad_char = (zero_pad and is_numeric) and "0" or " "
|
||||
local pad = string.rep(pad_char, width - #s)
|
||||
if zero_pad and is_numeric then
|
||||
-- keep sign before zeros: "-007"
|
||||
if s:sub(1,1) == "-" then
|
||||
return "-" .. pad .. s:sub(2)
|
||||
end
|
||||
return pad .. s
|
||||
end
|
||||
return left_align and (s .. pad) or (pad .. s)
|
||||
end
|
||||
|
||||
-- Apply precision to a string (truncation)
|
||||
local function apply_str_precision(s, prec)
|
||||
if prec and #s > prec then
|
||||
return s:sub(1, prec)
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
-- Quoted / escaped string (like Go %q)
|
||||
local function quote_string(s)
|
||||
s = s:gsub('\\', '\\\\')
|
||||
s = s:gsub('"', '\\"')
|
||||
s = s:gsub('\n', '\\n')
|
||||
s = s:gsub('\r', '\\r')
|
||||
s = s:gsub('\t', '\\t')
|
||||
s = s:gsub('%z', '\\0')
|
||||
return '"' .. s .. '"'
|
||||
end
|
||||
|
||||
-- ── core formatter ────────────────────────────────────────────────────────────
|
||||
|
||||
--[[
|
||||
fmt.Sprintf(format, ...) → string
|
||||
Returns the formatted string without printing it.
|
||||
--]]
|
||||
function fmt.Sprintf(format, ...)
|
||||
local args = { ... }
|
||||
local ai = 1 -- argument index
|
||||
local out = {} -- output buffer
|
||||
|
||||
local i = 1
|
||||
local len = #format
|
||||
|
||||
while i <= len do
|
||||
local c = format:sub(i, i)
|
||||
|
||||
if c ~= "%" then
|
||||
out[#out+1] = c
|
||||
i = i + 1
|
||||
else
|
||||
i = i + 1
|
||||
if i > len then
|
||||
error("fmt: trailing '%' in format string", 2)
|
||||
end
|
||||
|
||||
-- %% literal
|
||||
if format:sub(i, i) == "%" then
|
||||
out[#out+1] = "%"
|
||||
i = i + 1
|
||||
else
|
||||
-- ── parse flags ───────────────────────────────────────────
|
||||
local left_align = false
|
||||
local zero_pad = false
|
||||
local plus_sign = false
|
||||
local space_sign = false
|
||||
local hash_flag = false
|
||||
|
||||
while i <= len do
|
||||
local f = format:sub(i, i)
|
||||
if f == "-" then left_align = true
|
||||
elseif f == "0" then zero_pad = true
|
||||
elseif f == "+" then plus_sign = true
|
||||
elseif f == " " then space_sign = true
|
||||
elseif f == "#" then hash_flag = true
|
||||
else break
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
-- ── parse width ───────────────────────────────────────────
|
||||
local width = nil
|
||||
if i <= len and format:sub(i, i) == "*" then
|
||||
width = to_int(args[ai]); ai = ai + 1
|
||||
i = i + 1
|
||||
else
|
||||
local w = format:match("^%d+", i)
|
||||
if w then width = tonumber(w); i = i + #w end
|
||||
end
|
||||
|
||||
-- ── parse precision ───────────────────────────────────────
|
||||
local prec = nil
|
||||
if i <= len and format:sub(i, i) == "." then
|
||||
i = i + 1
|
||||
if i <= len and format:sub(i, i) == "*" then
|
||||
prec = to_int(args[ai]); ai = ai + 1
|
||||
i = i + 1
|
||||
else
|
||||
local p = format:match("^%d*", i)
|
||||
prec = tonumber(p) or 0
|
||||
i = i + #p
|
||||
end
|
||||
end
|
||||
|
||||
-- ── parse verb ────────────────────────────────────────────
|
||||
if i > len then
|
||||
error("fmt: missing verb in format string", 2)
|
||||
end
|
||||
local verb = format:sub(i, i)
|
||||
i = i + 1
|
||||
|
||||
local arg = args[ai]; ai = ai + 1
|
||||
local s = ""
|
||||
local is_numeric = false
|
||||
|
||||
-- %v default
|
||||
if verb == "v" then
|
||||
s = tostring(arg)
|
||||
|
||||
-- %T type
|
||||
elseif verb == "T" then
|
||||
s = type(arg)
|
||||
|
||||
-- %t boolean
|
||||
elseif verb == "t" then
|
||||
s = arg and "true" or "false"
|
||||
|
||||
-- %d decimal integer
|
||||
elseif verb == "d" then
|
||||
is_numeric = true
|
||||
local n = to_int(arg)
|
||||
s = tostring(math.abs(n))
|
||||
if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end
|
||||
if n < 0 then s = "-" .. s
|
||||
elseif plus_sign then s = "+" .. s
|
||||
elseif space_sign then s = " " .. s
|
||||
end
|
||||
|
||||
-- %b binary
|
||||
elseif verb == "b" then
|
||||
is_numeric = true
|
||||
s = to_binary(arg)
|
||||
if hash_flag then s = "0b" .. s end
|
||||
|
||||
-- %o octal
|
||||
elseif verb == "o" then
|
||||
is_numeric = true
|
||||
local n = to_int(arg)
|
||||
local neg = n < 0
|
||||
s = string.format("%o", math.abs(n))
|
||||
if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end
|
||||
if hash_flag and s:sub(1,1) ~= "0" then s = "0" .. s end
|
||||
if neg then s = "-" .. s end
|
||||
|
||||
-- %x hex lowercase
|
||||
elseif verb == "x" then
|
||||
is_numeric = true
|
||||
local n = to_int(arg)
|
||||
local neg = n < 0
|
||||
s = string.format("%x", math.abs(n))
|
||||
if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end
|
||||
if hash_flag then s = "0x" .. s end
|
||||
if neg then s = "-" .. s end
|
||||
|
||||
-- %X hex uppercase
|
||||
elseif verb == "X" then
|
||||
is_numeric = true
|
||||
local n = to_int(arg)
|
||||
local neg = n < 0
|
||||
s = string.format("%X", math.abs(n))
|
||||
if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end
|
||||
if hash_flag then s = "0X" .. s end
|
||||
if neg then s = "-" .. s end
|
||||
|
||||
-- %e scientific lowercase
|
||||
elseif verb == "e" then
|
||||
is_numeric = true
|
||||
local p = prec ~= nil and prec or 6
|
||||
s = string.format("%." .. p .. "e", to_num(arg))
|
||||
if plus_sign and arg >= 0 then s = "+" .. s end
|
||||
|
||||
-- %E scientific uppercase
|
||||
elseif verb == "E" then
|
||||
is_numeric = true
|
||||
local p = prec ~= nil and prec or 6
|
||||
s = string.format("%." .. p .. "E", to_num(arg))
|
||||
if plus_sign and arg >= 0 then s = "+" .. s end
|
||||
|
||||
-- %f decimal float
|
||||
elseif verb == "f" then
|
||||
is_numeric = true
|
||||
local p = prec ~= nil and prec or 6
|
||||
s = string.format("%." .. p .. "f", to_num(arg))
|
||||
if plus_sign and arg >= 0 then s = "+" .. s end
|
||||
if space_sign and arg >= 0 then s = " " .. s end
|
||||
|
||||
-- %g / %G shortest float representation
|
||||
elseif verb == "g" or verb == "G" then
|
||||
is_numeric = true
|
||||
local p = prec ~= nil and prec or -1
|
||||
local n = to_num(arg)
|
||||
local abs_n = math.abs(n)
|
||||
if p == -1 then
|
||||
-- mimic Go: use %e if exponent < -4 or >= precision(default 6)
|
||||
if abs_n ~= 0 and (abs_n < 1e-4 or abs_n >= 1e6) then
|
||||
s = string.format(verb == "G" and "%.6E" or "%.6e", n)
|
||||
else
|
||||
s = string.format("%.6g", n)
|
||||
if verb == "G" then s = s:upper() end
|
||||
end
|
||||
else
|
||||
s = string.format("%." .. p .. (verb == "G" and "G" or "g"), n)
|
||||
end
|
||||
if plus_sign and n >= 0 then s = "+" .. s end
|
||||
|
||||
-- %s string
|
||||
elseif verb == "s" then
|
||||
s = tostring(arg)
|
||||
s = apply_str_precision(s, prec)
|
||||
|
||||
-- %q quoted string
|
||||
elseif verb == "q" then
|
||||
s = quote_string(tostring(arg))
|
||||
|
||||
-- %c character
|
||||
elseif verb == "c" then
|
||||
local n = to_int(arg)
|
||||
-- utf8.char available in Lua 5.3+
|
||||
if utf8 then
|
||||
s = utf8.char(n)
|
||||
else
|
||||
s = string.char(n) -- ASCII only fallback
|
||||
end
|
||||
|
||||
else
|
||||
error(string.format("fmt: unknown verb %%%s", verb), 2)
|
||||
end
|
||||
|
||||
-- Apply width padding
|
||||
s = apply_width(s, width, left_align, zero_pad, is_numeric)
|
||||
out[#out+1] = s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
--[[
|
||||
fmt.Printf(format, ...)
|
||||
Prints to stdout (no trailing newline, just like Go).
|
||||
--]]
|
||||
function fmt.Printf(format, ...)
|
||||
io.write(fmt.Sprintf(format, ...))
|
||||
end
|
||||
|
||||
--[[
|
||||
fmt.Println(...)
|
||||
Prints args separated by spaces with a trailing newline.
|
||||
--]]
|
||||
function fmt.Println(...)
|
||||
local parts = {}
|
||||
for i = 1, select("#", ...) do
|
||||
parts[i] = tostring(select(i, ...))
|
||||
end
|
||||
print(table.concat(parts, " "))
|
||||
end
|
||||
|
||||
--[[
|
||||
fmt.Fprintf(file, format, ...)
|
||||
Writes formatted output to a file handle.
|
||||
--]]
|
||||
function fmt.Fprintf(file, format, ...)
|
||||
file:write(fmt.Sprintf(format, ...))
|
||||
end
|
||||
|
||||
--[[
|
||||
fmt.Errorf(format, ...) → string
|
||||
Returns a formatted error string (for use with error()).
|
||||
--]]
|
||||
function fmt.Errorf(format, ...)
|
||||
return fmt.Sprintf(format, ...)
|
||||
end
|
||||
|
||||
return fmt
|
||||
Reference in New Issue
Block a user