/p/moul/kit/num/v0
gno.land/p/moul/kit/num
Numbers, formatted for a human to read: fixed-point amounts, percentages, zero padding.
Third package of the p/moul/kit/* layer
(moul/gno-contracts#151),
after kit/ui and kit/store. kit composes the existing
packages, it does not replace them.
Why it exists
Unlike the rest of kit, this one is not deduplication. Nothing in p/moul
or p/nt turns 1234567 ugnot into 1.234567 GNOT, so every team that needed
it wrote it.
Measured 2026-09-28 across the 828 .gno files deployed on gnoland-1 by
somebody other than moul (gnolang/tx-exports, mainnet, genesis excluded):
| who | what they wrote | copies |
|---|---|---|
the bubblerumble realms |
GNOT(ugnot int64) |
5, byte-identical |
kourt |
ccAmt(n int64) |
2 |
| GnoSwap | gnsDisplayScale + zeroPadUnsigned + p/gnoswap/utils.FormatUint |
3 |
bubbletreasury |
one = int64(1_000_000) |
1 |
Five independent answers to "show money to a human". GnoSwap wrote a type
switch over any to turn an integer into a string, because there was nothing
to import.
The two live implementations disagree on every input that is not positive
Both were run verbatim, 2026-09-28, not read:
| ugnot | bubblerumble's GNOT |
kourt's ccAmt |
correct |
|---|---|---|---|
| 1234567 | 1.234567 GNOT |
1.234567 |
✅ both |
| 100 | 0.0001 GNOT |
0.0001 |
✅ both |
| -100 | 0.999 GNOT |
-0.0001 |
sign dropped, digits wrong |
| -1500000 | -1. GNOT |
-1.5 |
not a number |
| -1 | 0.99999 GNOT |
-0.000001 |
sign dropped, digits wrong |
| int64 min | -9223372036854.24192 |
infinite recursion | one wrong, one halts |
Two different failures from the same 8 lines:
GNOTcomputesfrac := ugnot % 1_000_000, which is negative for a negative amount, then feeds it to a trick (FormatInt(1_000_000+frac)[1:]) that only works on a positive remainder. Below one GNOT the sign vanishes entirely and the digits become the ten's complement.ccAmthandles the sign correctly and then recurses on-n. Onmath.MinInt64,-nismath.MinInt64again, still negative, so it calls itself forever. Measured: stack overflow.
Neither is reachable from a user-supplied negative today as far as I could tell, and I did not try to prove reachability either way. The point is that this is 8 lines everyone rewrites, half of them get it wrong, and nobody ever tests the sign.
This repo is not the exception, it is the sixth case
Six more copies live here, in five more implementations, and they disagree with each other as well:
| where | shape | gnot(-1500000) |
|---|---|---|
r/moul/vesting |
u = -u, never trims |
-1.500000 GNOT |
r/moul/faucet |
u = -u, trims |
-1.5 |
p/moul/x/storagecost |
amount = -amount, trims |
-1.5 |
r/moul/x/games/lastwords |
no sign handling | -1.-5 |
p/moul/x/wiki |
no sign handling | -1.-5 GNOT |
r/moul/x/daily/wrapped |
no decimals at all | n/a |
vesting also pads to six digits where the other four trim, so the same
amount reads 1.500000 in one realm and 1.5 in the next. All six are live on
mainnet, so porting them is a bump each and a separate decision, not part of
this package.
What is here
Dec(v, decimals) |
fixed point, trailing zeros trimmed. Dec(1234500, 6) is 1.2345 |
DecFixed(v, decimals) |
same, padded to full width, for a column |
GNOT(ugnot) / GNOTf(ugnot) |
Dec(v, 6), without and with the unit |
Pct(bps) |
basis points to a percentage. Pct(1234) is 12.34% |
Pad(v, width) |
Pad(7, 3) is 007, because ufmt has no width flags |
For a GRC20, pass the token's own decimals to Dec: six is GNOT's, not
everyone's.
The contract
Every function is total. No input panics, including math.MinInt64, and no
input returns a string that is not a number. Dec panics on a decimals
outside [0, 19], which is a programming error rather than a value.
That contract is the package. It is pinned by
TestEveryDecimalStringParsesBack, which parses every output back and checks
the sign survived: swap in either live implementation and it goes red on four
inputs, which is how it was verified rather than assumed.
What is deliberately not here
- Thousands separators.
1,234,567belongs top/moul/x/daily/humanize, which already ownsComma,Bytes,Ordinal,PluralandBlocks. One owner per fact. (ItsCommareturns--9,223,372,036,854,775,808at the int64 minimum, the samen = -nshape as everything else on this page, and that is a bump for it rather than a reason to write a second one here.) - Markdown.
Decreturns digits. Wrap it inkit/uiorp/moul/mdif it needs to be bold or in a cell. - Compact notation (
1.2k,3.4M). Nobody on chain has written one, so building it here would be speculative generality on a public path. - avl keys.
Padis for display. A key that must sort in insertion order belongs tokit/store, which has no ceiling to overflow. - Parsing. One direction only until something needs the other.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.