;;;; format-f64: a number rendered to a fixed number of decimal places. ;;;; ;;;; The runtime's f64->bytes is snprintf "%g" and there is no precision to ;;;; pass it, so this is the first number formatter in the language that a ;;;; caller can steer. Every case below is one a plausible wrong version gets ;;;; wrong, and three of them are the ones that actually ship broken: the ;;;; carry, where the rounded fraction equals the scale and is not a fraction ;;;; at all; the zero padding, without which 1.005 prints as "1.5"; and the ;;;; sign, which belongs to the number and not to its integer part, because ;;;; -0.5 has an integer part of 0 and 0 carries no sign. (defn show [x f64 p i32] () (let [v (format-f64 x p)] (println (string (as-slice v))) (free v))) (defn main [] i32 ;; The ordinary cases, and the one %g cannot do at all: 1/60 wanted to two ;; places is a frame time, and "%g" answers 0.0166667. (show 3.14159 2) ; 3.14 (show 0.0166667 2) ; 0.02 (show 1234.5 1) ; 1234.5 (show 2.0 0) ; 2 (show 2.0 3) ; 2.000 ;; Rounding is half away from zero at the last digit kept, on both signs. (show 0.125 2) ; 0.13 (show -0.125 2) ; -0.13 (show 2.5 0) ; 3 (show -2.5 0) ; -3 ;; The carry. 0.999995 scaled by 10^5 rounds to exactly 100000, which is the ;; next integer; without the carry this prints "0.100000". (show 0.999995 5) ; 1.00000 (show 9.99 1) ; 10.0 (show -9.99 1) ; -10.0 (show 0.99 0) ; 1 ;; Zero padding. The fraction of 1.005 at three places is 5, and five digits ;; is not the same number as 005. (show 1.005 3) ; 1.005 (show 1.0001 4) ; 1.0001 (show 7.0 6) ; 7.000000 ;; The sign lives on the number, not on the integer part: both of these have ;; an integer part of 0, which i64->bytes renders without a sign. (show -0.5 2) ; -0.50 (show -0.004 2) ; -0.00 ;; -0.0 prints as a plain zero. The sign test is (< x 0.0), which -0.0 fails, ;; and text is not where the sign of a zero should be read from. (show 0.0 2) ; 0.00 (show -0.0 2) ; 0.00 ;; Precision is clamped rather than refused, at both ends. (show 1.5 -3) ; 2 (show 1.5 40) ; 1.500000000 ;; The three inputs with no decimal expansion. (show (/ 0.0 0.0) 2) ; nan (show (/ 1.0 0.0) 2) ; inf (show (/ -1.0 0.0) 2) ; -inf ;; Past 9e18 an f64 has no fractional bits and the integer part does not fit ;; in an i64, so this falls back to %g rather than approximating. (show 1e20 2) ; 1e+20 ;; A large magnitude that does fit, where the fraction is genuinely gone: an ;; f64 has no bits below 1 up there, so the padding produces the zeros. (show 1234567890123.0 2) ; 1234567890123.00 ;; And the thing it is for: a formatted number inside a built string, which ;; needs the integer part copied out before the fraction is rendered, because ;; both come through the runtime's one shared scratch buffer. (let [b (vec-new u8)] (append! (addr b) (bytes "fps ")) (let [f (format-f64 59.94 1)] (append! (addr b) (as-slice f)) (free f)) (append! (addr b) (bytes " / frame ")) (let [f (format-f64 0.0166667 4)] (append! (addr b) (as-slice f)) (free f)) (println (string (as-slice b))) ; fps 59.9 / frame 0.0167 (free b)) 0)