From: Pillsy
Subject: There must be a better way.
Date: 
Message-ID: <1184006407.776229.284120@r34g2000hsd.googlegroups.com>
Say I have a string, and it may contain newlines, like so:

"foo
bar
baz"

I want to take this string and indent each line over by n spaces. If n
were 4, I would get this:

"    foo
    bar
    baz"

My solution is kind of fugly, and this seems, generically, like the
sort of thing FORMAT was designed to do in a reasonably
straightforward manner, but I couldn't really figure it out from the
CLHS. The solution I came up with ended a bit more general than I'd
initially intended, just because the way things broke down into
individual functions made the generality so easy.

(defun string->lines (string)
  (with-input-from-string (string string)
   (loop
      :for line := (read-line string nil string)
      :until (eq line string)
      :collect line)))

(defun prefix-string-lines (prefix string)
  (reduce (lambda (out line)
	    (format nil "~A~&~A~A" out prefix line))
	  (string->lines string)
	  :initial-value ""))

(defun indent-string-lines (indentation string
			    &optional (character #\Space))
  (prefix-string-lines
   (make-string indentation :initial-element character) string))

Test:

(devar *test-string*
"foo
bar
baz")

* (indent-string-lines 4 *test-string*)

"    foo
    bar
    baz"

So is there a less clunky way to do this? The one thing I considered
was doing something split-sequencish instead of repeatedly using READ-
LINE on the string, but I'm not sure I really like that better.

Cheers,
Pillsy

From: Joshua Taylor
Subject: Re: There must be a better way.
Date: 
Message-ID: <1184009686.844318.58190@22g2000hsm.googlegroups.com>
On Jul 9, 2:40 pm, Pillsy <·········@gmail.com> wrote:
> Say I have a string, and it may contain newlines, like so:
>
> "foo
> bar
> baz"
>
> I want to take this string and indent each line over by n spaces. If n
> were 4, I would get this:
>
> "    foo
>     bar
>     baz"
>
> My solution is kind of fugly, and this seems, generically, like the
> sort of thing FORMAT was designed to do in a reasonably
> straightforward manner, but I couldn't really figure it out from the
> CLHS. The solution I came up with ended a bit more general than I'd
> initially intended, just because the way things broke down into
> individual functions made the generality so easy.
>
> (defun string->lines (string)
>   (with-input-from-string (string string)
>    (loop
>       :for line := (read-line string nil string)
>       :until (eq line string)
>       :collect line)))
>
> (defun prefix-string-lines (prefix string)
>   (reduce (lambda (out line)
>             (format nil "~A~&~A~A" out prefix line))
>           (string->lines string)
>           :initial-value ""))
>
> (defun indent-string-lines (indentation string
>                             &optional (character #\Space))
>   (prefix-string-lines
>    (make-string indentation :initial-element character) string))
>
> Test:
>
> (devar *test-string*
> "foo
> bar
> baz")
>
> * (indent-string-lines 4 *test-string*)
>
> "    foo
>     bar
>     baz"
>
> So is there a less clunky way to do this? The one thing I considered
> was doing something split-sequencish instead of repeatedly using READ-
> LINE on the string, but I'm not sure I really like that better.
>
> Cheers,
> Pillsy

Well, this isn't amazingly elegant, the following code might give you
some ideas. It requires only a single iteration across the string,
doesn't require a call to read, but it does use with-output-to-string.
Depending on where your input is coming from, this might not be the
best approach, though. (E.g., if you're reading from a file, as in
http://www.emmett.ca/~sabetts/slurp.html).

(defun indent (string indent)
  "Return a new string similar to string, but in which each #\Newline
is followed by indent #\Space characters."
  (let ((pad (make-string indent :initial-element #\Space)))
    (with-output-to-string (out)
      (write-string pad out)
      (loop for char across string
	 do (write-char char out)
	 when (char= char #\Newline)
	 do (write-string pad out)))))

Cl-User> (indent "one
two
three" 4)
;=>
"    one
    two
    three"

You could also, of course, count #\newline ahead of time, and then
make a new static string into which you could copy the old string:

(defun indent2 (string indent)
  (let* ((newlines (count #\Newline string :test #'char=))
	 (output (make-string (+ (length string) (* indent (1+ newlines)))
			      :initial-element #\Space)))
    (loop with i = indent
       for char across string
       do (setf (char output i) char)	; copy char
       do (incf i)			; i++
       when (char= #\Newline char)	; account for the indent
       do (incf i indent)
       finally (return-from indent2 output))))

Cl-User> (indent2 "hello
world" 3)
;=>
"   hello
   world"

The size of output has to account for the characters of the original
string, plus indent characters for each newline, /and/ at the
beginning of the string. The loop could be written tail-recursively
with a labels, if one was so inclined.

There are, of course, lots of ways to do this; these are only two
relatively quick and dirty ways to do it. If writing to strings were
expensive, you might look at replace (http://www.lispworks.com/
documentation/HyperSpec/Body/f_replac.htm) and try to combine the
copying operations, but I doubt this would be an issue these days.

Enjoy,
//J
From: Richard M Kreuter
Subject: Re: There must be a better way.
Date: 
Message-ID: <87ps31pdqn.fsf@tan-ru.localdomain>
Pillsy <·········@gmail.com> writes:

> Say I have a string, and it may contain newlines, like so:
>
> "foo
> bar
> baz"
>
> I want to take this string and indent each line over by n spaces. If n
> were 4, I would get this:
>
> "    foo
>     bar
>     baz"
>
> My solution is kind of fugly, and this seems, generically, like the
> sort of thing FORMAT was designed to do in a reasonably
> straightforward manner, but I couldn't really figure it out from the
> CLHS. The solution I came up with ended a bit more general than I'd
> initially intended, just because the way things broke down into
> individual functions made the generality so easy.

I don't think you can do the splitting with FORMAT, but you can do the
indentation.  For example,

(format t "~{~,,4:<~A~%~>~^~}" (list "foo" "bar" "baz"))

Unfortunately, the 4 in that format string doesn't parameterize
nicely.  If that does need to vary, the best I can think of is to
replace the 4 in the following with a variable:

(format t "~:{~,,v:<~A~%~>~}"
        (mapcar (lambda (x) (list 4 x))
                (list "foo" "bar" "baz")))

--
RmK
From: Wade Humeniuk
Subject: Re: There must be a better way.
Date: 
Message-ID: <m2y7hog6oe.fsf@telus.net.no.spam>
Here is another possible solution (it has the little quirk
of putting at newline at the very end, even if there
was none before).

(defun indent (string indent)
  (with-input-from-string (is string)
    (with-output-to-string (os)
      (loop for line = (read-line is nil nil)
	   while line do
	   (format os "~V,0T~A~%" indent line)))))


CL-USER> (defvar *test-string*
"foo
bar
baz")
*TEST-STRING*
CL-USER> (indent *test-string* 4)
"    foo
    bar
    baz
"
CL-USER> 

Wade