2
\$\begingroup\$

As an exercise, I've implemented a simple reader macro in Common Lisp (using SBCL). It converts Octal (unsigned integers only) into numbers.

Usage:

* #z1234

668

The code:

(defun oct-string-to-number 
  (string)
  "Converts an octal string to a number.  Only digits from 0 - 7 are accepted; sign or decimal point symbols will cause oct-to-number to fail"
  (setq place 1)
  (setq result 0)
  (setq digits '(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7))
  (loop for char across (reverse string)
    do 
    (setq pos (position char digits))
    (setq result (+ result (* pos place)))
    (setq place (* 8 place)))
  result)

(defun slurp-octal-digits 
  (stream)
  (setq string (make-array 0 :element-type 'character :fill-pointer 0 :adjustable t))
  "Slurps all digits from 0 - 7 from a stream into a string, stopping at EOF, no data, or a non-digit character." 
  (setq digits '(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7))
  (with-output-to-string (out)
             (loop do
                   (setq char (read-char stream))
                   (setq isnum nil)
                   (if char
                   (progn
                     (setq isnum (find char digits))
                     (if isnum
                     (vector-push-extend char string)
                       (unread-char char stream))))
                   while (not (eq nil isnum))))
  string)

(defun octal-string-transformer 
  (stream subchar args)
  "Slurps an octal number from stream, and converts it to a number.  Number must be an unsigned integer."
  (setq oct-string (slurp-octal-digits stream))
  (oct-string-to-number oct-string))

;; Sets #z to call octal-string-transformer, so e.g. #z1234 will evaluate to 668.  Use #z as SBCL has #o already :-)
(set-dispatch-macro-character
 #\# #\z
 #'octal-string-transformer)

I'm quite new to Common Lisp, so I'd greatly appreciate feedback on everything: formatting, style, idiom, correctness :-)

Edited to add: Actually, the formatting in the pasted code snippet is rendered oddly; indentation that is present when I edit vanishes when I submit. So maybe be a bit gentle with feedback about the formatting ;-)

\$\endgroup\$
4
\$\begingroup\$

First of all a note about variables: You're using setq on previously undefined and undeclared variables. This is actually undefined behavior and in most implementations will create global dynamic/special variables. To create local variables (which you presumably intended) use let.

(defun oct-string-to-number 
  (string)
  "Converts an octal string to a number.  Only digits from 0 - 7 are accepted; sign or decimal point symbols will cause oct-to-number to fail"

This whole function can be implemented as (parse-integer string :radix 8). However for the sake of learning let's go through the function anyway:

(setq digits '(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7))
...
(setq pos (position char digits))

Iterating through the list of digits to convert a digit to an integer, is neither the most efficient nor the most succinct to do it. You can use the digit-char-p function which returns the digit's int value, if the given char is a digit and nil otherwise. (The same applies in slurp-octal-digits where you do the same thing).

(loop for char across (reverse string)
    do 
    (setq pos (position char digits))
    (setq result (+ result (* pos place)))
    (setq place (* 8 place)))

Instead of reversing the string and keeping track of a place variable, you can just iterate over the string from the beginning and multiply the result by 8 each step:

(loop for char across string
    do 
    (setq result (+ (* 8 result) (digit-char-p char))))

That being said, I'd rather use reduce then loop here (that is, if I weren't using parse-integer) - especially as higher order functions are one of the most useful things in lisp that a newcomer should get acquainted to. Using reduce the function would be written like this:

(defun oct-string-to-number (string)
  "Converts an octal string to a number.  Only digits from 0 - 7 are accepted; sign, non-octal digit or decimal point will signal error."
  (reduce
    (lambda (sum digit)
      (+ (* 8 sum) (digit-char-p digit 8)))
    string :initial-value 0))
\$\endgroup\$
  • 1
    \$\begingroup\$ Thank you for being the anti-setq, pro-built-in police in my absence. \$\endgroup\$ – Inaimathi Apr 28 '11 at 3:23
  • \$\begingroup\$ @Inaimathi: it's good to know about the built-in functionality, but I did this as a learning exercise; otherwise I'd have just used #o in the first place :-) \$\endgroup\$ – Duncan Bayne Apr 28 '11 at 7:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.