ccl
ccl copied to clipboard
Numerous incorrect printings of floating point numbers with FORMAT directive ~F
I tested with:
Clozure Common Lisp Version 1.12 (v1.12) WindowsX8664
and the following program and test cases:
(defvar *test-cases*
;;((<formt control string> <argument>) <expected result>)
'((("~13,2,9,'?,'_@F" 1) "?????????????")
(("~13,2,9,'F,'_@F" 1) "FFFFFFFFFFFFF")
(("~13,2,9,'Q,'_@F" 1) "QQQQQQQQQQQQQ")
(("~,,,,F" 1234.56) "1234.56")
(("~,,,,F" -1234.56) "-1234.56")
(("~5,1,,,F" 1234.56) "1234.6")
(("~6,1,,,F" 1234.56) "1234.6")
(("~7,1,,,F" 1234.56) " 1234.6")
(("~10,1,,,F" 1234.56) " 1234.6")
(("~10,1,,,@F" 1234.56) " +1234.6")
(("~5,1,,,F" -1234.56) "-1234.6")
(("~6,1,,,F" -1234.56) "-1234.6")
(("~7,1,,,F" -1234.56) "-1234.6")
(("~8,1,,,F" -1234.56) " -1234.6")
(("~10,1,,,F" -1234.56) " -1234.6")
(("~10,1,,,@F" -1234.56) " -1234.6")
(("~0F" 0.1) ".1")
(("~0F" 0.0) ".0")
(("~1F" 0.1) ".1")
(("~1F" 0.0) ".0")
(("~2F" 0.1) ".1")
(("~2F" 0.0) ".0")
(("~3F" 0.1) "0.1")
(("~3F" 0.0) "0.0")
(("~4F" 0.1) " 0.1")
(("~4F" 0.0) " 0.0")
(("~5F" 0.1) " 0.1")
(("~5F" 0.0) " 0.0")
(("~0F" -0.1) "-.1")
(("~0F" -0.0) "-.0")
(("~1F" -0.1) "-.1")
(("~1F" -0.0) "-.0")
(("~2F" -0.1) "-.1")
(("~2F" -0.0) "-.0")
(("~3F" -0.1) "-.1")
(("~3F" -0.0) "-.0")
(("~4F" -0.1) "-0.1")
(("~4F" -0.0) "-0.0")
(("~5F" -0.1) " -0.1")
(("~5F" -0.0) " -0.0")
(("~0,,,,F" 0.1) ".1")
(("~0,,,,F" 0.0) ".0")
(("~1,,,,F" 0.1) ".1")
(("~1,,,,F" 0.0) ".0")
(("~2,,,,F" 0.1) ".1")
(("~2,,,,F" 0.0) ".0")
(("~3,,,,F" 0.1) "0.1")
(("~3,,,,F" 0.0) "0.0")
(("~4,,,,F" 0.1) " 0.1")
(("~4,,,,F" 0.0) " 0.0")
(("~5,,,,F" 0.1) " 0.1")
(("~5,,,,F" 0.0) " 0.0")
(("~0,1,,,F" 0.1) ".1")
(("~0,1,,,F" 0.0) ".0")
(("~1,1,,,F" 0.1) ".1")
(("~1,1,,,F" 0.0) ".0")
(("~2,1,,,F" 0.1) ".1")
(("~2,1,,,F" 0.0) ".0")
(("~3,1,,,F" 0.1) "0.1")
(("~3,1,,,F" 0.0) "0.0")
(("~4,1,,,F" 0.1) " 0.1")
(("~4,1,,,F" 0.0) " 0.0")
(("~5,1,,,F" 0.1) " 0.1")
(("~5,1,,,F" 0.0) " 0.0")
(("~0,,,'?,F" 0.1) "")
(("~0,,,'?,F" 0.0) "")
(("~1,,,'?,F" 0.1) "?")
(("~1,,,'?,F" 0.0) "?")
(("~2,,,'?,F" 0.1) ".1")
(("~2,,,'?,F" 0.0) ".0")
(("~3,,,'?,F" 0.1) "0.1")
(("~3,,,'?,F" 0.0) "0.0")
(("~4,,,'?,F" 0.1) " 0.1")
(("~4,,,'?,F" 0.0) " 0.0")
(("~5,,,'?,F" 0.1) " 0.1")
(("~5,,,'?,F" 0.0) " 0.0")
(("~0,,,'?,'xF" 0.1) "")
(("~0,,,'?,'xF" 0.0) "")
(("~1,,,'?,'xF" 0.1) "?")
(("~1,,,'?,'xF" 0.0) "?")
(("~2,,,'?,'xF" 0.1) ".1")
(("~2,,,'?,'xF" 0.0) ".0")
(("~3,,,'?,'xF" 0.1) "0.1")
(("~3,,,'?,'xF" 0.0) "0.0")
(("~4,,,'?,'xF" 0.1) "x0.1")
(("~4,,,'?,'xF" 0.0) "x0.0")
(("~5,,,'?,'xF" 0.1) "xx0.1")
(("~5,,,'?,'xF" 0.0) "xx0.0")
(("~0,,,'?,'xF" -0.1) "")
(("~0,,,'?,'xF" -0.0) "")
(("~1,,,'?,'xF" -0.1) "?")
(("~1,,,'?,'xF" -0.0) "?")
(("~2,,,'?,'xF" -0.1) "??")
(("~2,,,'?,'xF" -0.0) "??")
(("~3,,,'?,'xF" -0.1) "-.1")
(("~3,,,'?,'xF" -0.0) "-.0")
(("~4,,,'?,'xF" -0.1) "-0.1")
(("~4,,,'?,'xF" -0.0) "-0.0")
(("~5,,,'?,'xF" -0.1) "x-0.1")
(("~5,,,'?,'xF" -0.0) "x-0.0")
(("~0,,,,F" -0.1) "-.1")
(("~0,,,,F" -0.0) "-.0")
(("~1,,,,F" -0.1) "-.1")
(("~1,,,,F" -0.0) "-.0")
(("~2,,,,F" -0.1) "-.1")
(("~2,,,,F" -0.0) "-.0")
(("~3,,,,F" -0.1) "-.1")
(("~3,,,,F" -0.0) "-.0")
(("~4,,,,F" -0.1) "-0.1")
(("~4,,,,F" -0.0) "-0.0")
(("~5,,,,F" -0.1) " -0.1")
(("~5,,,,F" -0.0) " -0.0")
(("~0,1,,,F" -0.1) "-.1")
(("~0,1,,,F" -0.0) "-.0")
(("~1,1,,,F" -0.1) "-.1")
(("~1,1,,,F" -0.0) "-.0")
(("~2,1,,,F" -0.1) "-.1")
(("~2,1,,,F" -0.0) "-.0")
(("~3,1,,,F" -0.1) "-.1")
(("~3,1,,,F" -0.0) "-.0")
(("~4,1,,,F" -0.1) "-0.1")
(("~4,1,,,F" -0.0) "-0.0")
(("~5,1,,,F" -0.1) " -0.1")
(("~5,1,,,F" -0.0) " -0.0")
(("~0,,,,F" +0.1) ".1")
(("~0,,,,F" +0.0) ".0")
(("~1,,,,F" +0.1) ".1")
(("~1,,,,F" +0.0) ".0")
(("~2,,,,F" +0.1) ".1")
(("~2,,,,F" +0.0) ".0")
(("~3,,,,F" +0.1) "0.1")
(("~3,,,,F" +0.0) "0.0")
(("~4,,,,F" +0.1) " 0.1")
(("~4,,,,F" +0.0) " 0.0")
(("~5,,,,F" +0.1) " 0.1")
(("~5,,,,F" +0.0) " 0.0")
(("~0,1,,,F" +0.1) ".1")
(("~0,1,,,F" +0.0) ".0")
(("~1,1,,,F" +0.1) ".1")
(("~1,1,,,F" +0.0) ".0")
(("~2,1,,,F" +0.1) ".1")
(("~2,1,,,F" +0.0) ".0")
(("~3,1,,,F" +0.1) "0.1")
(("~3,1,,,F" +0.0) "0.0")
(("~4,1,,,F" +0.1) " 0.1")
(("~4,1,,,F" +0.0) " 0.0")
(("~5,1,,,F" +0.1) " 0.1")
(("~5,1,,,F" +0.0) " 0.0")
(("~10F" 0.1) " 0.1")
(("~10F" 0.0) " 0.0")
(("~10F" -0.1) " -0.1")
(("~10F" -0.0) " -0.0")
(("~10,,,,'xF" 0.1) "xxxxxxx0.1")
(("~10,,,,'xF" 0.0) "xxxxxxx0.0")
(("~10,,,,'xF" -0.1) "xxxxxx-0.1")
(("~10,,,,'xF" -0.0) "xxxxxx-0.0")
(("~0@F" 0.1) "+.1")
(("~0@F" 0.0) "+.0")
(("~1@F" 0.1) "+.1")
(("~1@F" 0.0) "+.0")
(("~2@F" 0.1) "+.1")
(("~2@F" 0.0) "+.0")
(("~3@F" 0.1) "+.1")
(("~3@F" 0.0) "+.0")
(("~4@F" 0.1) "+0.1")
(("~4@F" 0.0) "+0.0")
(("~5@F" 0.1) " +0.1")
(("~5@F" 0.0) " +0.0")
(("~0@F" +0.1) "+.1")
(("~0@F" +0.0) "+.0")
(("~1@F" +0.1) "+.1")
(("~1@F" +0.0) "+.0")
(("~2@F" +0.1) "+.1")
(("~2@F" +0.0) "+.0")
(("~3@F" +0.1) "+.1")
(("~3@F" +0.0) "+.0")
(("~4@F" +0.1) "+0.1")
(("~4@F" +0.0) "+0.0")
(("~5@F" +0.1) " +0.1")
(("~5@F" +0.0) " +0.0")
(("~0@F" -0.1) "-.1")
(("~0@F" -0.0) "-.0")
(("~1@F" -0.1) "-.1")
(("~1@F" -0.0) "-.0")
(("~2@F" -0.1) "-.1")
(("~2@F" -0.0) "-.0")
(("~3@F" -0.1) "-.1")
(("~3@F" -0.0) "-.0")
(("~4@F" -0.1) "-0.1")
(("~4@F" -0.0) "-0.0")
(("~5@F" -0.1) " -0.1")
(("~5@F" -0.0) " -0.0")))
(defun test-format (test-case &optional verbose)
(let* ((formatted-string (apply #'format nil (first test-case)))
(successp (string= formatted-string (second test-case))))
(when verbose
(print (first test-case) nil)
(format t
"~%Should:~A~% Is:~A~:[ <<< Error!~;~]~%"
(second test-case)
formatted-string
successp))
(values successp (cons test-case (list formatted-string)))))
(defun run-all-tests (&key verbose)
"Runs all test cases and returns a true value iff all tests
succeeded. A list of the erroneous test cases together with the erroneous results
are returned as a second value. VERBOSE is interpreted by the individual test ."
(let ((count 0)
(errors '()))
(mapcar #'(lambda (test-case)
(multiple-value-bind (successp result)
(test-format test-case verbose)
(unless successp
(incf count)
(push result errors))))
*test-cases*)
(if (zerop count)
(format t "~2&All tests passed.")
(format t "~2&~D tests failed!" count))
(values (zerop count) (reverse errors))))
In the following I show the erroneous results produced by run-all-tests formated like this
(((<formt control string> <argument>) <expected result>) <erroneous result>)
and followed by comments explaining why I consider the results to be erroneous – with references to CLHS:
((("~0F" 0.1) ".1") "0.1")
((("~0F" 0.0) ".0") "0.0")
((("~1F" 0.1) ".1") "0.1")
((("~1F" 0.0) ".0") "0.0")
CLHS: Section 22.3.3.1 states:
Leading zeros are not permitted, except that a single zero digit is output before the decimal point if the printed value is less than one, and this single zero digit is not output at all if w=d+1.
and
If the parameter d is omitted, then there is no constraint on the number of digits to appear after the decimal point. A value is chosen for d in such a way that as many digits as possible may be printed subject to the width constraint imposed by the parameter w and the constraint that no trailing zero digits may appear in the fraction, except that if the fraction to be printed is zero, then a single zero digit should appear after the decimal point if permitted by the width constraint.
Firsts, d is omitted, but chosen to be 1, even for 0.0. For 0.1 this is necessary because the fraction is the only significant information, and for 0.0 it makes a lot of sense because otherwise 0.0 and 0.1 would be unintuitively printed differently. Second, the constraint w=d+1 is not exactly violated, but it makes a lot more sense to interpret it as w<=d+1. Otherwise you get unintuitive result. Or why should (format nil "~1F" 0.1) print differently than (format nil "~2F" 0.1)? In both cases w is too small to completely print the arg.
((("~2F" 0.1) ".1") "0.1")
Given the two citations from CLHS above, this is clearly wrong, since w=d+1. Ok, d is ommited, but it is chosen to be 1. And why should (format nil "~2F" 0.1) print diffently than (format nil "2,1F" 0.1)?
((("~2F" 0.0) ".0") "0.")
Why should this print differntly than (format nil "~2F" 0.1) and differently than (format nil "~2,1F" 0.0)? This makes no sense.
((("~0F" -0.1) "-.1") "-0.1")
((("~0F" -0.0) "-.0") "-0.0")
((("~1F" -0.1) "-.1") "-0.1")
((("~1F" -0.0) "-.0") "-0.0")
((("~2F" -0.1) "-.1") "-0.1")
See above.
((("~2F" -0.0) "-.0") "-0.")
See above.
((("~3F" -0.1) "-.1") "-0.1")
((("~3F" -0.0) "-.0") "-0.0")
w is still to small to allow the numbers to be printed completely. So, the zero digit should not be output before the decimal point here too.
((("~4F" -0.0) "-0.0") " -0.0")
((("~5F" -0.0) " -0.0") " -0.0")
CLHS: Section 22.3.3.1 states:
Exaclty w characters will be output.
Here, 5 and 6 characters are output, respectively, instead of 4 and 5.
((("~0,,,,F" 0.1) ".1") "0.1")
((("~0,,,,F" 0.0) ".0") "0.0")
((("~1,,,,F" 0.1) ".1") "0.1")
((("~1,,,,F" 0.0) ".0") "0.0")
((("~2,,,,F" 0.1) ".1") "0.1")
See above.
((("~2,,,,F" 0.0) ".0") "0."
See above.
((("~0,1,,,F" 0.1) ".1") "0.1")
((("~0,1,,,F" 0.0) ".0") "0.0")
See above.
((("~0,,,'?,F" 0.1) "") "0.1")
((("~0,,,'?,F" 0.0) "") "0.0")
CLHS: Section 22.3.3.1 states:
If it is impossible to print the value in the required format in a field of width w, then one of two actions is taken. If the parameter overflowchar is supplied, then w copies of that parameter are printed instead of the scaled value of arg.
There is an overflowchar supplied, so w=0 copies of it should be printed instead of the number.
((("~2,,,'?,F" 0.1) ".1") "0.1")
See above.
((("~2,,,'?,F" 0.0) ".0") "0.")
See above.
((("~0,,,'?,'xF" 0.1) "") "0.1")
((("~0,,,'?,'xF" 0.0) "") "0.0")
See above.
((("~2,,,'?,'xF" 0.1) ".1") "0.1")
((("~2,,,'?,'xF" 0.0) ".0") "0.")
See above.
((("~0,,,'?,'xF" -0.1) "") "-0.1")
((("~0,,,'?,'xF" -0.0) "") "-0.0")
See above.
((("~2,,,'?,'xF" -0.0) "??") "-0.")
CLHS: Section 22.3.3.1 states:
If it is impossible to print the value in the required format in a field of width w, then one of two actions is taken. If the parameter overflowchar is supplied, then w copies of that parameter are printed instead of the scaled value of arg.
There is an overflowchar supplied, so w=2 copies of it should be printed instead of the number.
((("~3,,,'?,'xF" -0.1) "-.1") "-0.1")
((("~3,,,'?,'xF" -0.0) "-.0") "-0.0")
See above.
((("~4,,,'?,'xF" -0.0) "-0.0") "x-0.0")
((("~5,,,'?,'xF" -0.0) "x-0.0") "xx-0.0")
CLHS: Section 22.3.3.1 states:
Exactly w characters will be output. First, leading copies of the character padchar (which defaults to a space) are printed, if necessary, to pad the field on the left.
Here, the arg takes 4 characters to print, so in both cases there is one padchar printed to much, resulting in an output of 5 and 6 characers instead of 4 and 5.
((("~0,,,,F" -0.1) "-.1") "-0.1")
((("~0,,,,F" -0.0) "-.0") "-0.0")
((("~1,,,,F" -0.1) "-.1") "-0.1")
((("~1,,,,F" -0.0) "-.0") "-0.0")
((("~2,,,,F" -0.1) "-.1") "-0.1")
((("~2,,,,F" -0.0) "-.0") "-0.")
((("~3,,,,F" -0.1) "-.1") "-0.1")
((("~3,,,,F" -0.0) "-.0") "-0.0")
See above.
((("~4,,,,F" -0.0) "-0.0") " -0.0")
((("~5,,,,F" -0.0) " -0.0") " -0.0")
See above.
((("~0,1,,,F" -0.1) "-.1") "-0.1")
((("~0,1,,,F" -0.0) "-.0") "-0.0")
((("~3,1,,,F" -0.1) "-.1") "-0.1")
((("~3,1,,,F" -0.0) "-.0") "-0.0")
See above.
((("~4,1,,,F" -0.0) "-0.0") " -0.0")
((("~5,1,,,F" -0.0) " -0.0") " -0.0")
See above.
((("~0,,,,F" 0.1) ".1") "0.1")
((("~0,,,,F" 0.0) ".0") "0.0")
((("~1,,,,F" 0.1) ".1") "0.1")
((("~1,,,,F" 0.0) ".0") "0.0")
((("~2,,,,F" 0.1) ".1") "0.1")
((("~2,,,,F" 0.0) ".0") "0.")
((("~0,1,,,F" 0.1) ".1") "0.1")
((("~0,1,,,F" 0.0) ".0") "0.0")
See above.
((("~10F" -0.0) " -0.0") " -0.0")
See above.
((("~10,,,,'xF" -0.0) "xxxxxx-0.0") "xxxxxxx-0.0")
See above.
((("~0@F" 0.1) "+.1") "0.1")
((("~0@F" 0.0) "+.0") "0.0")
CLHS: Section 22.3.3.1 states:
If the arg is negative, then a minus sign is printed; if the arg is not negative, then a plus sign is printed if and only if the @ modifier was supplied.
The arg is not negative and the @ modifier is supplied. So, the plus sign needs to be printed.
((("~1@F" 0.1) "+.1") "+0.1")
((("~1@F" 0.0) "+.0") "+0.0")
((("~2@F" 0.1) "+.1") "+0.1")
((("~2@F" 0.0) "+.0") "+0.0")
((("~3@F" 0.1) "+.1") "+0.1")
See above.
((("~3@F" 0.0) "+.0") "+0.")
See above.
((("~0@F" 0.1) "+.1") "0.1")
((("~0@F" 0.0) "+.0") "0.0")
See above.
((("~1@F" 0.1) "+.1") "+0.1")
((("~1@F" 0.0) "+.0") "+0.0")
((("~2@F" 0.1) "+.1") "+0.1")
((("~2@F" 0.0) "+.0") "+0.0")
((("~3@F" 0.1) "+.1") "+0.1")
See above.
((("~3@F" 0.0) "+.0") "+0.")
See above.
((("~0@F" -0.1) "-.1") "-0.1")
((("~0@F" -0.0) "-.0") "-0.0")
((("~1@F" -0.1) "-.1") "-0.1")
((("~1@F" -0.0) "-.0") "-0.0")
((("~2@F" -0.1) "-.1") "-0.1")
((("~2@F" -0.0) "-.0") "-0.0")
((("~3@F" -0.1) "-.1") "-0.1")
See above.
((("~3@F" -0.0) "-.0") "-0.")
See above.
And finally:
- SBCL 2.2.1 on Windows produces the expected results for all test cases presented above (.i.e. no errors).
- LispWorks 7.2.1 on Windows runs into an error in an internal function and produces erroneous results for some of the test cases, but there are two patches available making all test cases print as expected.
- LispWorks 8: I assume this works as expected, since LispWorks 7.2.1 got two patches fixing it. But I haven't tested it. Maybe someone else can check this.
LispWorks 8: I assume this works as expected, since LispWorks 7.2.1 got two patches fixing it. But I haven't tested it. Maybe someone else can check this.
I can confirm all tests pass with LispWorks 8 on Windows 10 LTSC,
CL-USER 1 > (run-all-tests)
All tests passed.
T
NIL
I just ran the above tests with Allegro CL 10.1 (64-bit Linux) and get:
cl-user> (run-all-tests)
49 tests failed!
nil
(((("~0F" 0.0) ".0") "0.") ((("~1F" 0.0) ".0") "0.")
((("~2F" 0.0) ".0") "0.") ((("~0F" 0.0) "-.0") "0.")
((("~1F" 0.0) "-.0") "0.") ((("~2F" 0.0) "-.0") "0.")
((("~3F" 0.0) "-.0") "0.0") ((("~4F" 0.0) "-0.0") " 0.0")
((("~5F" 0.0) " -0.0") " 0.0") ((("~0,,,,F" 0.0) ".0") "0.")
((("~1,,,,F" 0.0) ".0") "0.") ((("~2,,,,F" 0.0) ".0") "0.")
((("~2,,,'?,F" 0.0) ".0") "0.") ((("~2,,,'?,'xF" 0.0) ".0") "0.")
((("~2,,,'?,'xF" 0.0) "??") "0.") ((("~3,,,'?,'xF" 0.0) "-.0") "0.0")
((("~4,,,'?,'xF" 0.0) "-0.0") "x0.0")
((("~5,,,'?,'xF" 0.0) "x-0.0") "xx0.0") ((("~0,,,,F" 0.0) "-.0") "0.")
((("~1,,,,F" 0.0) "-.0") "0.") ((("~2,,,,F" 0.0) "-.0") "0.")
((("~3,,,,F" 0.0) "-.0") "0.0") ((("~4,,,,F" 0.0) "-0.0") " 0.0")
((("~5,,,,F" 0.0) " -0.0") " 0.0") ((("~0,1,,,F" 0.0) "-.0") ".0")
((("~1,1,,,F" 0.0) "-.0") ".0") ((("~2,1,,,F" 0.0) "-.0") ".0")
((("~3,1,,,F" 0.0) "-.0") "0.0") ((("~4,1,,,F" 0.0) "-0.0") " 0.0")
((("~5,1,,,F" 0.0) " -0.0") " 0.0") ((("~0,,,,F" 0.0) ".0") "0.")
((("~1,,,,F" 0.0) ".0") "0.") ((("~2,,,,F" 0.0) ".0") "0.")
((("~10F" 0.0) " -0.0") " 0.0")
((("~10,,,,'xF" 0.0) "xxxxxx-0.0") "xxxxxxx0.0")
((("~0@F" 0.0) "+.0") "+0.") ((("~1@F" 0.0) "+.0") "+0.")
((("~2@F" 0.0) "+.0") "+0.") ((("~3@F" 0.0) "+.0") "+0.")
((("~0@F" 0.0) "+.0") "+0.") ((("~1@F" 0.0) "+.0") "+0.")
((("~2@F" 0.0) "+.0") "+0.") ((("~3@F" 0.0) "+.0") "+0.")
((("~0@F" 0.0) "-.0") "+0.") ((("~1@F" 0.0) "-.0") "+0.")
((("~2@F" 0.0) "-.0") "+0.") ((("~3@F" 0.0) "-.0") "+0.")
((("~4@F" 0.0) "-0.0") "+0.0") ((("~5@F" 0.0) " -0.0") " +0.0"))
cl-user> (lisp-implementation-version)
"10.1 [64-bit Linux (x86-64)] (Sep 8, 2022 13:20)"
("lisp_build 2865")
cl-user>
I think several of these are because Allegro CL is treating -0.0 as "not negative" and so leaving out the - would be compliant. But there are also some "0." instead of ".0" which may not be in compliance.