--[[ 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