Skip to content

Commit 9252893

Browse files
jafingerhutstuarthalloway
authored andcommitted
Fix for CLJ-753, CLJ-870, and CLJ-905.
Signed-off-by: Stuart Halloway <stu@thinkrelevance.com>
1 parent a085c81 commit 9252893

2 files changed

Lines changed: 94 additions & 22 deletions

File tree

src/clj/clojure/string.clj

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ Design notes for clojure.string:
3636
general than String. In ordinary usage you will almost always
3737
pass concrete strings. If you are doing something unusual,
3838
e.g. passing a mutable implementation of CharSequence, then
39-
thead-safety is your responsibility."
39+
thread-safety is your responsibility."
4040
:author "Stuart Sierra, Stuart Halloway, David Liebke"}
4141
clojure.string
4242
(:refer-clojure :exclude (replace reverse))
43-
(:import (java.util.regex Pattern)
43+
(:import (java.util.regex Pattern Matcher)
4444
clojure.lang.LazilyPersistentVector))
4545

4646
(defn ^String reverse
@@ -49,16 +49,26 @@ Design notes for clojure.string:
4949
[^CharSequence s]
5050
(.toString (.reverse (StringBuilder. s))))
5151

52+
(defn ^String re-quote-replacement
53+
"Given a replacement string that you wish to be a literal
54+
replacement for a pattern match in replace or replace-first, do the
55+
necessary escaping of special characters in the replacement."
56+
{:added "1.4"}
57+
[^CharSequence replacement]
58+
(Matcher/quoteReplacement (.toString ^CharSequence replacement)))
59+
5260
(defn- replace-by
5361
[^CharSequence s re f]
5462
(let [m (re-matcher re s)]
55-
(let [buffer (StringBuffer. (.length s))]
56-
(loop []
57-
(if (.find m)
58-
(do (.appendReplacement m buffer (f (re-groups m)))
59-
(recur))
60-
(do (.appendTail m buffer)
61-
(.toString buffer)))))))
63+
(if (.find m)
64+
(let [buffer (StringBuffer. (.length s))]
65+
(loop [found true]
66+
(if found
67+
(do (.appendReplacement m buffer (Matcher/quoteReplacement (f (re-groups m))))
68+
(recur (.find m)))
69+
(do (.appendTail m buffer)
70+
(.toString buffer)))))
71+
s)))
6272

6373
(defn ^String replace
6474
"Replaces all instance of match with replacement in s.
@@ -69,7 +79,21 @@ Design notes for clojure.string:
6979
char / char
7080
pattern / (string or function of match).
7181
72-
See also replace-first."
82+
See also replace-first.
83+
84+
The replacement is literal (i.e. none of its characters are treated
85+
specially) for all cases above except pattern / string.
86+
87+
For pattern / string, $1, $2, etc. in the replacement string are
88+
substituted with the string that matched the corresponding
89+
parenthesized group in the pattern. If you wish your replacement
90+
string r to be used literally, use (re-quote-replacement r) as the
91+
replacement argument. See also documentation for
92+
java.util.regex.Matcher's appendReplacement method.
93+
94+
Example:
95+
(clojure.string/replace \"Almost Pig Latin\" #\"\\b(\\w)(\\w+)\\b\" \"$2$1ay\")
96+
-> \"lmostAay igPay atinLay\""
7397
{:added "1.2"}
7498
[^CharSequence s match replacement]
7599
(let [s (.toString s)]
@@ -85,12 +109,13 @@ Design notes for clojure.string:
85109
(defn- replace-first-by
86110
[^CharSequence s ^Pattern re f]
87111
(let [m (re-matcher re s)]
88-
(let [buffer (StringBuffer. (.length s))]
89-
(if (.find m)
90-
(let [rep (f (re-groups m))]
91-
(.appendReplacement m buffer rep)
92-
(.appendTail m buffer)
93-
(str buffer))))))
112+
(if (.find m)
113+
(let [buffer (StringBuffer. (.length s))
114+
rep (Matcher/quoteReplacement (f (re-groups m)))]
115+
(.appendReplacement m buffer rep)
116+
(.appendTail m buffer)
117+
(str buffer))
118+
s)))
94119

95120
(defn- replace-first-char
96121
[^CharSequence s ^Character match replace]
@@ -100,6 +125,14 @@ Design notes for clojure.string:
100125
s
101126
(str (subs s 0 i) replace (subs s (inc i))))))
102127

128+
(defn- replace-first-str
129+
[^CharSequence s ^String match ^String replace]
130+
(let [^String s (.toString s)
131+
i (.indexOf s match)]
132+
(if (= -1 i)
133+
s
134+
(str (subs s 0 i) replace (subs s (+ i (.length match)))))))
135+
103136
(defn ^String replace-first
104137
"Replaces the first instance of match with replacement in s.
105138
@@ -109,16 +142,31 @@ Design notes for clojure.string:
109142
string / string
110143
pattern / (string or function of match).
111144
112-
See also replace-all."
145+
See also replace.
146+
147+
The replacement is literal (i.e. none of its characters are treated
148+
specially) for all cases above except pattern / string.
149+
150+
For pattern / string, $1, $2, etc. in the replacement string are
151+
substituted with the string that matched the corresponding
152+
parenthesized group in the pattern. If you wish your replacement
153+
string r to be used literally, use (re-quote-replacement r) as the
154+
replacement argument. See also documentation for
155+
java.util.regex.Matcher's appendReplacement method.
156+
157+
Example:
158+
(clojure.string/replace-first \"swap first two words\"
159+
#\"(\\w+)(\\s+)(\\w+)\" \"$3$2$1\")
160+
-> \"first swap two words\""
113161
{:added "1.2"}
114162
[^CharSequence s match replacement]
115163
(let [s (.toString s)]
116164
(cond
117165
(instance? Character match)
118166
(replace-first-char s match replacement)
119167
(instance? CharSequence match)
120-
(.replaceFirst s (Pattern/quote (.toString ^CharSequence match))
121-
(.toString ^CharSequence replacement))
168+
(replace-first-str s (.toString ^CharSequence match)
169+
(.toString ^CharSequence replacement))
122170
(instance? Pattern match)
123171
(if (instance? CharSequence replacement)
124172
(.replaceFirst (re-matcher ^Pattern match s)

test/clojure/test_clojure/string.clj

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,36 @@
1212

1313
(deftest t-replace
1414
(is (= "faabar" (s/replace "foobar" \o \a)))
15+
(is (= "foobar" (s/replace "foobar" \z \a)))
1516
(is (= "barbarbar" (s/replace "foobarfoo" "foo" "bar")))
16-
(is (= "FOObarFOO" (s/replace "foobarfoo" #"foo" s/upper-case))))
17+
(is (= "foobarfoo" (s/replace "foobarfoo" "baz" "bar")))
18+
(is (= "f$$d" (s/replace "food" "o" "$")))
19+
(is (= "f\\\\d" (s/replace "food" "o" "\\")))
20+
(is (= "barbarbar" (s/replace "foobarfoo" #"foo" "bar")))
21+
(is (= "foobarfoo" (s/replace "foobarfoo" #"baz" "bar")))
22+
(is (= "f$$d" (s/replace "food" #"o" (s/re-quote-replacement "$"))))
23+
(is (= "f\\\\d" (s/replace "food" #"o" (s/re-quote-replacement "\\"))))
24+
(is (= "FOObarFOO" (s/replace "foobarfoo" #"foo" s/upper-case)))
25+
(is (= "foobarfoo" (s/replace "foobarfoo" #"baz" s/upper-case)))
26+
(is (= "OObarOO" (s/replace "foobarfoo" #"f(o+)" (fn [[m g1]] (s/upper-case g1)))))
27+
(is (= "baz\\bang\\" (s/replace "bazslashbangslash" #"slash" (constantly "\\")))))
1728

1829
(deftest t-replace-first
30+
(is (= "faobar" (s/replace-first "foobar" \o \a)))
31+
(is (= "foobar" (s/replace-first "foobar" \z \a)))
32+
(is (= "z.ology" (s/replace-first "zoology" \o \.)))
1933
(is (= "barbarfoo" (s/replace-first "foobarfoo" "foo" "bar")))
34+
(is (= "foobarfoo" (s/replace-first "foobarfoo" "baz" "bar")))
35+
(is (= "f$od" (s/replace-first "food" "o" "$")))
36+
(is (= "f\\od" (s/replace-first "food" "o" "\\")))
2037
(is (= "barbarfoo" (s/replace-first "foobarfoo" #"foo" "bar")))
21-
(is (= "z.ology" (s/replace-first "zoology" \o \.)))
22-
(is (= "FOObarfoo" (s/replace-first "foobarfoo" #"foo" s/upper-case))))
38+
(is (= "foobarfoo" (s/replace-first "foobarfoo" #"baz" "bar")))
39+
(is (= "f$od" (s/replace-first "food" #"o" (s/re-quote-replacement "$"))))
40+
(is (= "f\\od" (s/replace-first "food" #"o" (s/re-quote-replacement "\\"))))
41+
(is (= "FOObarfoo" (s/replace-first "foobarfoo" #"foo" s/upper-case)))
42+
(is (= "foobarfoo" (s/replace-first "foobarfoo" #"baz" s/upper-case)))
43+
(is (= "OObarfoo" (s/replace-first "foobarfoo" #"f(o+)" (fn [[m g1]] (s/upper-case g1)))))
44+
(is (= "baz\\bangslash" (s/replace-first "bazslashbangslash" #"slash" (constantly "\\")))))
2345

2446
(deftest t-join
2547
(are [x coll] (= x (s/join coll))
@@ -65,6 +87,7 @@
6587
s/reverse [nil]
6688
s/replace [nil #"foo" "bar"]
6789
s/replace-first [nil #"foo" "bar"]
90+
s/re-quote-replacement [nil]
6891
s/capitalize [nil]
6992
s/upper-case [nil]
7093
s/lower-case [nil]
@@ -85,6 +108,7 @@
85108
"baz::quux" s/replace-first ["baz--quux" #"--" "::"]
86109
"baz::quux" s/replace-first ["baz--quux" (StringBuffer. "--") (StringBuffer. "::")]
87110
"zim-zam" s/replace-first ["zim zam" #" " (StringBuffer. "-")]
111+
"\\\\ \\$" s/re-quote-replacement ["\\ $"]
88112
"Pow" s/capitalize ["POW"]
89113
"BOOM" s/upper-case ["boom"]
90114
"whimper" s/lower-case ["whimPER"]

0 commit comments

Comments
 (0)