Chris Edwards
Chris Edwards

Reputation: 1558

Clojure, replace each instance of a character with a different value from a collection

First of all, I am not sure how to easily word the title.

The problem I have is give a string insert value here ? I want to be able to swap the ? with a value of my choice, i can do this using clojure.string/replace.

Now, the use case I require is slightly more complex, given a string like:

these are the specified values: ?, ?, ?, ?

I want to replace the values of the ? with values from a collection which could look like:

[2 389 90 13]

so in this example the string would now read:

these are the specified values: 2, 389, 90, 13

so ? x maps to collection x (e.g. ? 0 maps to collection 0)

The number of ? will not always be 4 or a specific n, however the length of the collection will always be the same as the number of ?.

I tried doing the following:

(mapv #(clojure.string/replace-first statement "?" %) [1 2 3 4])

But this doesn't produce the desired results in produces a vector of size 4 where only the first ? is replaced by the value.

I am lost due the inability to modify variables in clojure, and I don't want to have a global string which is redefined and passed to a function n times.

Upvotes: 3

Views: 601

Answers (3)

Magos
Magos

Reputation: 3014

While I agree that DaoWen's answer is likely the most practical, the end of your question seems worth discussing a little as well as a matter of learning functional approaches. You're essentially looking for a way to

  1. Take the initial string and the first value and use replace-first to make another string from them.
  2. Take the result of that and the next value from the sequence and use replace-firston them.
  3. Repeat 2 until you've gone through the entire sequence.

This is actually a classic pattern in mathematics and functional programming, called a "left fold" or "reduction" over the value sequence. Functional languages usually build it into their standard libraries as a higher order function. In Clojure it's called reduce. Implementing your attempted strategy with it looks something like

(reduce #(clojure.string/replace-first %1 "?" %2) 
    "these are the specified values: ?, ?, ?, ?" 
     [2 389 90 13])
; => "these are the specified values: 2, 389, 90, 13"

Note that unlike your similar function literal, this takes two arguments, so that the statement can be rebound as we proceed through the reduction.
If you want to see what happens along the way with a reduce, you can swap it out with reductions. Here you get

("these are the specified values: ?, ?, ?, ?" ;After 0 applications of our replace-first fn
 "these are the specified values: 2, ?, ?, ?" ;;Intermediate value after 1 application
 "these are the specified values: 2, 389, ?, ?" ;;after 2...
 "these are the specified values: 2, 389, 90, ?"
 "these are the specified values: 2, 389, 90, 13");;Final value returned by reduce

Upvotes: 6

TheQuickBrownFox
TheQuickBrownFox

Reputation: 10624

DaoWen has given the pragmatic answer for strings but you do have to replace your "?" with "%s" first.

But suppose this wasn't a string. Clojure has a great collection library that often means you can avoid recursion or reducers but I couldn't think of a way to do that here without getting really inefficient and messy. So here's a reduce solution applicable to non-strings, with lots of comments.

(let [original "these are the specified values: ?, ?, ?, ?"
      replacements [2 389 90 13]
      starting-accumulator [[] replacements] ; Start with an empty vector and all of the replacements.
      reducing-fn (fn [[acc [r & rs :as all-r]] o] ; Apply destructuring liberally
                    (if (= o \?) ; If original character is "?".
                      [(conj acc r) rs] ; Then add the replacement character and return the rest of the remaining replacements.
                      [(conj acc o) all-r])) ; Else add the original character and return all the remaining replacements.
      reduced (reduce reducing-fn starting-accumulator original) ; Run the reduce.
      result (first reduced) ; Get the resulting seq (discard the second item: the remaining empty seq of replacements).
      string-joined (apply str result)] ; The string was turned into a seq of chars by `reduce`. Turn it back into a string.
  string-joined)

Upvotes: 1

DaoWen
DaoWen

Reputation: 33019

There might be other considerations that aren't covered by your question—but as written, it seems like you should just be using the string format function:

(apply format
       "these are the specified values: %s, %s, %s, %s"
       [2 389 90 13])
; => "these are the specified values: 2, 389, 90, 13"

Upvotes: 5

Related Questions