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

v0 source pure

Package num formats numbers for a human to read: fixed-point amounts, percentages and zero padding.

Readme View source

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:

  • GNOT computes frac := 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.
  • ccAmt handles the sign correctly and then recurses on -n. On math.MinInt64, -n is math.MinInt64 again, 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,567 belongs to p/moul/x/daily/humanize, which already owns Comma, Bytes, Ordinal, Plural and Blocks. One owner per fact. (Its Comma returns --9,223,372,036,854,775,808 at the int64 minimum, the same n = -n shape as everything else on this page, and that is a bump for it rather than a reason to write a second one here.)
  • Markdown. Dec returns digits. Wrap it in kit/ui or p/moul/md if 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. Pad is for display. A key that must sort in insertion order belongs to kit/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.

Overview

Package num formats numbers for a human to read: fixed-point amounts, percentages and zero padding.

Thousands separators are NOT here. They belong to p/moul/x/daily/humanize(/p/moul/x/daily/humanize/v1), which already owns Comma, Bytes, Ordinal, Plural and Blocks.

It exists because nothing in p/moul or p/nt did. Measured across the 828 .gno files deployed on gnoland-1 by someone other than moul, five independent teams wrote their own ugnot-to-GNOT formatter, and the two that are byte-level cousins disagree on every input that is not positive. The README carries the full table.

The contract

Every function here is total: no input panics, including the int64 minimum, and no input returns a malformed string. That is the whole point of the package, because it is precisely where the hand-rolled copies fail.

Where the decimals come from

gno.land denominates GNOT in ugnot, one millionth of a GNOT, so GNOT is Dec with decimals = 6. A GRC20 token declares its own decimals; pass that to Dec rather than assuming six.

Constants 1

const GnotDecimals

1const GnotDecimals = 6
source

GnotDecimals is the number of decimal places between ugnot and GNOT.

Functions 6

func Dec

1func Dec(v int64, decimals int) string
source

Dec renders v as a fixed-point decimal with the given number of places, trailing zeros trimmed: Dec(1234500, 6) is "1.2345", Dec(2000000, 6) is "2".

The sign is carried on the whole part, so Dec(-1, 6) is "-0.000001" and not "0.999999". Dec panics only on a negative or out-of-range decimals, which is a programming error rather than a value.

func DecFixed

1func DecFixed(v int64, decimals int) string
source

DecFixed is Dec without the trailing-zero trim, so every value renders at the same width: DecFixed(2000000, 6) is "2.000000". Use it in a column.

func GNOT

1func GNOT(ugnot int64) string
source

GNOT renders an amount in ugnot as GNOT, with no unit: "1.234567".

func GNOTf

1func GNOTf(ugnot int64) string
source

GNOTf is GNOT with the unit appended: "1.234567 GNOT".

func Pad

1func Pad(v int64, width int) string
source

Pad renders v in base 10, left-padded with zeros to at least width characters: Pad(7, 3) is "007". A negative value keeps its sign outside the padding, so Pad(-7, 3) is "-007" and the string is width+1 long.

It exists because ufmt supports no width flags: ufmt.Sprintf("%03d", 7) returns "7", silently.

Pad is for DISPLAY. For an avl key that must sort in insertion order, use p/moul/kit/store(/p/moul/kit/store/v0), which has no ceiling to overflow.

func Pct

1func Pct(bps int64) string
source

Pct renders basis points as a percentage: Pct(1234) is "12.34%". One basis point is a hundredth of a percent, which is the unit every fee on this chain is already quoted in.

Imports 2

  • strconv stdlib
  • strings stdlib

Source Files 4