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
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
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
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