356 lines
12 KiB
Lua
356 lines
12 KiB
Lua
--[[
|
|
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
|