Re: Wow, Lisp Reader !!

"Pascal Bourguignon (as informatimago at gmail dot com)" <[email protected]> Sun, 24 May 2026 11:29:02 +0200
Newsgroups gmane.lisp.lispworks.general
Message-ID <[email protected]>
The magic of lisp reader and printer, is that indeed, you can determine the type of the printed object usually by reading only the first or the first two characters.

[0-9] —> integer, ratio o real (it’s one exception)
: -> keyword symbol
# -> dispatching reader macro (note this is the standard dispatching reader macro character, but you could have your own introducing characters for another set of dispatching reader macros.
‘ -> a quote
“ -> a string
( -> a list

etc.


So indeed, using a prefix for your coordinates is a good way to integrate with lisp.

But lisp let’s you design your own language and if the syntax has to be different, then it’ll be different.

So if you want 12:42:33.24
you could do it by putting reader macros on each digits (and perhaps the signs too if you want signed angles)

But now you’re put in charge of reading normal integers, ratios or real yourself in addition to your angles, since they all start with a digit.
This is not to hard to do: just collect the characters in a buffer, and if the syntax is not yours, you can put this buffer in front of the stream and call READ again.


Note that we cannot just use unread-char because this has only 1 char buffer. 



;;;; Angle reader macro
;;;; Reads angles in format DD:MM:SS[.ss] and returns a double-float
;;;; in decimal degrees. Falls back to normal number reading via
;;;; concatenated-stream when the format doesn't match.

(defun digit-char-p* (c)
  "Like digit-char-p but returns NIL for non-characters too."
  (and (characterp c) (digit-char-p c)))

(defun angle-terminator-p (c)
  "Characters that terminate a token in standard Lisp syntax."
  (or (null c)
      (member c '(#\Space #\Tab #\Newline #\Return #\Page
                  #\( #\) #\' #\` #\, #\; #\" #\|))))
;; or use something like COM.INFORMATIMAGO.TOOLS.READER-MACRO
;; to compute the actual liste of terminating macro and dispatching macro characters.
;; (cache the result, it can be long with unicode).

(defun parse-angle-buffer (buffer)
  "Try to parse BUFFER as DD:MM:SS[.ss].
   Returns the angle as a double-float, or NIL if it doesn't match."
  (let ((len (length buffer))
        (i 0)
        (parts '())
        (sign 1))
    ;; optional leading sign
    (when (and (< i len) (or (char= (char buffer i) #\+)
                             (char= (char buffer i) #\-)))
      (when (char= (char buffer i) #\-)
        (setf sign -1))
      (incf i))
    ;; degrees: one or more digits
    (let ((start i))
      (loop while (and (< i len) (digit-char-p (char buffer i))) do (incf i))
      (when (= start i) (return-from parse-angle-buffer nil))
      (push (parse-integer buffer :start start :end i) parts))
    ;; first colon
    (unless (and (< i len) (char= (char buffer i) #\:))
      (return-from parse-angle-buffer nil))
    (incf i)
    ;; minutes
    (let ((start i))
      (loop while (and (< i len) (digit-char-p (char buffer i))) do (incf i))
      (when (= start i) (return-from parse-angle-buffer nil))
      (push (parse-integer buffer :start start :end i) parts))
    ;; second colon
    (unless (and (< i len) (char= (char buffer i) #\:))
      (return-from parse-angle-buffer nil))
    (incf i)
    ;; seconds: integer part
    (let ((start i))
      (loop while (and (< i len) (digit-char-p (char buffer i))) do (incf i))
      (when (= start i) (return-from parse-angle-buffer nil))
      ;; optional fractional part
      (when (and (< i len) (char= (char buffer i) #\.))
        (incf i)
        (loop while (and (< i len) (digit-char-p (char buffer i))) do (incf i)))
      (push (let ((*read-default-float-format* 'double-float))
              (read-from-string buffer t nil :start start :end i))
            parts))
    ;; must have consumed the whole buffer
    (unless (= i len)
      (return-from parse-angle-buffer nil))
    (destructuring-bind (seconds minutes degrees) parts
      (* sign
         (+ (coerce degrees 'double-float)
            (/ minutes 60.0d0)
            (/ seconds 3600.0d0))))))

(defun read-angle-or-fallback (stream char)
  "Reader macro function. CHAR is the first digit (or sign) that triggered us.
   Buffer characters until a terminator; if it parses as an angle, return the
   angle; otherwise, push the buffer back via a concatenated-stream and call
   READ with the angle reader macro disabled."
  (let ((buffer (make-array 16 :element-type 'character
                               :adjustable t :fill-pointer 0)))
    (vector-push-extend char buffer)
    (loop for c = (peek-char nil stream nil nil t)
          while (and c (not (angle-terminator-p c)))
          do (vector-push-extend (read-char stream t nil t) buffer))
    (let ((angle (parse-angle-buffer buffer)))
      (if angle
          angle
          ;; Fallback: put the buffer back in front of STREAM and re-READ
          ;; with standard syntax (no angle reader macro).
          (let* ((pushback (make-string-input-stream (coerce buffer 'string)))
                 (combined (make-concatenated-stream pushback stream))
                 (*readtable* (copy-readtable nil)))
            (read combined t nil t))))))

(defun install-angle-reader (&optional (readtable *readtable*))
  "Install the angle reader macro on digits and signs."
  (dolist (c '(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\+ #\-)) ; <- the magic is here ;-)
    (set-macro-character c #'read-angle-or-fallback t readtable))
  readtable)

;;; -----------------------------------------------------------------
;;; Examples
;;; -----------------------------------------------------------------

(defun run-examples ()
  (let ((*readtable* (install-angle-reader (copy-readtable nil))))
    (format t "~&=== Angle reader examples ===~%")
    (dolist (input '("12:30:00"          ; 12.5
                     "12:34:56.78"       ; full form with fraction
                     "0:30:00"           ; half a degree
                     "-12:30:00"         ; negative
                     "90:00:00"
                     "180:00:00"
                     "23:59:59.999"
                     ;; Fallbacks — not angles, should read as numbers:
                     "42"
                     "-3.14"
                     "1.5e2"
                     "100"
                     "+7"))
      (let ((value (read-from-string input)))
        (format t "  ~22s => ~s   (type: ~a)~%"
                input value (type-of value))))
    ;; Also show that it works mid-expression
    (format t "~%=== Inside a list ===~%")
    (let ((form (read-from-string "(12:30:00 -45:15:00 100 3.14 0:0:1)")))
      (format t "  ~s~%" form))))

(run-examples)

=== Angle reader examples ===
  "12:30:00"             => 12.5D0   (type: double-float)
  "12:34:56.78"          => 12.58243888888889D0   (type: double-float)
  "0:30:00"              => 0.5D0   (type: double-float)
  "-12:30:00"            => -12.5D0   (type: double-float)
  "90:00:00"             => 90.0D0   (type: double-float)
  "180:00:00"            => 180.0D0   (type: double-float)
  "23:59:59.999"         => 23.999999722222224D0   (type: double-float)
  "42"                   => 42   (type: (integer 0 1152921504606846975))
  "-3.14"                => -3.14   (type: single-float)
  "1.5e2"                => 150.0   (type: single-float)
  "100"                  => 100   (type: (integer 0 1152921504606846975))
  "+7"                   => 7   (type: (integer 0 1152921504606846975))

=== Inside a list ===
  (12.5D0 -45.25D0 100 3.14 2.777777777777778D-4)


-- 
__Pascal Bourguignon__
[email protected]




> On 24 May 2026, at 10:56, Tim Bradshaw (as tfb at cley dot com) <[email protected]> wrote:
> 
> On 23 May 2026, at 23:18, David McClain (as dbm at refined-audiometrics dot com) <[email protected]> wrote:
>> 
>> Perhaps I just need to polish it a bit harder. For the embedded colon chars in my angle input, I currently have to write #N|12:42:33.25| or #N”12:42:33.25” so that I have either a symbol or a string to take apart. But if I back up a bit and do the read-chars myself, then I ought to be able to get around this.
> 
> The problem with this is that you need to know when to stop.  Presumably '#N12:42:123(' is legal (leaving an open paren to be read, so the next form to be read being a list) but '#N12:42:123x' should not be.
> 
> You really need access to the syntax types of characters and you don't have that.
> 
> I'm guessing that the reason it's all so opaque is that the reader only has to behave as if the things the spec describes are true: it might internally be doing something quite different, especially for interactive use.  Exposing more of it would tend to force it to actually behave the way it's meant to, which would constrain implementations a lot more.
> 
> But it's annoying.  What I do is the delimiter thing: write something that will read characters up to an unescaped delimiter and hand them to whatever it is.  So my syntax would be '#N[...]', or '#N/.../' read-delimited-list is not how you need to do this because the characters you read need to be uninterpreted.
> 
> --tim