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