Writing and printing hex

With our inner functions finished, let’s back out a level and work on:

  • #'write-hex
  • #'print-hex

There is some trickiness to how these need to work. First, let us look at the built-in function, write-string, as an example.

Write-string takes in a string and a stream, walks across the string, and sends each of the characters to the stream. The stream is *standard-output* by default. It also returns the input as well as writing it.

Our write-hex will be similar, with one difference. We aren’t being handed a string as input. The number we are given will be an integer, which means it is stored in binary. So we need to grab 4 bits at a time.

Luckily there is a built-in function for just that, LDB. I think of it as extracting a range of bits from a binary number. LDB is short for load-byte and is a op-code on some of the PDP machines.

I think of LDB as working like this:

0
1
(ldb load-this-chunk
     within-this-number)

This is how to extract the two hex numbers from within a byte:

0
1
2
3
4
5
6
7
8
(let ((num #b10101100))
  (values
    (ldb (byte 4 4)
         num)
    (ldb (byte 4 0)
         num)))

=> 10 ; #b1010
=> 12 ; #b1100

So what is going on here?

First, I am using the #b form which is shorthand for binary. The CL reader takes in the number as binary and uses it from that point as an integer.

Within LDB there is a bytespec, which takes a size and position. In both of these cases we are taking 4 bits, one starting at index 0 and the other at 4, where the little endian bit is index 0.

0
1
2
#b10101100
     ^   ^
     4   0  ; indexes/positions

If you run off to the big endian side, for instance using index 9 or higher here with this 8-bit width, LDB will return the same number as if the size was smaller, eg #b00000101 is the same as #b101.

This brings up an important point about the writer and the printer. We will want to be able to print #b00000101 sometimes, so we should have a frame-width parameter, if for no other reason than to be able to align things vertically so it is more readable. The frame-width is for the output side of the function, not the input, so with #b10101100, a frame-width of 4 here would print “00AC”.

So write-hex should look something like this:

0
1
2
3
4
5
(loop for index from (* (1- frame-width) 4) downto 0 by 4
      do (write-char
           (hex-char
             (ldb (byte 4 index)
                  number))
           stream))

Basically we walk along the number, getting nibbles of bits, handing them to hex-char which gives us the correct character, then use write-char to send it to the stream.

We have to loop down to zero because the printer starts from the big-endian side of the number. We loop down by 4 because we are printing hex. This means the number we feed into LDB for the position is our index, which is nice as there is further no calculation within the loop.

Handling the frame width is a bit tricky.

What it needs to accomplish is to make sure the frame-width is wider than the number of bits we are handing in, in this case modulo 16 - the base of the printed character. So if the input is #b10101100, 8 bits, we need to print 2 characters. If it was 9 bits, we’d need to print 3.

We can use #’log for that, with a couple caveats. A quick check of how it works in the REPL illustrates three problems:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(log #b1111 16)

0.97672266

(log #b10000 16)

=> 1.0

(log #b0001 16)

=> 0.0

(log #b0000 16)

=> Evaluation aborted on #<DIVISION-BY-ZERO>

The first example would work if we rounded up. But the second example won’t round at all so it is wrong. The third gives the wrong answer too - we want at least one hex character as the output to hold the 1. The last example just outright errors.

We can fix the last one short circuiting the algorithm and hard-coding outputting 1 as the frame-width when the input is 0.

The other two can be handled by adding 1 to the number. #b1111 will still output a 1.0 so it doesn’t break our currently correct test. #b10000 will now output a 2.0 due to the addition of 1. That also fixes #b0001.

Since handling frame width is a thing I have used in other placed and may need again, let’s make it a function:

0
1
2
3
(defun min-frame-width (number base)
  (if (zerop number)
      1
      (ceiling (log (1+ number) base))))

A quick test of this in the REPL shows it works:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(min-frame-width #b1111 16)

=> 1

(min-frame-width #b10000 16)

=> 2

(min-frame-width #b0001 16)

=> 1

(min-frame-width #b0000 16)

=> 1

Putting that into write-hex, we have:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(defun write-hex (number &key stream frame-width case)
  "Write the number, in hex, to the stream."
  (let ((frame-width (if frame-width frame-width
                         (min-frame-width number 16))))
    (loop for index from (* (1- frame-width) 4) downto 0 by 4
          do (write-char
               (hex-char
                 (ldb (byte 4 index)
                      number)
                 :case case)
               stream)))
  number)

Which works as expected:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(write-hex #b10101100)

=> ac
=> 172

(write-hex #b10101100 :frame-width 1)

=> c
=> 172

(write-hex #b10101100 :frame-width 3)

=> 0ac
=> 172

Next, we should study what else the built in printer does. There are two general cases the built in printer handles in terms of where it prints to. When there is a stream argument, it prints to it and returns the input. Otherwise it prints to a string and returns that.

Returning the input is handy, it means you can insert a print function into the middle of working code and expect it to continue working. Write-hex already returns the number, so if that is the last call in the function we won’t need an explicit return.

So these three calls should work as written here:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(defvar *test-num* #b10101100)

(print-hex *test-num*) ; Should print in hex and return the number

=> ac
=> 172

(print-hex *test-num* nil) ; Should return a string but won't print it

=> "ac"

(* (print-hex *test-num*) 2) ; Should print the original number but return double

=> ac
=> 344

I think the printer should look something like this:

0
1
2
(if stream
    (handle-stream)
    (handle-string))

So we’ve got two sections to build.

Strings are simple, we can use the built-in handling:

0
1
2
3
(with-output-to-string (string)
  (write-hex #b10101100 :stream string))

=> "ac"``` ; TODO add option for up/down case in defuns

With streams however, the built in handling doesn’t do everything we want:

0
1
2
(progn (write-hex #b110 *standard-output*)
       (write-hex #b001 *standard-output*))
=> 61

Looks like we need to add a newline between the calls. But that leads to a slight issue of adding a new-line to the end of the printing which we don’t really want either. The fresh-line function should fix that as it only prints at the start if there is no newline on the stream.

Putting that all together, with the standard output as a default:

0
1
2
3
4
5
6
7
8
(defun print-hex (number &key (stream *standard-output*) frame-width (case :lower))
  "Print the number in hexidecimal. Print to the stream if given, otherwise
  return a string."
  (if stream
      (progn
        (fresh-line stream)
        (write-hex number :stream stream :frame-width frame-width :case case))
      (with-output-to-string (string)
        (write-hex number :stream string :frame-width frame-width :case case))))

Our three tests now show as expected in the REPL.