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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

humanize.gno

3.73 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/nym-moultest123/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}