Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions py/string.go
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,26 @@ func (s String) M__contains__(item Object) (Object, error) {
return NewBool(strings.Contains(string(s), string(needle))), nil
}

// stringSearchBounds adjusts negative indices without clamping a start
// beyond the end, which must not match even an empty substring.
func stringSearchBounds(start, end, length int) (int, int) {
if start < 0 {
start += length
if start < 0 {
start = 0
}
}
if end < 0 {
end += length
if end < 0 {
end = 0
}
} else if end > length {
end = length
}
return start, end
}

func (s String) Count(args Tuple) (Object, error) {
var (
pysub Object
Expand All @@ -636,14 +656,9 @@ func (s String) Count(args Tuple) (Object, error) {
end = int(pyend.(Int))
size = s.len()
)
if beg > size {
beg = size
}
if end < 0 {
end = size
}
if end > size {
end = size
beg, end = stringSearchBounds(beg, end, size)
if beg > end {
return Int(0), nil
}

var (
Expand All @@ -670,14 +685,9 @@ func (s String) find(args Tuple) (Object, error) {
end = int(pyend.(Int))
size = s.len()
)
if beg > size {
beg = size
}
if end < 0 {
end = size
}
if end > size {
end = size
beg, end = stringSearchBounds(beg, end, size)
if beg > end {
return Int(-1), nil
}

var (
Expand Down
56 changes: 56 additions & 0 deletions py/string_search_bounds_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2026 The go-python Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package py

import (
"fmt"
"testing"
)

func TestStringSearchBounds(t *testing.T) {
// Expected results were checked against CPython.
for _, tc := range []struct {
s, sub string
start, end, find, count int
}{
{"abcabc", "a", -3, 6, 3, 1},
{"abcabc", "a", 0, -1, 0, 2},
{"abcabc", "c", 0, -1, 2, 1},
{"abcabc", "a", -100, 100, 0, 2},
{"abcabc", "a", 0, -100, -1, 0},
{"abcabc", "", 7, 100, -1, 0},
{"abcabc", "", 4, 2, -1, 0},
{"abcabc", "", 6, 6, 6, 1},
{"abcabc", "", 0, -1, 0, 6},
{"é世界é", "é", -1, 4, 3, 1},
{"é世界é", "界", 0, -1, 2, 1},
{"é世界é", "", -3, -1, 1, 3},
{"", "", 0, 0, 0, 1},
{"", "", 1, 1, -1, 0},
} {
for _, method := range []string{"find", "count"} {
t.Run(fmt.Sprintf("%s/%q/%q/%d:%d", method, tc.s, tc.sub, tc.start, tc.end), func(t *testing.T) {
defer func() {
if p := recover(); p != nil {
t.Errorf("unexpected panic: %v", p)
}
}()
args := Tuple{String(tc.sub), Int(tc.start), Int(tc.end)}
var got Object
var err error
want := tc.find
if method == "find" {
got, err = String(tc.s).find(args)
} else {
got, err = String(tc.s).Count(args)
want = tc.count
}
if err != nil || got != Int(want) {
t.Errorf("got %v, %v; want %d", got, err, want)
}
})
}
}
}
8 changes: 8 additions & 0 deletions py/tests/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -955,3 +955,11 @@ def __index__(self):


doc="finished"

# Search bounds count Unicode characters and accept negative indices.
assert 'abcabc'.find('a', -3) == 3
assert 'abcabc'.count('c', 0, -1) == 1
assert 'abc'.find('', 4) == -1
assert 'abc'.count('', 4) == 0
assert 'é世界é'.find('é', -1) == 3
assert 'é世界é'.count('', -3, -1) == 3