Re: Peculiar number formatting?

"Pascal Bourguignon (as informatimago at gmail dot com)" <[email protected]> Tue, 19 May 2026 14:16:54 +0200
Newsgroups gmane.lisp.lispworks.general
Message-ID <[email protected]>



> On 19 May 2026, at 12:07, David McClain (as dbm at refined-audiometrics dot com) <[email protected]> wrote:
> 
> While developing some simulation code for 24-bit DSP arithmetic, I ran into what appeared at first to be incorrect number formatting.
> 
> (format t “~6,’0x”  -1)  =>  0000-1
> 
> instead of -000001, or even -00001 if you insist on the width control of 6.
> 
> Indeed, even this is peculiar to me:
> 
> (format t “~6,’0@d” 1)  =>  0000+1
> 
> I have been around for more than 55 years in programming, and I have never before seen numbers displayed like this. It is almost as though we are working in ancient COBOL banking times where the width control is completely divorced from the numeric display, and you want to be sure to fill the field excess with a printed character to prevent check cashing fraud. “*****1.20"
> 
> I did cross check with SBCL and found the same behavior. And there were no examples of negative values being printed in fixed radix format in the HyperSpec.
> 
> I wonder what the Lisp world was thinking when they decided to standardize on this kind of format output?? COBOL era?
> 


Yes they were thinking about the formatting usages of their time.
But they were also forward thinking enough, to provide us with ~/
and the ability to use our own formatter macros.

You can write easily your own ~/hex/ formatter:

#||

Write a Common Lisp function named cl-user::hex
to be used in format ~/ taking:
- an optional width parameter
- an optional colon flag,
- an optional at flag,
and displaying the absolute value of the argument as an hexadecimal number.
If the width is specified, then the digits are left-padded with 0s.

| flags   | positive argument          | negative argument    |
|---------+----------------------------+----------------------|
| no flag | nothing                    | digits prefixed by - |
| :       | digits prefixed by a space | digits prefixed by - |
| : and @ | nothing                    | nothing              |
| @       | digits prefixed by +       | digits prefixed by - |

||#

(defun cl-user::hex (stream arg colonp atsignp &optional width)
  "Format directive ~[width][flags]/hex/ for hexadecimal output.
   WIDTH:    optional minimum number of hex digits, left-padded with 0s
   No flags: positive → nothing,  negative → '-'
   Colon:    positive → ' ',      negative → '-'
   At:       positive → '+',      negative → '-'
   Colon+At: positive → nothing,  negative → nothing"
  (let* ((negative (minusp arg))
         (prefix (cond ((and colonp atsignp) nil)
                       (colonp               (if negative #\- #\Space))
                       (atsignp              (if negative #\- #\+))
                       (t                    (if negative #\- nil))))
         (hex-string (if width
                         (format nil "~v,'0X" width (abs arg))
                         (format nil "~X" (abs arg)))))
    (when prefix (write-char prefix stream))
    (write-string hex-string stream)))

(progn
(format t "~/hex/~%"     255)   ; → FF
(format t "~/hex/~%"    -255)   ; → -FF

(format t "~8/hex/~%"    255)   ; → 000000FF
(format t "~8/hex/~%"   -255)   ; → -000000FF

(format t "~8:/hex/~%"   255)   ; →  000000FF   (leading space)
(format t "~8:/hex/~%"  -255)   ; → -000000FF

(format t "~8@/hex/~%"   255)   ; → +000000FF
(format t "~8@/hex/~%"  -255)   ; → -000000FF

(format t "~8:@/hex/~%"  255)   ; → 000000FF    (no sign at all)
(format t "~8:@/hex/~%"  -255)  ; → 000000FF    (no sign at all)
)

FF
-FF
000000FF
-000000FF
 000000FF
-000000FF
+000000FF
-000000FF
000000FF
000000FF
nil

;; or better, this let you see the alignment problems with signs when
;; you don't put the sign in the "width" argument:

(loop for s in '("~3/hex/" "~3:/hex/" "~3@/hex/" "~3:@/hex/")
      do (loop for n in '(-42 0 42)
               do (format t ">~@?<~8T~:*~4D~%" s n)
               finally (terpri)))
>-02A<   -42
>000<      0
>02A<     42

>-02A<   -42
> 000<     0
> 02A<    42

>-02A<   -42
>+000<     0
>+02A<    42

>02A<    -42
>000<      0
>02A<     42

nil

But the truth of hexadecimal, octal and binary formatting, 
is that you don’t use them for negative numbers. When you 
have negative integers, which is Common Lisp have infinite 
number of digits (1s) before the meaningful digits, what you
 do to format them is that you truncate them to the word size:

(loop for n in  '(-42 -1 0 1 42)
      do (format t "~4/hex/~%" (logand #xffff n)))
FFD6
FFFF
0000
0001
002A
nil

This prints the 2-complement representation of negative integers.

-- 
__Pascal Bourguignon__
[email protected]