Raymond Toy pushed to branch rtoy-print-using-ryu at cmucl / cmucl Commits: 7e00ca38 by Raymond Toy at 2026-05-28T12:48:53-07:00 Fix ~F d=nil to drop leading zero only when needed Two ansi-tests show opposite behavior for the leading "0." ~F when a width is given but no d (precision) is specified. FORMAT.F.46 wants ".0" for "~2f 0.01" (drop) because the width is just 2. FORMAT.F.47 wants "0.0" for "~3f 1e-6" (keep). The rule: keep the zero when the result fits in a width, W; drop it only when keeping would overflow. Added tests for this and updated cmucl.pot - - - - - 3 changed files: - src/code/ryu-print.lisp - src/i18n/locale/cmucl.pot - tests/ryu.lisp Changes: ===================================== src/code/ryu-print.lisp ===================================== @@ -489,10 +489,11 @@ d2fixed and write it right-justified in a field of width W. At least one fractional digit is always shown: if FRAC is 0 the rounded value is integral and a single \"0\" is appended after the - decimal point (CLHS 22.3.3.1). The optional leading zero before - the decimal point is dropped when the field would otherwise - overflow W. Used by the free-format ~F path when the shortest - representation does not fit in W." + decimal point (CLHS 22.3.3.1). The leading zero before the + decimal point is kept when the field fits in W (FORMAT.F.47) and + dropped when keeping it would overflow W (FORMAT.F.46). Used by + the free-format ~F path when the shortest representation does not + fit in W." (declare (type (or single-float double-float) value) (type (integer 0 *) frac)) (let* ((rounded (d2fixed (float (abs value) 1d0) frac)) @@ -510,7 +511,7 @@ (sign-len (if (or is-negative-p at-sign-p) 1 0)) ;; The leading "0." may be shortened to "." when the magnitude ;; is < 1 (integer part is the single digit "0") and nonzero, - ;; and the full field would not fit in W. + ;; and only when the full field would not fit in W. (lpoint-droppable (and (= int-end 1) (char= (char rounded 0) #\0) @@ -544,23 +545,14 @@ (int-len (max 1 (1+ exponent))) (shortest-d (max 0 (- digit-count 1 exponent))) (sign-len (if (or is-negative-p at-sign-p) 1 0)) - (d-fit (and w (- w sign-len int-len 1))) - ;; When the magnitude is < 1 the integer part is a single - ;; "0" that may be dropped (the leading zero is optional), - ;; freeing one more slot for a fractional digit. Compute - ;; the fractional-digit count that exactly fills W under - ;; that rule. - (frac-fit (and w - (if (plusp (1+ exponent)) - (- w sign-len int-len 1) ; "[int]." - (- w sign-len 1))))) ; ".[frac]" + (d-fit (and w (- w sign-len int-len 1)))) (cond ((or (null w) (>= d-fit shortest-d)) ;; No width, or the shortest round-trip form already fits ;; within a field width of W. Emit it directly. (emit-shortest stream mantissa exponent is-negative-p w overflowchar padchar at-sign-p)) - ((and overflowchar (minusp frac-fit)) + ((and overflowchar (minusp d-fit)) ;; Not even the integer part and decimal point fit, and an ;; overflow character was supplied: fill the field. (loop repeat w @@ -568,12 +560,12 @@ (t ;; The shortest form does not fit in W. D was not specified, ;; so round the value to the largest number of fractional - ;; digits FRAC-FIT that fits, then display it. Per CLHS - ;; 22.3.3.1, if the fraction rounds away to nothing a single - ;; zero digit must still appear after the decimal point. We - ;; therefore round at FRAC-FIT places (which may be 0) but - ;; always show at least one fractional digit. - (emit-rounded-to-width stream value (max 0 frac-fit) + ;; digits D-FIT that fits, then display it. Per CLHS 22.3.3.1, + ;; if the fraction rounds away to nothing a single zero digit + ;; must still appear after the decimal point. EMIT-ROUNDED-TO- + ;; WIDTH may drop the leading "0" before the dot if keeping it + ;; would still overflow W. + (emit-rounded-to-width stream value (max 0 d-fit) is-negative-p w overflowchar padchar at-sign-p))))))) ===================================== src/i18n/locale/cmucl.pot ===================================== @@ -7490,10 +7490,11 @@ msgid "" " d2fixed and write it right-justified in a field of width W. At\n" " least one fractional digit is always shown: if FRAC is 0 the\n" " rounded value is integral and a single \"0\" is appended after the\n" -" decimal point (CLHS 22.3.3.1). The optional leading zero before\n" -" the decimal point is dropped when the field would otherwise\n" -" overflow W. Used by the free-format ~F path when the shortest\n" -" representation does not fit in W." +" decimal point (CLHS 22.3.3.1). The leading zero before the\n" +" decimal point is kept when the field fits in W (FORMAT.F.47) and\n" +" dropped when keeping it would overflow W (FORMAT.F.46). Used by\n" +" the free-format ~F path when the shortest representation does not\n" +" fit in W." msgstr "" #: src/code/print.lisp ===================================== tests/ryu.lisp ===================================== @@ -767,18 +767,24 @@ (define-test format-f.d-nil-rounds-to-width-with-forced-zero (:tag :format-f) - ;; ANSI FORMAT.F.45/F.47: ~F with unspecified d rounds the value to - ;; the fractional digits that fit in W, but always shows at least one - ;; fractional digit (a single "0" if the fraction rounds away). The - ;; field expands past W when even that minimum doesn't fit and no - ;; overflow char is given. - ;; ~2f of 1.1 and 1.9 round to integers, forced ".0", overflow to 3. + ;; ANSI FORMAT.F.45/F.46/F.47: ~F with unspecified d rounds the + ;; value to the fractional digits that fit in W, always shows at + ;; least one fractional digit (a single "0" if the fraction rounds + ;; away), and drops the leading "0." -> "." only when keeping it + ;; would overflow W. + ;; ~2f of 1.1 and 1.9 round to integers, forced ".0", overflow to 3 + ;; (FORMAT.F.45). (assert-equal "1.0" (lisp::format-f 1.1f0 2 nil 0 nil nil nil)) (assert-equal "2.0" (lisp::format-f 1.9f0 2 nil 0 nil nil nil)) - ;; ~3f of 1e-6: rounds to .00 (leading zero dropped to fit width 3). - (assert-equal ".00" (lisp::format-f 1.0f-6 3 nil 0 nil nil nil)) - ;; ~4f of 1e-6: .000. - (assert-equal ".000" (lisp::format-f 1.0f-6 4 nil 0 nil nil nil))) + ;; ~3f of 1e-6: "0.0" fits w=3 exactly, leading zero kept + ;; (FORMAT.F.47). + (assert-equal "0.0" (lisp::format-f 1.0f-6 3 nil 0 nil nil nil)) + ;; ~4f of 1e-6: "0.00" fits w=4 exactly. + (assert-equal "0.00" (lisp::format-f 1.0f-6 4 nil 0 nil nil nil)) + ;; ~2f, ~1f, ~0f of 0.01: leading zero dropped to ".0" (FORMAT.F.46). + (assert-equal ".0" (lisp::format-f 0.01f0 2 nil 0 nil nil nil)) + (assert-equal ".0" (lisp::format-f 0.01f0 1 nil 0 nil nil nil)) + (assert-equal ".0" (lisp::format-f 0.01f0 0 nil 0 nil nil nil))) (define-test format-f.single-narrow-width-with-overflow-char (:tag :format-f :single-float) View it on GitLab: https://gitlab.common-lisp.net/cmucl/cmucl/-/commit/7e00ca38b5112e3ccd5e824d... -- View it on GitLab: https://gitlab.common-lisp.net/cmucl/cmucl/-/commit/7e00ca38b5112e3ccd5e824d... You're receiving this email because of your account on gitlab.common-lisp.net. Manage all notifications: https://gitlab.common-lisp.net/-/profile/notifications | Help: https://gitlab.common-lisp.net/help
participants (1)
-
Raymond Toy (@rtoy)