moonnum.
FiveFormatting

One specifier for four kinds

The specifier is parsed once, left to right, in a fixed order. Learning the order is learning the whole language; everything else is which type does what with the precision.

[[fill]align][sign][z][#][0][width][grouping][.precision][type]
PartValuesMeaning
fill, align*>, <, >, ^, =padding character, and where the padding goes; = puts it after the sign
sign+, -, spacewhat to print before a non-negative number
zflaga zero result is never negative
#flagalternate form: prefixes for integers, kept zeros for floats
0flagzero padding, which participates in grouping
widthdigitsminimum columns
grouping,, _thousands separators; _ groups binary, octal and hex by four
.precisiondigitsdigits after the point for f, significant digits for g
typeb c d e E f F g G n o x X %presentation; none means "like a repr, with at least one decimal"

Width, fill, alignment

moonbit
@moonnum.format_int(1234567N, "15d")
@moonnum.format_int(1234567N, "<15d")
@moonnum.format_int(1234567N, "^15d")
@moonnum.format_int(1234567N, "*^15d")
@moonnum.format_int(1234567N, "0>15d")
output
|      1234567|
|1234567       |
|    1234567    |
|****1234567****|
|000000001234567|

Sign and zero padding

Zero padding goes inside the sign and, for other bases, inside the prefix: -00001234567, 0x000_00ff. Padding outside the sign is the classic bug that makes a column of numbers impossible to align.

moonbit
@moonnum.format_int(1234567N, "+15d")
@moonnum.format_int(-1234567N, "+15d")
@moonnum.format_int(1234567N, "015d")
@moonnum.format_int(-1234567N, "015d")
@moonnum.format_int(255N, "#010_x")
output
       +1234567
       -1234567
000000001234567
-00000001234567
0x000_00ff

Grouping that reaches into the zeros

With zero padding, the separators have to be inserted among the padding zeros too — otherwise the column stops lining up at exactly the values that need it most.

moonbit
@moonnum.format_int(1234567N, ",d")
@moonnum.format_int(1234567N, "015,d")
@moonnum.format_int(255N, "_b")
@moonnum.format_float(1234.5678, ",.2f")
output
1,234,567
000,001,234,567
1111_1111
1,234.57

Precision means different things

f counts digits after the point, e counts digits after the point and always writes an exponent, g counts significant digits and drops trailing zeros. No type at all is g with a guaranteed decimal: format(1.0, "") is 1.0, not 1.

moonbit
@moonnum.format_float(1234.5678, "f")
@moonnum.format_float(1234.5678, ".2f")
@moonnum.format_float(1234.5678, ".3e")
@moonnum.format_float(1234.5678, ".3g")
@moonnum.format_float(1234.5678, "")
output
1234.567800
1234.57
1.235e+03
1.23e+03
1234.5678

Three flags that are easy to get wrong

# keeps the zeros g would drop; z coerces a negative zero after rounding, before the sign is chosen; % multiplies by a hundred, formats as f, and appends a percent sign.

moonbit
@moonnum.format_float(2.0, "g")
@moonnum.format_float(2.0, "#g")
@moonnum.format_float(-0.0, "")
@moonnum.format_float(-0.0, "z")
@moonnum.format_float(-0.0001, "z.2f")
@moonnum.format_float(0.25, ".2%")
output
2
2.00000
-0.0
0.0
0.00
25.00%

One entry point

format takes a Number and dispatches to the right kind, so a table of mixed values can be printed with one specifier. A float type on an integer converts it; a fraction formats exactly; a complex number applies the specifier to both halves.

moonbit
for text in ["42", "-0.5", "22/7", "3+4j"] {
  println(@moonnum.format(@moonnum.parse(text), ".3f"))
}
output
42.000
-0.500
3.143
3.000+4.000j

Refusals

A specifier that cannot mean anything is refused rather than approximated: precision on an integer, , on a hexadecimal number, z on an integer, zero padding on a complex number, two grouping characters at once. The offset is the position in the specifier that cannot be read, which is where the caret should point.

moonbit
@moonnum.format_int(255N, ".2d")
@moonnum.format_int(255N, ",x")
@moonnum.format_int(255N, "z")
@moonnum.format_float(1.5, "d")
output
precision is not allowed for integers at offset 2
cannot specify ',' with this presentation type at offset 1
negative-zero coercion is not allowed for integers at offset 0
unknown format code for a float at offset 0