Where the digits come from
Three procedures do all the work: turning a rational into rounded decimal digits, turning those digits into a laid-out field, and comparing two numbers of different kinds without rounding either. Everything else is bookkeeping.
One, the exact decimal kernel
A Double is a rational whose denominator is a power of two, and
a Fraction is one already. So the kernel never sees a double: it
sees num/den, both BigInt, and it produces digits by
integer division only. No step of the conversion is itself rounded.
Two, three ways to choose digits
Everything that prints a float asks for one of three modes. The mode, not the caller, decides how the digits are rounded, and the layout step never re-rounds anything.
| Mode | Digits | Used by |
|---|---|---|
| shortest | the fewest that read back as the same double | repr_float, a specifier with no type and no precision |
| significant | exactly n, half to even, trailing zeros dropped | e, E, g, G, n |
| fixed | exactly n after the point, half to even | f, F, % |
The digits then carry a decimal point position, decpt, and the
choice between 0.00012 and 1.2e-04 is made on
decpt alone. That is the same rule for every mode, which is why
a specifier and a repr never disagree about where the exponent starts.
Three, the field
A formatted number is not a string that gets padded. It is six slots, filled in this order, and padding is a decision about which slot grows.
Four, comparing across the tower
Comparison is where a numeric library usually lies. Converting the integer to
a double to compare it loses the difference between
9007199254740992 and 9007199254740993, and that
difference is exactly the sort of thing a big-integer library exists to keep.
So compare_numeric never converts: it cross-multiplies.
let a = @moonnum.parse("9007199254740993")
let b = @moonnum.parse("9007199254740992.0")
a.compare_numeric(b)
1 // greater, though both are the same double
NaN has no place in that order, so ordering against one is refused, and so is
ordering a complex number at all. Equality is a different question and has an
answer for every pair: nan != nan, and
1 == 1.0 == 3/3 == 1+0j.
Five, the two places complex numbers surprise
Division uses Smith's algorithm — divide both parts by whichever of the divisor's components is larger — because the textbook formula overflows and underflows long before the answer does. When a NaN appears where an infinity belongs, the C11 Annex G recovery step reconstructs it.
Powers with a small integer exponent are computed by repeated squaring rather
than through the polar form, which is why (1+1j)² is exactly
2j. A fractional exponent has no such shortcut and goes through
hypot, atan2 and exp, with the error
that implies.
Six, what is deliberately not here
Three edges are out of scope, and each is a decision rather than an oversight:
-
Locale. Presentation type
nbehaves likedorgwith no thousands separator, because a MoonBit program has no locale to consult. Everything else about it — being refused with grouping flags — matches the language this follows. -
Unicode digits. Parsers accept ASCII digits only. A
full-width
42is refused, where the reference implementation would read it as 42. -
Grouping inside the fraction part. The integer part
groups;
0.123456789does not group its fractional digits.