Multiple replacements in string using a map

1,813 views
Skip to first unread message

shuaybi2 shuaybi2

unread,
Mar 14, 2011, 1:02:29 PM3/14/11
to clo...@googlegroups.com
I have a string such as:

"select * from account where acctId = _ACCT-ID_ and acctTyp = _ACCT-TYP_"

I have a map such as:

{:_ACCT-ID_ 9876 :_ACCT-TYP "B"}

I want to write a clojure function that will take the string and map as a parameter and return me the string with all values from the map substituted.

Output:

select * from account where acctId = 9876 and acctTyp = 'B'

Thanks for your help.

Daniel Solano Gomez

unread,
Mar 14, 2011, 2:17:30 PM3/14/11
to clo...@googlegroups.com

Something like the following does the trick:

(defn substitute-mappings
[string mappings]
(let [substitute-mapping
(fn [string mapping]
(let [pattern (java.util.regex.Pattern/quote (name (key mapping)))
pattern (re-pattern pattern)
matcher (re-matcher pattern string)
replacement (java.util.regex.Matcher/quoteReplacement (str (val mapping)))]
(.replaceAll matcher replacement)))]
(reduce substitute-mapping string (seq mappings))))

It uses reduce with an local function so that the string gets its
replacements one at a time. Actually doing the replacement uses Java's
regular expressions.

Just a few notes about the above:

1. quote and quoteReplacement to take care of any special characters
that may otherwise muck up replacement process
2. The name function is used to convert a keyword to a string without
the initial colon.
3. For integers and the like, I use the str function to coerce them into
a string that can be used as a replacement pattern. So, you'd want
to make sure this works correctly with any data types you want to
use.

Sincerely,

Daniel Solano Gómez

Ken Wesson

unread,
Mar 14, 2011, 3:28:08 PM3/14/11
to clo...@googlegroups.com

Do we have a str function that will prevent the use of {:_ACCT-ID_
"9876; delete * from account;"}? ;)

Or are those maps only ever going to be visible to the application's internals?

semperos

unread,
Mar 14, 2011, 4:49:04 PM3/14/11
to clo...@googlegroups.com
There is also this contrib code which facilitates string interpolation: http://clojure.github.com/clojure-contrib/strint-api.html

But I second Ken's concern about SQL injection attacks.

Aaron Cohen

unread,
Mar 15, 2011, 11:35:04 AM3/15/11
to clo...@googlegroups.com
On Mon, Mar 14, 2011 at 2:17 PM, Daniel Solano Gomez
<clo...@sattvik.com> wrote:
> On Mon Mar 14 13:02 2011, shuaybi2 shuaybi2 wrote:
>> I have a string such as:
>>
>> "select * from account where acctId = _ACCT-ID_ and acctTyp = _ACCT-TYP_"

There are several clojure libraries that exist to improve the ease and
safety of doing something like this. Amongst them are
clojure.contrib.sql and ClojureQL, which take different approaches.
They all should be sufficient to guard against SQL injection and
should probably be the first place you look.

For the more general question you were asking about how to generically
replace a map of matches-to-replacements though, Daniel did a good job
showing how to use a reduce over the map. That method will call
"replaceAll" once per entry in the map, which is probably fine if you
don't have many substitutions.

Another way to do it is using clojure.string.replace, which has an
often-overlooked third overload which matches with a regex and
replaces with a "mapping function."

Starting with a simple example:
user=>(require '[clojure.string :as s])
nil
user=>(s/replace "a b a" #"a|b" {"a" "1" "b" "2"})
"1 2 1"

In the example, the map was being used as a "replacement function".

---
If you're willing to change your map to use strings as keys and
values, then the previous example is good enough.

Otherwise, because you're wanting to use keywords as your keys, and
arbitratry values for your values, we'll need to use a slightly more
sophisticated replacement function.

(defn key-pattern
"Create a regex Pattern of the form '<key1>|<key2>', the key names
will be quoted in case they contain special regex characters"
[m]
(->> (keys m)
(map #(java.util.regex.Pattern/quote (name %)))
(s/join "|")
java.util.regex.Pattern/compile))

(defn replace-map [text m]
(s/replace text
(key-pattern m)
(fn [field-name]
(java.util.regex.Matcher/quoteReplacement (str (get m
(keyword field-name)))))))

Mond Ray

unread,
Apr 19, 2013, 8:17:57 PM4/19/13
to clo...@googlegroups.com
Old thread but what the heck... it doesn't work in my REPL

user=> (defn key-pattern
  #_=>     "Create a regex Pattern of the form '<key1>|<key2>', the key names
  #_=> will be quoted in case they contain special regex characters"
  #_=>     [m]
  #_=>     (->> (keys m)
  #_=>         (map #(java.util.regex.Pattern/quote (name %)))
  #_=>         (s/join "|")
  #_=>         java.util.regex.Pattern/compile))
#'user/key-pattern
user=> 

user=> (defn replace-map [text m]
  #_=>     (s/replace text
  #_=>        (key-pattern m)
  #_=>        (fn [field-name]
  #_=>           (java.util.regex.Matcher/quoteReplacement (str (get m
  #_=> (keyword field-name)))))))
#'user/replace-map
user=> (replace-map "/path/:p0/b/:p1" {:p0 "1" :p1 "2"})
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn  user/key-pattern/fn--408 (NO_SOURCE_FILE:6)

user=> (key-pattern {:a 1})
ClassCastException java.lang.String cannot be cast to clojure.lang.IFn  user/key-pattern/fn--408 (NO_SOURCE_FILE:6)

Am I doing something wrong or is there a typo in your code?

Sean Corfield

unread,
Apr 19, 2013, 8:32:03 PM4/19/13
to clo...@googlegroups.com
I just tried the code in a fresh REPL with Clojure 1.5.1 and it works,
so I tried it with Clojure 1.4.0 and it works. Well, assuming you do
this first: (require '[clojure.string :as s])

What version of Clojure are you using? Are you doing the require? Do
you have something else defined as `s`?

Sean
> --
> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@googlegroups.com
> Note that posts from new members are moderated - please be patient with your
> first post.
> To unsubscribe from this group, send email to
> clojure+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en
> ---
> You received this message because you are subscribed to the Google Groups
> "Clojure" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to clojure+u...@googlegroups.com.
> For more options, visit https://groups.google.com/groups/opt_out.
>
>



--
Sean A Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
World Singles, LLC. -- http://worldsingles.com/

"Perfection is the enemy of the good."
-- Gustave Flaubert, French realist novelist (1821-1880)

Andy Fingerhut

unread,
Apr 19, 2013, 8:37:14 PM4/19/13
to clo...@googlegroups.com
I fired up a Clojure 1.5.1 REPL, did (require '[clojure.string :as s]) first, then copied and pasted those two function definitions, and did not get the errors you are seeing.  I don't have a good guess why you are getting those errors.  Did you do the require first?  What version of Clojure are you using?

Andy


On Fri, Apr 19, 2013 at 5:17 PM, Mond Ray <mondr...@gmail.com> wrote:

--
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Mond Ray

unread,
Apr 20, 2013, 3:51:21 AM4/20/13
to clo...@googlegroups.com
Just tried again - using lein repl (clojure 1.4.0) and it worked fine.

It was late - who knows what I did ;-)

Thanks for checking guys.

Mond Ray

unread,
Apr 22, 2013, 4:45:36 PM4/22/13
to clo...@googlegroups.com
Something very odd going on here - one day it works the next day it fails :(

$ lein repl
nREPL server started on port 51502
REPL-y 0.1.10
Clojure 1.5.1
    Exit: Control+D or (exit) or (quit)
Commands: (user/help)
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
          (user/sourcery function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
Examples from clojuredocs.org: [clojuredocs or cdoc]
          (user/clojuredocs name-here)
          (user/clojuredocs "ns-here" "name-here")
user=> (require '[clojure.string :as s])
nil
user=> ; code from the clojure google group to help map properties into paths

user=> ; first a way to take the keys and produce a regex

user=> (defn key-pattern
  #_=>   "Create a regex Pattern of the form '<key1>|<key2>', the key names
  #_=>   will be quoted in case they contain special regex characters"
  #_=>   [m]
  #_=>   (->> (keys m)
  #_=>     (map #(java.util.regex.Pattern/quote (keyword %)))
  #_=>     (s/join "|")
  #_=>     java.util.regex.Pattern/compile))
#'user/key-pattern
user=> 

user=> ; second a way to use the above function to achieve the replacement

user=> (defn replace-map [text m]
  #_=>   "Replace keys in a String from matching values in a map"
  #_=>   (s/replace text
  #_=>     (key-pattern m)
  #_=>     (fn [field-name]
  #_=>       (java.util.regex.Matcher/quoteReplacement (str (get m (keyword field-name)))))))
#'user/replace-map
user=> 

user=> (def param-map {:$tld "com"})
#'user/param-map
user=> 

user=> (def url (replace-map "https://google.$tld" param-map))
ClassCastException clojure.lang.Keyword cannot be cast to java.lang.String  user/key-pattern/fn--352 (NO_SOURCE_FILE:6)

user=> 

You see my full REPL session.

I reckon I must be doing something dumb wrong - any ideas on what's going on?

Thanks

Ray

Andy Fingerhut

unread,
Apr 22, 2013, 4:59:50 PM4/22/13
to clo...@googlegroups.com
You changed the definition of key-pattern.  The original had "name" where your most recent version has "keyword".  Change it back to "name" and at least I was able to get it to work.

Andy

Sean Corfield

unread,
Apr 22, 2013, 5:19:16 PM4/22/13
to clo...@googlegroups.com
On Mon, Apr 22, 2013 at 1:45 PM, Mond Ray <mondr...@gmail.com> wrote:
> Something very odd going on here - one day it works the next day it fails :(

This code is different to what you posted the other day...

> #_=> (map #(java.util.regex.Pattern/quote (keyword %)))

That won't work - Pattern/quote will not accept a keyword (which is
what the exception is telling you). In the code you posted before,
which did work, you had (name %) instead which would yield a String
from either a keyword or a string - which is clearly what you want
here.

Mond Ray

unread,
Apr 23, 2013, 12:55:51 AM4/23/13
to clo...@googlegroups.com
Man - you guys are good and *fast*.

I was pootling around with a version that would directly replace "/a/:key/b" {:key "value"} with "/a/value/b" and a failed version sneaked into my code.

Incidentally that is my only slight complaint about the code as it stands: I have to use something like

(def url (replace-map "https://google.$tld"  {:$tld "com"}))

rather than the more elegant and seemingly idiomatic

(def url (replace-map "https://google.:tld"  {:tld "com"}))

Since I am struggling to win this small battle, do you guys have any ideas (including that its not a good idea!)?

Cheers

Ray

rod naph

unread,
Apr 23, 2013, 2:23:27 AM4/23/13
to clo...@googlegroups.com
I scratched my itch with...

(defn map-replace [m text]
  (reduce
    (fn [acc [k v]] (s/replace acc (str k) (str v)))
    text m))

Hope it helps.

mond

unread,
Apr 23, 2013, 3:53:17 PM4/23/13
to clo...@googlegroups.com
Chapeau - nicely, ahem, reduced

shin...@gmail.com

unread,
Apr 24, 2013, 9:52:06 AM4/24/13
to clo...@googlegroups.com
if map is following:
{:_ACCT-ID_ 9876 :_ACCT-TYP_ "B"}

then:
(defn sql-map-replace [sql m]
  (letfn [(quote-if-str [s]
           (if (string? s) (str "'" s "'") s))]
    (reduce-kv (fn [ret k v]
                 (clojure.string/replace ret
                  (re-pattern (str "(.*)" (name k) "(.*)"))
                  (str "$1" (quote-if-str v) "$2")))
               sql m)))
Reply all
Reply to author
Forward
0 new messages