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}