What's the canonical way to join strings in a list?
Asked Answered
B

6

33

I want to convert ("USERID=XYZ" "USERPWD=123") to "USERID=XYZ&USERPWD=123". I tried

(apply #'concatenate 'string '("USERID=XYZ" "USERPWD=123"))

which will return ""USERID=XYZUSERPWD=123".

But i do not know how to insert '&'? The following function works but seems a bit complicated.

(defun join (list &optional (delim "&"))
    (with-output-to-string (s)
        (when list
            (format s "~A" (first list))
            (dolist (element (rest list))
               (format s "~A~A" delim element)))))
Bonaparte answered 12/1, 2012 at 6:12 Comment(0)
S
58

Use FORMAT.

~{ and ~} denote iteration, ~A denotes aesthetic printing, and ~^ (aka Tilde Circumflex in the docs) denotes printing the , only when something follows it.

* (format nil "~{~A~^, ~}" '( 1 2 3 4 ))

"1, 2, 3, 4"
* 
Smalltime answered 12/1, 2012 at 6:30 Comment(3)
This unfortunately does not work in Emacs "elisp". They have a different format function. Is there any comparable way to do this in Emacs?Ajmer
@russ: Probably. Not being an elisp wizard, I went back to basic Lisp... (defun join-to-str (thing &rest strings) (labels ((recurser (strings) (cond ((> (length strings) 1) (append (list (car strings) thing) (recurser (cdr strings)))) (t (cons (car strings) nil))))) (apply 'concat (recurser strings))))Smalltime
@russ better late than never, if you're concatenating strings: (mapconcat 'identity '("one" "two") ", ") but if they aren't strings, you want to change 'identity to (lambda (x) (format "%s" x))Torrent
P
9

This solution allows us to use FORMAT to produce a string and to have a variable delimiter. The aim is not to cons a new format string for each call of this function. A good Common Lisp compiler also may want to compile a given fixed format string - which is defeated when the format string is constructed at runtime. See the macro formatter.

(defun %d (stream &rest args)
  "internal function, writing the dynamic value of the variable
DELIM to the output STREAM. To be called from inside JOIN."
  (declare (ignore args)
           (special delim))
  (princ delim stream))

(defun join (list delim)
  "creates a string, with the elements of list printed and each
element separated by DELIM"
  (declare (special delim))
  (format nil "~{~a~^~/%d/~:*~}" list))

Explanation:

"~{      iteration start
 ~a      print element
 ~^      exit iteration if no more elements
 ~/%d/   call function %d with one element
 ~:*     move one element backwards
 ~}"     end of iteration command

%d is just an 'internal' function, which should not be called outside join. As a marker for that, it has the % prefix.

~/foo/ is a way to call a function foo from a format string.

The variables delim are declared special, so that there can be a value for the delimiter transferred into the %d function. Since we can't make Lisp call the %d function from FORMAT with a delimiter argument, we need to get it from somewhere else - here from a dynamic binding introduced by the join function.

The only purpose of the function %d is to write a delimiter - it ignores the arguments passed by format - it only uses the stream argument.

Privatdocent answered 11/12, 2016 at 21:1 Comment(8)
Dear Rainer, is there a reason, why you make the variable name begin with %? Is the (declare (special delim)) kind of a way to make the delim variable bind dynamically or so? How does this function exactly work? Why the %d function needs those arguments?Vanlandingham
Where one can find anythng about the ~/ / directive? - I couldn't find about it anything in CLTL ...Vanlandingham
@coredump Thank you! Okay, still learning where to search/gather the information for cl stuff - obviously :D.Vanlandingham
@Gwang-JinKim: you can find ~/ in the Common Lisp Hyperspec, use the master index, non-alphabetic chars: lispworks.com/documentation/HyperSpec/Front/X_Mast_9.htmPrivatdocent
@RainerJoswig: Thank you very much! Thank you for the detailed explanation! This is sth I was searching. However, do you have an idea about my "#\\Tab" as "#\Tab" problem in my question?Vanlandingham
@Gwang-JinKim: which tab problem? Make a separate question. Note that my function does: (join '(1 2 3 4 5) #\tab) -> "1 2 3 4 5" - with tab characters between the numbers.Privatdocent
@Rainer - just right now I see that someone asked exactly the question I want to post (or did post). My question: #50648135 and other persons: #23577695Vanlandingham
Let us continue this discussion in chat.Vanlandingham
T
6

Melpa hosts the package s ("The long lost Emacs string manipulation library"), which provides many simple string utilities, including a string joiner:

(require 's)                                                                                                                                                                                          
(s-join ", " (list "a" "b" "c"))                                                                                                                                                                      
"a, b, c"

For what it's worth, very thinly under the hood, it's using a couple lisp functions from fns.c, namely mapconcat and identity:

(mapconcat 'identity (list "a" "b" "c") ", ")                                                                                                                                                         
"a, b, c"
Tryparsamide answered 27/9, 2019 at 20:44 Comment(1)
I note that this question is tagged with common-lisp, so this answer is arguably off-topic... that said, it does look useful for those who will undoubtedly also land here when looking for an elisp solution, so... good stuff, just noting for any non-emacs-user common-lisp newbies that this answer does not apply to you. :)Ozonosphere
B
5

A bit late to the party, but reduce works fine:

(reduce (lambda (acc x)
          (if (zerop (length acc))
              x
              (concatenate 'string acc "&" x)))
        (list "name=slappy" "friends=none" "eats=dogpoo")
        :initial-value "")
Brumbaugh answered 19/2, 2016 at 20:57 Comment(0)
Z
4

Assuming a list of strings and a single character delimiter, the following should work efficiently for frequent invocation on short lists:

(defun join (list &optional (delimiter #\&))
  (with-output-to-string (stream)
    (join-to-stream stream list delimiter)))

(defun join-to-stream (stream list &optional (delimiter #\&))
  (destructuring-bind (&optional first &rest rest) list
    (when first
      (write-string first stream)
      (when rest
        (write-char delimiter stream)
        (join-to-stream stream rest delimiter)))))
Zuniga answered 24/2, 2012 at 14:50 Comment(0)
D
2

With the newish and simple str library:

(ql:quickload "str")
(str:join "&" '("USERID=XYZ" "USERPWD=123"))

It uses format like explained in the other answers:

(defun join (separator strings)
" "
(let ((separator (replace-all "~" "~~" separator)))
  (format nil
        (concatenate 'string "~{~a~^" separator "~}")
        strings)))

(author of it, to make simple things like this simple).

Dropping answered 1/6, 2018 at 19:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.