cybermax / studwright /free /Format.luau
CyberMax-tools's picture
Deploy studwright to the hub
5e00f68 verified
Raw History Blame Contribute Delete
3.61 kB
-- Studwright Format: the number and time text every simulator, tycoon and obby needs.
-- Format.abbreviate(1530000) --> "1.53M"
-- Format.commas(1234567) --> "1,234,567"
-- Format.clock(3725) --> "1:02:05"
-- Format.duration(3725) --> "1h 2m"
-- Format.ordinal(22) --> "22nd"
-- Format.percent(0.0125) --> "1.25%"
-- (c) 2026 CyberMax. Studwright Licence (see LICENSE.txt).
local Format = {}
-- Suffixes up to 10^63. Override with Format.setSuffixes({...}) if your game uses other names.
local SUFFIXES = { "", "K", "M", "B", "T", "Qd", "Qn", "Sx", "Sp", "Oc", "No", "Dc", "Ud", "Dd", "Td", "Qtd", "Qnd", "Sxd", "Spd", "Ocd", "Nod", "Vg" }
function Format.setSuffixes(list: { string })
assert(list[1] == "", "the first suffix must be an empty string")
SUFFIXES = list
end
local function trimZeros(s: string): string
if string.find(s, "%.") then
s = (string.gsub(s, "0+$", ""))
s = (string.gsub(s, "%.$", ""))
end
return s
end
-- abbreviate(n, decimals?) keeps up to `decimals` (default 2) digits after the point and never rounds
-- up into a wrong suffix: 999,999 shows "999.99K", not "1000K".
function Format.abbreviate(n: number, decimals: number?): string
if n ~= n then
return "NaN"
end
if n == math.huge then
return "inf"
elseif n == -math.huge then
return "-inf"
end
local d = decimals or 2
local sign = n < 0 and "-" or ""
n = math.abs(n)
if n < 1000 then
local f = 10 ^ d
return sign .. trimZeros(string.format("%." .. d .. "f", math.floor(n * f + 1e-9) / f))
end
local tier = math.min(math.floor(math.log10(n) / 3), #SUFFIXES - 1)
local scaled = n / 10 ^ (tier * 3)
if scaled >= 1000 and tier < #SUFFIXES - 1 then
tier += 1
scaled /= 1000
end
local f = 10 ^ d
scaled = math.floor(scaled * f + 1e-9) / f
return sign .. trimZeros(string.format("%." .. d .. "f", scaled)) .. SUFFIXES[tier + 1]
end
function Format.commas(n: number): string
local sign = n < 0 and "-" or ""
local whole = tostring(math.floor(math.abs(n)))
local grouped = (string.gsub(string.reverse(whole), "(%d%d%d)", "%1,"))
local out = string.reverse(grouped)
if string.sub(out, 1, 1) == "," then
out = string.sub(out, 2)
end
return sign .. out
end
-- clock(seconds) -> "m:ss" under an hour, "h:mm:ss" above.
function Format.clock(seconds: number): string
seconds = math.max(0, math.floor(seconds))
local h = seconds // 3600
local m = (seconds % 3600) // 60
local s = seconds % 60
if h > 0 then
return string.format("%d:%02d:%02d", h, m, s)
end
return string.format("%d:%02d", m, s)
end
-- duration(seconds) -> the two largest units: "2d 4h", "1h 2m", "45s".
function Format.duration(seconds: number): string
seconds = math.max(0, math.floor(seconds))
local units = { { 86400, "d" }, { 3600, "h" }, { 60, "m" }, { 1, "s" } }
local parts = {}
for _, u in units do
if seconds >= u[1] then
local v = seconds // u[1]
seconds -= v * u[1]
table.insert(parts, v .. u[2])
if #parts == 2 then
break
end
elseif #parts > 0 then
break
end
end
return #parts > 0 and table.concat(parts, " ") or "0s"
end
function Format.ordinal(n: number): string
local last2 = n % 100
local last = n % 10
local suffix = "th"
if last2 < 11 or last2 > 13 then
if last == 1 then
suffix = "st"
elseif last == 2 then
suffix = "nd"
elseif last == 3 then
suffix = "rd"
end
end
return n .. suffix
end
function Format.percent(fraction: number, decimals: number?): string
return trimZeros(string.format("%." .. (decimals or 2) .. "f", fraction * 100)) .. "%"
end
return Format