moonnum.
TwoFloats

Text that tells the truth about a double

A Double is an exact binary value. Printing it is a translation, and every translation makes a choice: how many digits, where the point goes, when to give up and use an exponent. This page is the choice, made once and made consistently.

The shortest string that reads back

repr_float prints the fewest digits that parse back to the same bit pattern, then decides between fixed and scientific notation on where the point landed — never on how large the number feels. That is why 1e15 is written out in full while 1e16 is not.

moonbit
for v in [42.0, 0.1, 1.0 / 3.0, 1.0e15, 1.0e16, 1.0e-4, 1.0e-5, -0.0] {
  println(@moonnum.repr_float(v))
}
output
42.0
0.1
0.3333333333333333
1000000000000000.0
1e+16
0.0001
1e-05
-0.0

Why not always 17 digits? Because a double does not have 17 digits of meaning; it has 53 bits. Seventeen digits is what it takes to round-trip the worst case, and printing them for 0.1 would claim a precision the value does not have.

The exact value, on demand

When the shortest form is not the honest form, ask for the exact one. A double is a rational whose denominator is a power of two, so it can be written exactly — which is the entire explanation of 0.1 + 0.2.

moonbit
@moonnum.float_hex(0.1)
@moonnum.as_ratio(0.1)
@moonnum.repr_float(0.1 + 0.2)
@moonnum.float_of_hex("0x1.999999999999ap-4")
output
0x1.999999999999ap-4
3602879701896397/36028797018963968
0.30000000000000004
0.1

Rounding is a real decision

round_float rounds the exact binary value to a decimal place, half to even. Rounding the printed digits instead — the shortcut every naive implementation takes — turns 2.675 into 2.68, because the double is slightly below the halfway point.

moonbit
@moonnum.repr_float(@moonnum.round_float(2.675, ndigits=2))
@moonnum.round_big(0.5)
@moonnum.round_big(1.5)
@moonnum.round_big(2.5)
@moonnum.round_big(-2.5)
output
2.67
0
2
2
-2

Floor, ceiling and truncation return integers rather than doubles, so there is no silent precision loss when the value is outside the range an Int can hold. The last line above is also the clearest argument for reading the exact value: 1.0e30 is not ten to the thirtieth, and the integer says by how much.

moonbit
@moonnum.floor_big(-1.2)
@moonnum.ceil_big(-1.2)
@moonnum.trunc_big(-1.2)
@moonnum.floor_big(1.0e30)
output
-2
-1
-1
1000000000000000019884624838656

Infinities and NaNs are values too

They have spellings, they survive a round trip, and they behave: a NaN is not equal to itself, and ordering against one is refused rather than answered with a number that means nothing.

moonbit
@moonnum.parse_float("infinity")
@moonnum.parse_float("-inf")
@moonnum.parse_float("NaN")
@moonnum.parse_float("nan") == @moonnum.parse_float("nan")
output
inf
-inf
nan
false