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 // The magnitude goes through uint64 rather than -n: -n on math.MinInt64 is62 // math.MinInt64 again, so FormatInt emits its own sign and the neg branch63 // below prepended a second one. v0 returned "--9,223,372,036,854,775,808".64 neg := n < 065 var m uint6466 if neg {67 m = uint64(-(n+1)) + 168 } else {69 m = uint64(n)70 }71 s := strconv.FormatUint(m, 10)72 var b strings.Builder73 for i, c := range []byte(s) {74 if i > 0 && (len(s)-i)%3 == 0 {75 b.WriteByte(',')76 }77 b.WriteByte(c)78 }79 if neg {80 return "-" + b.String()81 }82 return b.String()83}8485// Ordinal renders 1 -> "1st", 2 -> "2nd", 11 -> "11th".86//87// The teens are the trap: 11/12/13 take "th" despite ending in 1/2/3, so the88// 11–13 case must be checked before the last digit.89func Ordinal(n int64) string {90 s := strconv.FormatInt(n, 10)91 a := n92 if a < 0 {93 a = -a94 }95 if a%100 >= 11 && a%100 <= 13 {96 return s + "th"97 }98 switch a % 10 {99 case 1:100 return s + "st"101 case 2:102 return s + "nd"103 case 3:104 return s + "rd"105 }106 return s + "th"107}108109// Plural returns "1 block" / "2 blocks", using plural when given, else word+"s".110func Plural(n int64, word, plural string) string {111 if n == 1 || n == -1 {112 return strconv.FormatInt(n, 10) + " " + word113 }114 if plural == "" {115 plural = word + "s"116 }117 return strconv.FormatInt(n, 10) + " " + plural118}119120// Blocks renders a block count at a coarse magnitude — deliberately vague,121// because a block count is not a wall-clock duration.122func Blocks(n int64) string {123 switch {124 case n < 0:125 return "in the past"126 case n == 0:127 return "now"128 case n < 10:129 return Plural(n, "block", "")130 case n < 1000:131 return "~" + Comma(n/10*10) + " blocks"132 default:133 return "~" + Comma(n/100*100) + " blocks"134 }135}136137// Truncate shortens s to at most max runes, appending "…" when it cut.138// Counts RUNES, not bytes, so a multi-byte string is not sliced mid-character.139func Truncate(s string, max int) string {140 if max <= 0 {141 return ""142 }143 r := []rune(s)144 if len(r) <= max {145 return s146 }147 if max == 1 {148 return "…"149 }150 return string(r[:max-1]) + "…"151}152Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.