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.

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}