Skip to content

Commit 33859e0

Browse files
committed
regexp: add ReplaceAllSubmatchFunc
Fixes #5690
1 parent 5bb6d16 commit 33859e0

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

src/regexp/all_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,22 @@ var replaceFuncTests = []ReplaceFuncTest{
263263
{"[a-c]*", func(s string) string { return "x" + s + "y" }, "defabcdef", "xydxyexyfxabcydxyexyfxy"},
264264
}
265265

266+
type ReplaceFuncSubmatchTest struct {
267+
pattern string
268+
replacement func([]string) string
269+
input, output string
270+
}
271+
272+
var replaceFuncSubmatchTests = []ReplaceFuncSubmatchTest{
273+
{"[a-c]+", func(g []string) string { return "x" + g[0] + "y" }, "defdef", "defdef"},
274+
{"[a-c]", func(g []string) string { return "x" + g[0] + "y" }, "defabcdef", "defxayxbyxcydef"},
275+
{"[a-c]+", func(g []string) string { return "x" + g[0] + "y" }, "defabcdef", "defxabcydef"},
276+
{"[a-c]*", func(g []string) string { return "x" + g[0] + "y" }, "defabcdef", "xydxyexyfxabcydxyexyfxy"},
277+
{"<<placeholder\\.(\\d+)>>", func(g []string) string { return "-" + g[1] + "-" }, "a<<placeholder.1>>b<<placeholder.2>>c<<placeholder.3>>d", "a-1-b-2-c-3-d"},
278+
{"(\\w+)\\s+(\\w+)", func(g []string) string { return g[2] + " " + g[1] }, "hello world", "world hello"},
279+
{"[aeiou]", func(g []string) string { return "" }, "hello", "hll"},
280+
}
281+
266282
func TestReplaceAll(t *testing.T) {
267283
for _, tc := range replaceTests {
268284
re, err := Compile(tc.pattern)
@@ -350,6 +366,36 @@ func TestReplaceAllFunc(t *testing.T) {
350366
}
351367
}
352368

369+
func TestReplaceAllSubmatchFunc(t *testing.T) {
370+
for _, tc := range replaceFuncSubmatchTests {
371+
re, err := Compile(tc.pattern)
372+
if err != nil {
373+
t.Errorf("Unexpected error compiling %q: %v", tc.pattern, err)
374+
continue
375+
}
376+
actual := re.ReplaceAllStringSubmatchFunc(tc.input, tc.replacement)
377+
if actual != tc.output {
378+
t.Errorf("%q.ReplaceFunc(%q,fn) = %q; want %q",
379+
tc.pattern, tc.input, actual, tc.output)
380+
}
381+
// now try bytes
382+
actual = string(re.ReplaceAllSubmatchFunc(
383+
[]byte(tc.input),
384+
func(g [][]byte) []byte {
385+
stringGroups := make([]string, len(g))
386+
for i, group := range g {
387+
stringGroups[i] = string(group)
388+
}
389+
return []byte(tc.replacement(stringGroups))
390+
},
391+
))
392+
if actual != tc.output {
393+
t.Errorf("%q.ReplaceFunc(%q,fn) = %q; want %q",
394+
tc.pattern, tc.input, actual, tc.output)
395+
}
396+
}
397+
}
398+
353399
type MetaTest struct {
354400
pattern, output, literal string
355401
isLiteral bool

src/regexp/regexp.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,25 @@ func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) str
580580
return string(b)
581581
}
582582

583+
// ReplaceAllStringSubmatchFunc returns a copy of src in which all matches of the
584+
// [Regexp] have been replaced by the return value of function repl applied to the
585+
// submatch. The replacement returned by repl is substituted directly, without using
586+
// [Regexp.Expand].
587+
func (re *Regexp) ReplaceAllStringSubmatchFunc(src string, repl func(groups []string) string) string {
588+
// Submatch positions are needed in this function, similar to when ReplaceAllString finds a $ in the replacement string.
589+
n := 2 * (re.numSubexp + 1)
590+
b := re.replaceAll(nil, src, n, func(dst []byte, match []int) []byte {
591+
groups := make([]string, len(match)/2)
592+
for i := 0; i < len(groups); i++ {
593+
if match[2*i] >= 0 {
594+
groups[i] = src[match[2*i]:match[2*i+1]]
595+
}
596+
}
597+
return append(dst, repl(groups)...)
598+
})
599+
return string(b)
600+
}
601+
583602
func (re *Regexp) replaceAll(bsrc []byte, src string, nmatch int, repl func(dst []byte, m []int) []byte) []byte {
584603
lastMatchEnd := 0 // end position of the most recent match
585604
searchPos := 0 // position where we next look for a match
@@ -682,6 +701,25 @@ func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte {
682701
})
683702
}
684703

704+
// ReplaceAllSubmatchFunc returns a copy of src in which all matches of the
705+
// [Regexp] have been replaced by the return value of function repl applied to the
706+
// submatch. The replacement returned by repl is substituted directly, without using
707+
// [Regexp.Expand].
708+
func (re *Regexp) ReplaceAllSubmatchFunc(src []byte, repl func(groups [][]byte) []byte) []byte {
709+
// Submatch positions are needed in this function, similar to when ReplaceAll finds a $ in the replacement string.
710+
n := 2 * (re.numSubexp + 1)
711+
b := re.replaceAll(src, "", n, func(dst []byte, match []int) []byte {
712+
groups := make([][]byte, len(match)/2)
713+
for i := 0; i < len(groups); i++ {
714+
if match[2*i] >= 0 {
715+
groups[i] = src[match[2*i]:match[2*i+1]:match[2*i+1]]
716+
}
717+
}
718+
return append(dst, repl(groups)...)
719+
})
720+
return b
721+
}
722+
685723
// Bitmap used by func special to check whether a character needs to be escaped.
686724
var specialBytes [16]byte
687725

0 commit comments

Comments
 (0)