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.44 Kb · 145 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	neg := n < 0
 62	if neg {
 63		n = -n
 64	}
 65	s := strconv.FormatInt(n, 10)
 66	var b strings.Builder
 67	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}
 78
 79// 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 the
 82// 11–13 case must be checked before the last digit.
 83func Ordinal(n int64) string {
 84	s := strconv.FormatInt(n, 10)
 85	a := n
 86	if a < 0 {
 87		a = -a
 88	}
 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}
102
103// 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) + " " + word
107	}
108	if plural == "" {
109		plural = word + "s"
110	}
111	return strconv.FormatInt(n, 10) + " " + plural
112}
113
114// 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}
130
131// 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 s
140	}
141	if max == 1 {
142		return "…"
143	}
144	return string(r[:max-1]) + "…"
145}