humanize.gno
3.72 Kb · 151 lines
1// Package humanize formats numbers for people rather than machines, as a pure,
2// reusable package: byte sizes, thousands separators, ordinals, pluralisation
3// and block-height "durations".
4//
5// Everything is integer-only. There are no floats here on purpose: gno has no
6// float determinism guarantees worth relying on for consensus output, and a
7// rendered value that differs between nodes would be a consensus bug. One
8// 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 on
11// chain, so "about 2 hours" is a lie dressed as precision; this package says
12// "~1200 blocks" and lets the caller decide what that means on their chain.
13//
14// A live demo of this package is at
15// [r/moul/x/daily/humanizedemo](/r/moul/x/daily/humanizedemo/v0).
16package humanize
17
18import (
19 "strconv"
20 "strings"
21)
22
23// 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 < 0
27 if neg {
28 n = -n
29 }
30 const unit = 1024
31 if n < unit {
32 s := strconv.FormatInt(n, 10) + " B"
33 if neg {
34 return "-" + s
35 }
36 return s
37 }
38 units := []string{"KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
39 div := int64(unit)
40 i := 0
41 for n/div >= unit && i < len(units)-1 {
42 div *= unit
43 i++
44 }
45 // one decimal place without floats: scale by 10, then split
46 scaled := n * 10 / div
47 whole, freq := scaled/10, scaled%10
48 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 "-" + s
55 }
56 return s
57}
58
59// 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 is
62 // math.MinInt64 again, so FormatInt emits its own sign and the neg branch
63 // below prepended a second one. v0 returned "--9,223,372,036,854,775,808".
64 neg := n < 0
65 var m uint64
66 if neg {
67 m = uint64(-(n+1)) + 1
68 } else {
69 m = uint64(n)
70 }
71 s := strconv.FormatUint(m, 10)
72 var b strings.Builder
73 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}
84
85// 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 the
88// 11–13 case must be checked before the last digit.
89func Ordinal(n int64) string {
90 s := strconv.FormatInt(n, 10)
91 a := n
92 if a < 0 {
93 a = -a
94 }
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}
108
109// 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) + " " + word
113 }
114 if plural == "" {
115 plural = word + "s"
116 }
117 return strconv.FormatInt(n, 10) + " " + plural
118}
119
120// 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}
136
137// 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 s
146 }
147 if max == 1 {
148 return "…"
149 }
150 return string(r[:max-1]) + "…"
151}