1

I have already searched the website, but did not find a suitable answer to my particular question on substring replacements. I know how to replace substring/regexp via clojure.string/replace but am not sure to to utilize it in this case.

Let us say. I have some strings at first place:

(def test-str "I am a test string. :name to replace, :age to replace. And there are strange symbols in this string. And here are the keyword shall not modify, such as :not-interested.")
(def another-test-str ":nothing :to :replace")

And I have a translation table:

(def translation-table {:name "Alice"
                :age  19})

I want to replace :name in test-str with "Alice", :age with 19, but do not want to replace :not-interested. The table is long, and different strings (to be replaced) contains different keywords.

Given the translation table, what is a possible systematic way of doing the substring replacement then? Namely, I want a function replace-with-translation:

(replace-with-translation test-str translation-table)
=> "I am a test string. Alice to replace, 19 to replace. And there are strange symbols in this string. And here are the keyword shall not modify, such as :not-interested."

(replace-with-translation another-test-str translation-table)
=> ":nothing :to :replace"

(replace-with-translation test-str {})
=> "I am a test string. :name to replace, :age to replace. And there are strange symbols in this string. And here are the keyword shall not modify, such as :not-interested."

4 Answers 4

8

string/replace can use a function as the replacement, and get (retrieving from the map) takes an optional argument to use in case the entry was missing. Together, those features lend themselves to a concise solution:

(require '[clojure.string :as string])

(defn replace-with-translation [s m]
  (string/replace s #":([-_a-zA-Z0-9]+)" 
    (fn [[r k]]
      (str (get m (keyword k) r)))))

P.S. That regex does not completely account for all possible keywords, but if it meets your requirements then it has the advantage of most likely also working in ClojureScript.

Sign up to request clarification or add additional context in comments.

2 Comments

this one is an absolute winner, since it does it in one pass, and also allows a flexible logic for the keys, that are present in a template, but absent in a dictionary, in contrast with other solutions making regex from dict keys. So it looks like it should be faster AND is more flexible AND more idiomatic
also, the regex can be rewritten to #":([\w\-]+)", \w === [A-Za-z0-9_]
2

A one liner:

(reduce (fn [s [k v]] (str/replace s (str k) (str v)))
        "I am a test string. :name to replace, :age to replace. And there are strange symbols in this string. And here are the keyword shall not modify, such as :not-interested."
        {:name "Alice"
         :age  19})
;; => "I am a test string. Alice to replace, 19 to replace. And there are strange symbols in this string. And here are the keyword shall not modify, such as :not-interested."

Or you can make your translation-table to be string->string first. With that you can avoid the repeated calls to replace.

(let [translation-table {":name" "Alice"
                         ":age"  "19"}
      pat               (->> translation-table
                             keys
                             (str/join "|")
                             re-pattern)]
  (time
   (str/replace "I am a test string. :name to replace, :age to replace. And there are strange symbols in this string. And here are the keyword shall not modify, such as :not-interested."
                pat
                translation-table)))
;; "Elapsed time: 0.0258 msecs"
;; => "I am a test string. Alice to replace, 19 to replace. And there are strange symbols in this string. And here are the keyword shall not modify, such as :not-interested."

...previous version is 0.1345 msecs

Comments

1

Here is a corner case question for you.

What if the value of translation-table was {:not-in "foo"}?

Would you want (replace-with-translation "such as :not-interested." translation-table) to return:

(a) "such as footerested."

(b) "such as :not-interested."

(c) Something else?

If the answer is (a), then I can show you how to implement that using one or more calls to clojure.string/replace.

If the answer is (b), then it opens up all kinds of corner case questions of how you want to decide whether an occurrence of the characters ":not-in" should be matched on, or should not be matched on, in the string where replacements might be made.

2 Comments

Thank you for comments. It shall be (b) in my case.
I do not know if this is a decision under your control, but if you can make the strings to be replaced have explicit delimiters characters like (replace-with-translation "such as [:not-interested]" translation-table), it will be much easier to do the replacements as you wish, without replacing things you do not want to.
0

You need to write a function to apply the str/replace to each pair of target/value in the map. Here is how I would do it:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [schema.core :as s]
    [tupelo.schema :as tsk]
    [clojure.string :as str]))


(def test-str "I am a test string. :name to replace, :age to replace. And there are strange symbols in this string. And here are the keyword shall not modify, such as :not-interested.")
(def another-test-str ":nothing :to :replace")

(def translation-table {:name "Alice"
                        :age  19})

(s/defn replace-with-translation :- s/Str
  [template-str :- s/Str
   tx-table :- tsk/KeyMap]
  (let [tx-tgts                (mapv pr-str (keys tx-table)) ; convert all keywords => string
        tx-vals                (mapv str (vals tx-table)) ; coerce any integers => string
        tx-pairs               (mapv vector tx-tgts tx-vals)
        ; ^^^ example:  tx-pairs => [[":name" "Alice"] [":age" "19"]]

        replace-placeholder-fn (fn [result tx-pair]
                                 (let [[tx-tgt tx-val] tx-pair]
                                   (str/replace result tx-tgt tx-val)))
        result-str             (reduce
                                 replace-placeholder-fn
                                 template-str
                                 tx-pairs)]
    result-str))

The unit tests show the code in action:

(dotest
  (is= (replace-with-translation test-str translation-table)
    "I am a test string. Alice to replace, 19 to replace. And there are strange symbols in this string. And here are the keyword shall not modify, such as :not-interested.")
  (is= (replace-with-translation another-test-str translation-table)
    ":nothing :to :replace")
  )

1 Comment

Thanks. Yeah, loop-over the keys in translation-table will definitely do it with careful treatment of corner cases.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.