Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}