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

num.gno

4.49 Kb · 132 lines
  1// Package num formats numbers for a human to read: fixed-point amounts,
  2// percentages and zero padding.
  3//
  4// Thousands separators are NOT here. They belong to
  5// [p/moul/x/daily/humanize](/p/moul/x/daily/humanize/v1), which already owns
  6// Comma, Bytes, Ordinal, Plural and Blocks.
  7//
  8// It exists because nothing in p/moul or p/nt did. Measured across the 828
  9// .gno files deployed on gnoland-1 by someone other than moul, five
 10// independent teams wrote their own ugnot-to-GNOT formatter, and the two that
 11// are byte-level cousins disagree on every input that is not positive. The
 12// README carries the full table.
 13//
 14// # The contract
 15//
 16// Every function here is total: no input panics, including the int64 minimum,
 17// and no input returns a malformed string. That is the whole point of the
 18// package, because it is precisely where the hand-rolled copies fail.
 19//
 20// # Where the decimals come from
 21//
 22// gno.land denominates GNOT in ugnot, one millionth of a GNOT, so [GNOT] is
 23// [Dec] with decimals = 6. A GRC20 token declares its own decimals; pass that
 24// to [Dec] rather than assuming six.
 25package num
 26
 27import (
 28	"strconv"
 29	"strings"
 30)
 31
 32// GnotDecimals is the number of decimal places between ugnot and GNOT.
 33const GnotDecimals = 6
 34
 35// maxDecimals caps the scale [Dec] accepts. An int64 holds at most 19 decimal
 36// digits, so beyond that every value is pure fraction and the cap costs
 37// nothing real while keeping the padding loop bounded.
 38const maxDecimals = 19
 39
 40// Dec renders v as a fixed-point decimal with the given number of places,
 41// trailing zeros trimmed: Dec(1234500, 6) is "1.2345", Dec(2000000, 6) is "2".
 42//
 43// The sign is carried on the whole part, so Dec(-1, 6) is "-0.000001" and not
 44// "0.999999". Dec panics only on a negative or out-of-range decimals, which is
 45// a programming error rather than a value.
 46func Dec(v int64, decimals int) string {
 47	return dec(v, decimals, false)
 48}
 49
 50// DecFixed is [Dec] without the trailing-zero trim, so every value renders at
 51// the same width: DecFixed(2000000, 6) is "2.000000". Use it in a column.
 52func DecFixed(v int64, decimals int) string {
 53	return dec(v, decimals, true)
 54}
 55
 56// GNOT renders an amount in ugnot as GNOT, with no unit: "1.234567".
 57func GNOT(ugnot int64) string { return Dec(ugnot, GnotDecimals) }
 58
 59// GNOTf is [GNOT] with the unit appended: "1.234567 GNOT".
 60func GNOTf(ugnot int64) string { return GNOT(ugnot) + " GNOT" }
 61
 62// Pct renders basis points as a percentage: Pct(1234) is "12.34%".
 63// One basis point is a hundredth of a percent, which is the unit every fee on
 64// this chain is already quoted in.
 65func Pct(bps int64) string { return Dec(bps, 2) + "%" }
 66
 67// Pad renders v in base 10, left-padded with zeros to at least width
 68// characters: Pad(7, 3) is "007". A negative value keeps its sign outside the
 69// padding, so Pad(-7, 3) is "-007" and the string is width+1 long.
 70//
 71// It exists because ufmt supports no width flags: ufmt.Sprintf("%03d", 7)
 72// returns "7", silently.
 73//
 74// Pad is for DISPLAY. For an avl key that must sort in insertion order, use
 75// [p/moul/kit/store](/p/moul/kit/store/v0), which has no ceiling to overflow.
 76func Pad(v int64, width int) string {
 77	neg, m := mag(v)
 78	s := strconv.FormatUint(m, 10)
 79	if n := width - len(s); n > 0 {
 80		s = strings.Repeat("0", n) + s
 81	}
 82	if neg {
 83		return "-" + s
 84	}
 85	return s
 86}
 87
 88func dec(v int64, decimals int, fixed bool) string {
 89	if decimals < 0 || decimals > maxDecimals {
 90		panic("num: decimals out of range [0, 19]")
 91	}
 92	neg, m := mag(v)
 93	sign := ""
 94	if neg {
 95		sign = "-"
 96	}
 97	if decimals == 0 {
 98		return sign + strconv.FormatUint(m, 10)
 99	}
100
101	digits := strconv.FormatUint(m, 10)
102	if n := decimals + 1 - len(digits); n > 0 {
103		digits = strings.Repeat("0", n) + digits
104	}
105	cut := len(digits) - decimals
106	whole, frac := digits[:cut], digits[cut:]
107
108	if !fixed {
109		frac = strings.TrimRight(frac, "0")
110		if frac == "" {
111			return sign + whole
112		}
113	}
114	return sign + whole + "." + frac
115}
116
117// mag splits v into a sign and an unsigned magnitude.
118//
119// -v on math.MinInt64 is math.MinInt64 again, so every shape that stays in
120// int64 is wrong there: `if v < 0 { v = -v }` silently keeps the value
121// negative, and a recursive `return "-" + f(-v)` never terminates at all. Both
122// are live on gnoland-1 today; the second halts the realm.
123//
124// `uint64(-v)` happens to give the right answer by twos-complement wraparound,
125// but it gets it from the overflow rather than despite it. -(v+1)+1 never
126// overflows, so it does not depend on that.
127func mag(v int64) (bool, uint64) {
128	if v < 0 {
129		return true, uint64(-(v+1)) + 1
130	}
131	return false, uint64(v)
132}