BitTickler
BitTickler

Reputation: 11875

Looking for format string which allows custom formatting for list of pairs

So I have lists, looking like this:

((24 . 23) (9 . 6) ... )

and want to custom format the output to something looking like this:

"24/23 9/6 ..."

I tried:

(defun show-pair (ostream pair col-used atsign-used) 
  (declare (ignore col-used atsign-used)) 
  (format ostream "~d/~d" (first pair) (second pair)))

(let ((x '( 1 . 2))) (format nil "~{~/show-pair/~^~}" (list x)))

as a simple warming up exercise to show a list with only 1 pair. But when trying this in the emacs slime repl, I get the error

The value 2 is not of type LIST [Condition of type TYPE-ERROR]

Which, of course is confusing as ~/show-pair/ was expected to handle one entry in the list, which is is the pair, passing the pair to show-pair. But it appears, something else is actually happening.

Upvotes: 1

Views: 161

Answers (4)

Gwang-Jin Kim
Gwang-Jin Kim

Reputation: 9865

If you want to do it with format - the problem is to access first and second element of the alist using format directives. I didn't found how I could access them inside a format directive.

However, in such regularly formed structures like an alist, one could flatten the list first and then let format-looping directive consume two elements per looping - then one consumes the pair.

Since the famous :alexandria library doesn't count as dependency in Common Lisp world, one could directly use alexandria:flatten:

(defparameter *al* '((24 . 23) (9 . 6)))
(ql:quickload :alexandria) ;; import alexandria library

(format nil "~{~a/~a~^ ~}" (alexandria:flatten *al*))
;; => "24/23 9/6"

  • nil return as string
  • ~{ ~} loop over the list
  • ~a/~a the fraction
  • ~^ empty space between the elements but not after last element

flatten by the way without :alexandria-"dependency" would be in this case:

(defun flatten (l)
  (cond ((null l) nil)
        ((atom l) (list l))
        (t (append (flatten (car l)) (flatten (cdr l))))))

Upvotes: 2

chrnybo
chrnybo

Reputation: 145

Using hints from Redefinition of the print-object method for conses..., you could end up with something like this:

CL-USER> (let ((std-function (pprint-dispatch 'cons)))
           (unwind-protect 
                (progn (set-pprint-dispatch 
                        'cons 
                        (lambda (s o) (format s "~d/~d" (car o) (cdr o))))
                       (format t "~{~a~%~}" '((23 . 24) (5 . 9))))
             (set-pprint-dispatch 'cons std-function))
           (format t "~{~a~%~}" '((23 . 24) (5 . 9))))
23/24
5/9
(23 . 24)
(5 . 9)
NIL
CL-USER> 

Hiding the bookkeeping;

(defmacro with-fractional-conses (&body body)
  (let ((std-function (gensym "std-function")))
    `(let ((,std-function (pprint-dispatch 'cons)))
       (unwind-protect 
            (progn (set-pprint-dispatch 
                    'cons 
                    (lambda (s o) (format s "~d/~d" 
                                          (car o) 
                                          (cdr o))))
                   ,@body)
         (set-pprint-dispatch 'cons ,std-function)))))


CL-USER> (with-fractional-conses 
           (format t "~{~a~%~}" 
                   '((23 . 24) (5 . 9))))
23/24
5/9
NIL
CL-USER> 

Upvotes: 1

user5920214
user5920214

Reputation:

It is fairly seldom a good idea to use ~/.../ in format in my experience. It's probably much better to simply turn the list you have into the list you need and then process that directly. So, for instance:

> (format t "~&~:{~D/~D~:^, ~}~%"
        (mapcar (lambda (p)
                  (list (car p) (cdr p)))
                '((1 . 2) (3 . 4))))
1/2, 3/4
nil

Or if you want to use loop:

 > (format t "~&~:{~D/~D~:^, ~}.~%"
        (loop for (n . d) in '((1 . 2) (3 . 4))
              collect (list n d)))
1/2, 3/4.
nil

The cost (and storage) associated with converting the list is likely to be absolutely tiny compared with the I/O cost of printing it.

Upvotes: 1

BitTickler
BitTickler

Reputation: 11875

While waiting for feedback, I found out, where the problem is coming from:

So far, I considered (second x) to behave exactly like (cdr x) but for tagged values, this assumption is wrong. If I change in show-pairs above (in the question) accordingly, it all works.

So, it is not a formatting problem at all, nor any surprises with how ~/foo~/ works.

(defun show-pair (ostream pair col-used atsign-used) 
  (declare (ignore col-used atsign-used)) 
  (format ostream "~d/~d" (first pair) (cdr pair))) ;; second -> cdr fixes the problem

Upvotes: 1

Related Questions