1// Package humanize formats numbers for people rather than machines, as a pure,2// reusable package: byte sizes, thousands separators, ordinals, pluralisation3// and block-height "durations".4//5// Everything is integer-only. There are no floats here on purpose: gno has no6// float determinism guarantees worth relying on for consensus output, and a7// rendered value that differs between nodes would be a consensus bug. One8// decimal place is produced by scaling by 10 and taking a remainder.9//10// Durations are expressed in BLOCKS, not seconds. There is no wall clock on11// chain, so "about 2 hours" is a lie dressed as precision; this package says12// "~1200 blocks" and lets the caller decide what that means on their chain.13//14// A live demo of this package is at15// [r/moul/x/daily/humanizedemo](/r/moul/x/daily/humanizedemo/v0).16package humanize1718import (19 "strconv"20 "strings"21)2223// Bytes renders n bytes with SI-ish binary units and one decimal place.24// Negative input is rendered with a leading minus rather than rejected.25func Bytes(n int64) string {26 neg := n < 027 if neg {28 n = -n29 }30 const unit = 102431 if n < unit {32 s := strconv.FormatInt(n, 10) + " B"33 if neg {34 return "-" + s35 }36 return s37 }38 units := []string{"KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}39 div := int64(unit)40 i := 041 for n/div >= unit && i < len(units)-1 {42 div *= unit43 i++44 }45 // one decimal place without floats: scale by 10, then split46 scaled := n * 10 / div47 whole, freq := scaled/10, scaled%1048 s := strconv.FormatInt(whole, 10)49 if freq != 0 {50 s += "." + strconv.FormatInt(freq, 10)51 }52 s += " " + units[i]53 if neg {54 return "-" + s55 }56 return s57}5859// Comma inserts thousands separators: 1234567 -> "1,234,567".60func Comma(n int64) string {61 neg := n < 062 if neg {63 n = -n64 }65 s := strconv.FormatInt(n, 10)66 var b strings.Builder67 for i, c := range []byte(s) {68 if i > 0 && (len(s)-i)%3 == 0 {69 b.WriteByte(',')70 }71 b.WriteByte(c)72 }73 if neg {74 return "-" + b.String()75 }76 return b.String()77}7879// Ordinal renders 1 -> "1st", 2 -> "2nd", 11 -> "11th".80//81// The teens are the trap: 11/12/13 take "th" despite ending in 1/2/3, so the82// 11–13 case must be checked before the last digit.83func Ordinal(n int64) string {84 s := strconv.FormatInt(n, 10)85 a := n86 if a < 0 {87 a = -a88 }89 if a%100 >= 11 && a%100 <= 13 {90 return s + "th"91 }92 switch a % 10 {93 case 1:94 return s + "st"95 case 2:96 return s + "nd"97 case 3:98 return s + "rd"99 }100 return s + "th"101}102103// Plural returns "1 block" / "2 blocks", using plural when given, else word+"s".104func Plural(n int64, word, plural string) string {105 if n == 1 || n == -1 {106 return strconv.FormatInt(n, 10) + " " + word107 }108 if plural == "" {109 plural = word + "s"110 }111 return strconv.FormatInt(n, 10) + " " + plural112}113114// Blocks renders a block count at a coarse magnitude — deliberately vague,115// because a block count is not a wall-clock duration.116func Blocks(n int64) string {117 switch {118 case n < 0:119 return "in the past"120 case n == 0:121 return "now"122 case n < 10:123 return Plural(n, "block", "")124 case n < 1000:125 return "~" + Comma(n/10*10) + " blocks"126 default:127 return "~" + Comma(n/100*100) + " blocks"128 }129}130131// Truncate shortens s to at most max runes, appending "…" when it cut.132// Counts RUNES, not bytes, so a multi-byte string is not sliced mid-character.133func Truncate(s string, max int) string {134 if max <= 0 {135 return ""136 }137 r := []rune(s)138 if len(r) <= max {139 return s140 }141 if max == 1 {142 return "…"143 }144 return string(r[:max-1]) + "…"145}146Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.