Raymond Toy pushed to branch rtoy-print-using-ryu at cmucl / cmucl
Commits:
-
0a9c9c7c
by Raymond Toy at 2026-05-28T07:23:00-07:00
3 changed files:
Changes:
| ... | ... | @@ -491,24 +491,79 @@ |
| 491 | 491 | (int-len (max 1 (1+ exponent)))
|
| 492 | 492 | (shortest-d (max 0 (- digit-count 1 exponent)))
|
| 493 | 493 | (sign-len (if (or is-negative-p at-sign-p) 1 0))
|
| 494 | - (d-fit (and w (- w sign-len int-len 1))))
|
|
| 494 | + (d-fit (and w (- w sign-len int-len 1)))
|
|
| 495 | + ;; When the magnitude is < 1 the integer part is a single
|
|
| 496 | + ;; "0" that may be dropped (the leading zero is optional),
|
|
| 497 | + ;; freeing one more slot for a fractional digit. Compute
|
|
| 498 | + ;; the fractional-digit count that exactly fills W under
|
|
| 499 | + ;; that rule.
|
|
| 500 | + (frac-fit (and w
|
|
| 501 | + (if (plusp (1+ exponent))
|
|
| 502 | + (- w sign-len int-len 1) ; "[int]."
|
|
| 503 | + (- w sign-len 1))))) ; ".[frac]"
|
|
| 495 | 504 | (cond
|
| 496 | 505 | ((or (null w) (>= d-fit shortest-d))
|
| 497 | - ;; No width or the shortest form fits within a field width
|
|
| 498 | - ;; of W.
|
|
| 506 | + ;; No width, or the shortest round-trip form already fits
|
|
| 507 | + ;; within a field width of W. Emit it directly.
|
|
| 499 | 508 | (emit-shortest stream mantissa exponent is-negative-p w
|
| 500 | 509 | overflowchar padchar at-sign-p))
|
| 501 | - (overflowchar
|
|
| 502 | - ;; Shortest form does not fit and OVERFLOWCHAR is set; fill
|
|
| 503 | - ;; the field with overflow characters.
|
|
| 510 | + ((and overflowchar (minusp frac-fit))
|
|
| 511 | + ;; Not even the integer part and decimal point fit, and an
|
|
| 512 | + ;; overflow character was supplied: fill the field.
|
|
| 504 | 513 | (loop repeat w
|
| 505 | 514 | do (write-char overflowchar stream)))
|
| 506 | 515 | (t
|
| 507 | - ;; Shortest form does not fit and no OVERFLOWCHAR; emit the
|
|
| 508 | - ;; full shortest form, letting the field expand. CLHS
|
|
| 509 | - ;; 22.3.3.1 requires this.
|
|
| 510 | - (emit-shortest stream mantissa exponent is-negative-p w
|
|
| 511 | - overflowchar padchar at-sign-p)))))))
|
|
| 516 | + ;; The shortest form does not fit in W. D was not specified,
|
|
| 517 | + ;; so round the value to the largest number of fractional
|
|
| 518 | + ;; digits FRAC-FIT that fits, then display it. Per CLHS
|
|
| 519 | + ;; 22.3.3.1, if the fraction rounds away to nothing a single
|
|
| 520 | + ;; zero digit must still appear after the decimal point. We
|
|
| 521 | + ;; therefore round at FRAC-FIT places (which may be 0) but
|
|
| 522 | + ;; always show at least one fractional digit.
|
|
| 523 | + (emit-rounded-to-width stream value (max 0 frac-fit)
|
|
| 524 | + is-negative-p w
|
|
| 525 | + overflowchar padchar at-sign-p)))))))
|
|
| 526 | + |
|
| 527 | +(defun emit-rounded-to-width (stream value frac is-negative-p w
|
|
| 528 | + overflowchar padchar at-sign-p)
|
|
| 529 | + "Round (the magnitude of) VALUE to FRAC fractional digits with
|
|
| 530 | + d2fixed and write it right-justified in a field of width W. At
|
|
| 531 | + least one fractional digit is always shown: if FRAC is 0 the
|
|
| 532 | + rounded value is integral and a single \"0\" is appended after the
|
|
| 533 | + decimal point (CLHS 22.3.3.1). The optional leading zero before
|
|
| 534 | + the decimal point is dropped when the field would otherwise
|
|
| 535 | + overflow W. Used by the free-format ~F path when the shortest
|
|
| 536 | + representation does not fit in W."
|
|
| 537 | + (declare (type (or single-float double-float) value)
|
|
| 538 | + (type (integer 0 *) frac))
|
|
| 539 | + (let* ((rounded (d2fixed (float (abs value) 1d0) frac))
|
|
| 540 | + ;; ROUNDED is "ddd" (frac=0) or "ddd.ddd" (frac>0). Split it
|
|
| 541 | + ;; into integer and fractional digit runs.
|
|
| 542 | + (dot (position #\. rounded))
|
|
| 543 | + (int-part (if dot (subseq rounded 0 dot) rounded))
|
|
| 544 | + (frac-part (if dot (subseq rounded (1+ dot)) ""))
|
|
| 545 | + ;; Force at least one fractional digit.
|
|
| 546 | + (frac-out (if (zerop (length frac-part)) "0" frac-part))
|
|
| 547 | + (sign-len (if (or is-negative-p at-sign-p) 1 0))
|
|
| 548 | + ;; The leading "0." may be shortened to "." when the magnitude
|
|
| 549 | + ;; is < 1 (integer part is a single "0") and nonzero, and the
|
|
| 550 | + ;; full field would not fit in W.
|
|
| 551 | + (lpoint-droppable
|
|
| 552 | + (and (string= int-part "0")
|
|
| 553 | + (not (zerop value))))
|
|
| 554 | + (full-len (+ sign-len (length int-part) 1 (length frac-out)))
|
|
| 555 | + (drop-leading-zero-p
|
|
| 556 | + (and lpoint-droppable w (> full-len w)))
|
|
| 557 | + (field-len (if drop-leading-zero-p (1- full-len) full-len)))
|
|
| 558 | + (flet ((write-field ()
|
|
| 559 | + (cond (is-negative-p (write-char #\- stream))
|
|
| 560 | + (at-sign-p (write-char #\+ stream)))
|
|
| 561 | + (unless drop-leading-zero-p
|
|
| 562 | + (write-string int-part stream))
|
|
| 563 | + (write-char #\. stream)
|
|
| 564 | + (write-string frac-out stream)))
|
|
| 565 | + (declare (dynamic-extent #'write-field))
|
|
| 566 | + (pad-overflow stream field-len w overflowchar padchar #'write-field))))
|
|
| 512 | 567 | |
| 513 | 568 | (defun format-f (value w d k overflowchar padchar at-sign-p)
|
| 514 | 569 | (declare (type (or single-float double-float) value)
|
| ... | ... | @@ -367,20 +367,17 @@ |
| 367 | 367 | (assert-equal " 1.0"
|
| 368 | 368 | (lisp::format-f 1d0 10 nil 0 nil nil nil)))
|
| 369 | 369 | |
| 370 | -(define-test format-f.d-nil-with-w-no-shrink
|
|
| 370 | +(define-test format-f.d-nil-with-w-shrinks
|
|
| 371 | 371 | (:tag :format-f)
|
| 372 | - ;; CLHS 22.3.3.1: when d is unspecified, the digit count is set so
|
|
| 373 | - ;; that the result reads back as an EQUAL float, with no extraneous
|
|
| 374 | - ;; trailing zeros. This is the round-trip count. When the result
|
|
| 375 | - ;; does not fit in w and no overflowchar is supplied, the field
|
|
| 376 | - ;; expands -- it does NOT shrink the digits, because doing so would
|
|
| 377 | - ;; produce a different float on read-back. (Earlier ryu code did
|
|
| 378 | - ;; shrink here; that change was needed to pass ANSI FORMAT.F.5.)
|
|
| 379 | - (assert-equal "1.234567"
|
|
| 372 | + ;; d=nil with a tight width: CL rounds the value to the largest
|
|
| 373 | + ;; number of fractional digits that fits in W (matching the B&D
|
|
| 374 | + ;; reference output). 3.141592653589793 shrinks to fit; 1.234567
|
|
| 375 | + ;; with w=6 rounds to "1.2346".
|
|
| 376 | + (assert-equal "1.2346"
|
|
| 380 | 377 | (lisp::format-f 1.234567d0 6 nil 0 nil nil nil))
|
| 381 | - (assert-equal "3.141592653589793"
|
|
| 378 | + (assert-equal "3.14159265"
|
|
| 382 | 379 | (lisp::format-f 3.141592653589793d0 10 nil 0 nil nil nil))
|
| 383 | - (assert-equal "3.141592653589793"
|
|
| 380 | + (assert-equal "3.141593"
|
|
| 384 | 381 | (lisp::format-f 3.141592653589793d0 8 nil 0 nil nil nil)))
|
| 385 | 382 | |
| 386 | 383 | (define-test format-f.d-nil-with-w-overflow
|
| ... | ... | @@ -755,10 +752,11 @@ |
| 755 | 752 | |
| 756 | 753 | (define-test format-f.single-narrow-width-no-overflow-char
|
| 757 | 754 | (:tag :format-f :single-float)
|
| 758 | - ;; CLHS 22.3.3.1: when the shortest form does not fit in the
|
|
| 759 | - ;; specified width and no overflow character is supplied, the field
|
|
| 760 | - ;; expands rather than truncating digits. Round-trip and the
|
|
| 761 | - ;; "at least one digit after the decimal point" rule must both hold.
|
|
| 755 | + ;; ~F with unspecified d and a width too small for the shortest form:
|
|
| 756 | + ;; the value is rounded to the fractional digits that fit, but at
|
|
| 757 | + ;; least one fractional digit is always shown (CLHS 22.3.3.1). With
|
|
| 758 | + ;; no overflow char the field expands when even that minimum
|
|
| 759 | + ;; doesn't fit. 1.0 -> "1.0".
|
|
| 762 | 760 | (assert-equal "1.0"
|
| 763 | 761 | (lisp::format-f 1.0f0 2 nil 0 nil nil nil))
|
| 764 | 762 | (assert-equal "1.0"
|
| ... | ... | @@ -767,6 +765,21 @@ |
| 767 | 765 | (assert-equal "1.0"
|
| 768 | 766 | (lisp::format-f 1.0d0 2 nil 0 nil nil nil)))
|
| 769 | 767 | |
| 768 | +(define-test format-f.d-nil-rounds-to-width-with-forced-zero
|
|
| 769 | + (:tag :format-f)
|
|
| 770 | + ;; ANSI FORMAT.F.45/F.47: ~F with unspecified d rounds the value to
|
|
| 771 | + ;; the fractional digits that fit in W, but always shows at least one
|
|
| 772 | + ;; fractional digit (a single "0" if the fraction rounds away). The
|
|
| 773 | + ;; field expands past W when even that minimum doesn't fit and no
|
|
| 774 | + ;; overflow char is given.
|
|
| 775 | + ;; ~2f of 1.1 and 1.9 round to integers, forced ".0", overflow to 3.
|
|
| 776 | + (assert-equal "1.0" (lisp::format-f 1.1f0 2 nil 0 nil nil nil))
|
|
| 777 | + (assert-equal "2.0" (lisp::format-f 1.9f0 2 nil 0 nil nil nil))
|
|
| 778 | + ;; ~3f of 1e-6: rounds to .00 (leading zero dropped to fit width 3).
|
|
| 779 | + (assert-equal ".00" (lisp::format-f 1.0f-6 3 nil 0 nil nil nil))
|
|
| 780 | + ;; ~4f of 1e-6: .000.
|
|
| 781 | + (assert-equal ".000" (lisp::format-f 1.0f-6 4 nil 0 nil nil nil)))
|
|
| 782 | + |
|
| 770 | 783 | (define-test format-f.single-narrow-width-with-overflow-char
|
| 771 | 784 | (:tag :format-f :single-float)
|
| 772 | 785 | ;; With overflowchar supplied, the field is filled with the overflow
|