Skip to content

Commit d2f35f3

Browse files
elibeneric
authored andcommitted
slices: add sorting and comparison functions
Now that the `cmp` package exists, sorting and comparison functions from `x/exp/slices` can be ported to the standard library, using the `cmp.Ordered` type and the `cmp.Less` and `cmp.Compare` functions. This move also includes adjustments to the discussions in golang#60091 w.r.t. NaN handling and cmp vs. less functions, and adds Min/Max functions. The final API is taken from golang#60091 (comment) Updates golang#60091 Change-Id: Id7e6c88035b60d4ddd0c48dd82add8e8bc4e22d3 Reviewed-on: https://go-review.googlesource.com/c/go/+/496078 Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Eli Bendersky <[email protected]> Run-TryBot: Eli Bendersky‎ <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 4d9ee36 commit d2f35f3

10 files changed

+2080
-11
lines changed

api/next/60091.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
pkg slices, func BinarySearchFunc[$0 interface{}, $1 interface{}]([]$0, $1, func($0, $1) int) (int, bool) #60091
2+
pkg slices, func BinarySearch[$0 cmp.Ordered]([]$0, $0) (int, bool) #60091
3+
pkg slices, func CompareFunc[$0 interface{}, $1 interface{}]([]$0, []$1, func($0, $1) int) int #60091
4+
pkg slices, func Compare[$0 cmp.Ordered]([]$0, []$0) int #60091
5+
pkg slices, func IsSortedFunc[$0 interface{}]([]$0, func($0, $0) int) bool #60091
6+
pkg slices, func IsSorted[$0 cmp.Ordered]([]$0) bool #60091
7+
pkg slices, func MaxFunc[$0 interface{}]([]$0, func($0, $0) int) $0 #60091
8+
pkg slices, func Max[$0 cmp.Ordered]([]$0) $0 #60091
9+
pkg slices, func MinFunc[$0 interface{}]([]$0, func($0, $0) int) $0 #60091
10+
pkg slices, func Min[$0 cmp.Ordered]([]$0) $0 #60091
11+
pkg slices, func SortFunc[$0 interface{}]([]$0, func($0, $0) int) #60091
12+
pkg slices, func SortStableFunc[$0 interface{}]([]$0, func($0, $0) int) #60091
13+
pkg slices, func Sort[$0 cmp.Ordered]([]$0) #60091

src/go/build/deps_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ var depsRules = `
4949
unicode/utf8, unicode/utf16, unicode,
5050
unsafe;
5151
52-
# slices depends on unsafe for overlapping check.
53-
unsafe
54-
< slices;
55-
5652
# These packages depend only on internal/goarch and unsafe.
5753
internal/goarch, unsafe
5854
< internal/abi;
@@ -226,6 +222,11 @@ var depsRules = `
226222
< hash
227223
< hash/adler32, hash/crc32, hash/crc64, hash/fnv, hash/maphash;
228224
225+
# slices depends on unsafe for overlapping check, cmp for comparison
226+
# semantics, and math/bits for # calculating bitlength of numbers.
227+
unsafe, cmp, math/bits
228+
< slices;
229+
229230
# math/big
230231
FMT, encoding/binary, math/rand
231232
< math/big;

src/slices/slices.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package slices
77

88
import (
9+
"cmp"
910
"unsafe"
1011
)
1112

@@ -44,6 +45,50 @@ func EqualFunc[E1, E2 any](s1 []E1, s2 []E2, eq func(E1, E2) bool) bool {
4445
return true
4546
}
4647

48+
// Compare compares the elements of s1 and s2, using [cmp.Compare] on each pair
49+
// of elements. The elements are compared sequentially, starting at index 0,
50+
// until one element is not equal to the other.
51+
// The result of comparing the first non-matching elements is returned.
52+
// If both slices are equal until one of them ends, the shorter slice is
53+
// considered less than the longer one.
54+
// The result is 0 if s1 == s2, -1 if s1 < s2, and +1 if s1 > s2.
55+
func Compare[E cmp.Ordered](s1, s2 []E) int {
56+
for i, v1 := range s1 {
57+
if i >= len(s2) {
58+
return +1
59+
}
60+
v2 := s2[i]
61+
if c := cmp.Compare(v1, v2); c != 0 {
62+
return c
63+
}
64+
}
65+
if len(s1) < len(s2) {
66+
return -1
67+
}
68+
return 0
69+
}
70+
71+
// CompareFunc is like Compare but uses a custom comparison function on each
72+
// pair of elements.
73+
// The result is the first non-zero result of cmp; if cmp always
74+
// returns 0 the result is 0 if len(s1) == len(s2), -1 if len(s1) < len(s2),
75+
// and +1 if len(s1) > len(s2).
76+
func CompareFunc[E1, E2 any](s1 []E1, s2 []E2, cmp func(E1, E2) int) int {
77+
for i, v1 := range s1 {
78+
if i >= len(s2) {
79+
return +1
80+
}
81+
v2 := s2[i]
82+
if c := cmp(v1, v2); c != 0 {
83+
return c
84+
}
85+
}
86+
if len(s1) < len(s2) {
87+
return -1
88+
}
89+
return 0
90+
}
91+
4792
// Index returns the index of the first occurrence of v in s,
4893
// or -1 if not present.
4994
func Index[E comparable](s []E, v E) int {

src/slices/slices_test.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package slices
66

77
import (
8+
"cmp"
89
"internal/race"
910
"internal/testenv"
1011
"math"
@@ -134,6 +135,213 @@ func BenchmarkEqualFunc_Large(b *testing.B) {
134135
}
135136
}
136137

138+
var compareIntTests = []struct {
139+
s1, s2 []int
140+
want int
141+
}{
142+
{
143+
[]int{1},
144+
[]int{1},
145+
0,
146+
},
147+
{
148+
[]int{1},
149+
[]int{},
150+
1,
151+
},
152+
{
153+
[]int{},
154+
[]int{1},
155+
-1,
156+
},
157+
{
158+
[]int{},
159+
[]int{},
160+
0,
161+
},
162+
{
163+
[]int{1, 2, 3},
164+
[]int{1, 2, 3},
165+
0,
166+
},
167+
{
168+
[]int{1, 2, 3},
169+
[]int{1, 2, 3, 4},
170+
-1,
171+
},
172+
{
173+
[]int{1, 2, 3, 4},
174+
[]int{1, 2, 3},
175+
+1,
176+
},
177+
{
178+
[]int{1, 2, 3},
179+
[]int{1, 4, 3},
180+
-1,
181+
},
182+
{
183+
[]int{1, 4, 3},
184+
[]int{1, 2, 3},
185+
+1,
186+
},
187+
{
188+
[]int{1, 4, 3},
189+
[]int{1, 2, 3, 8, 9},
190+
+1,
191+
},
192+
}
193+
194+
var compareFloatTests = []struct {
195+
s1, s2 []float64
196+
want int
197+
}{
198+
{
199+
[]float64{},
200+
[]float64{},
201+
0,
202+
},
203+
{
204+
[]float64{1},
205+
[]float64{1},
206+
0,
207+
},
208+
{
209+
[]float64{math.NaN()},
210+
[]float64{math.NaN()},
211+
0,
212+
},
213+
{
214+
[]float64{1, 2, math.NaN()},
215+
[]float64{1, 2, math.NaN()},
216+
0,
217+
},
218+
{
219+
[]float64{1, math.NaN(), 3},
220+
[]float64{1, math.NaN(), 4},
221+
-1,
222+
},
223+
{
224+
[]float64{1, math.NaN(), 3},
225+
[]float64{1, 2, 4},
226+
-1,
227+
},
228+
{
229+
[]float64{1, math.NaN(), 3},
230+
[]float64{1, 2, math.NaN()},
231+
-1,
232+
},
233+
{
234+
[]float64{1, 2, 3},
235+
[]float64{1, 2, math.NaN()},
236+
+1,
237+
},
238+
{
239+
[]float64{1, 2, 3},
240+
[]float64{1, math.NaN(), 3},
241+
+1,
242+
},
243+
{
244+
[]float64{1, math.NaN(), 3, 4},
245+
[]float64{1, 2, math.NaN()},
246+
-1,
247+
},
248+
}
249+
250+
func TestCompare(t *testing.T) {
251+
intWant := func(want bool) string {
252+
if want {
253+
return "0"
254+
}
255+
return "!= 0"
256+
}
257+
for _, test := range equalIntTests {
258+
if got := Compare(test.s1, test.s2); (got == 0) != test.want {
259+
t.Errorf("Compare(%v, %v) = %d, want %s", test.s1, test.s2, got, intWant(test.want))
260+
}
261+
}
262+
for _, test := range equalFloatTests {
263+
if got := Compare(test.s1, test.s2); (got == 0) != test.wantEqualNaN {
264+
t.Errorf("Compare(%v, %v) = %d, want %s", test.s1, test.s2, got, intWant(test.wantEqualNaN))
265+
}
266+
}
267+
268+
for _, test := range compareIntTests {
269+
if got := Compare(test.s1, test.s2); got != test.want {
270+
t.Errorf("Compare(%v, %v) = %d, want %d", test.s1, test.s2, got, test.want)
271+
}
272+
}
273+
for _, test := range compareFloatTests {
274+
if got := Compare(test.s1, test.s2); got != test.want {
275+
t.Errorf("Compare(%v, %v) = %d, want %d", test.s1, test.s2, got, test.want)
276+
}
277+
}
278+
}
279+
280+
func equalToCmp[T comparable](eq func(T, T) bool) func(T, T) int {
281+
return func(v1, v2 T) int {
282+
if eq(v1, v2) {
283+
return 0
284+
}
285+
return 1
286+
}
287+
}
288+
289+
func TestCompareFunc(t *testing.T) {
290+
intWant := func(want bool) string {
291+
if want {
292+
return "0"
293+
}
294+
return "!= 0"
295+
}
296+
for _, test := range equalIntTests {
297+
if got := CompareFunc(test.s1, test.s2, equalToCmp(equal[int])); (got == 0) != test.want {
298+
t.Errorf("CompareFunc(%v, %v, equalToCmp(equal[int])) = %d, want %s", test.s1, test.s2, got, intWant(test.want))
299+
}
300+
}
301+
for _, test := range equalFloatTests {
302+
if got := CompareFunc(test.s1, test.s2, equalToCmp(equal[float64])); (got == 0) != test.wantEqual {
303+
t.Errorf("CompareFunc(%v, %v, equalToCmp(equal[float64])) = %d, want %s", test.s1, test.s2, got, intWant(test.wantEqual))
304+
}
305+
}
306+
307+
for _, test := range compareIntTests {
308+
if got := CompareFunc(test.s1, test.s2, cmp.Compare[int]); got != test.want {
309+
t.Errorf("CompareFunc(%v, %v, cmp[int]) = %d, want %d", test.s1, test.s2, got, test.want)
310+
}
311+
}
312+
for _, test := range compareFloatTests {
313+
if got := CompareFunc(test.s1, test.s2, cmp.Compare[float64]); got != test.want {
314+
t.Errorf("CompareFunc(%v, %v, cmp[float64]) = %d, want %d", test.s1, test.s2, got, test.want)
315+
}
316+
}
317+
318+
s1 := []int{1, 2, 3}
319+
s2 := []int{2, 3, 4}
320+
if got := CompareFunc(s1, s2, equalToCmp(offByOne)); got != 0 {
321+
t.Errorf("CompareFunc(%v, %v, offByOne) = %d, want 0", s1, s2, got)
322+
}
323+
324+
s3 := []string{"a", "b", "c"}
325+
s4 := []string{"A", "B", "C"}
326+
if got := CompareFunc(s3, s4, strings.Compare); got != 1 {
327+
t.Errorf("CompareFunc(%v, %v, strings.Compare) = %d, want 1", s3, s4, got)
328+
}
329+
330+
compareLower := func(v1, v2 string) int {
331+
return strings.Compare(strings.ToLower(v1), strings.ToLower(v2))
332+
}
333+
if got := CompareFunc(s3, s4, compareLower); got != 0 {
334+
t.Errorf("CompareFunc(%v, %v, compareLower) = %d, want 0", s3, s4, got)
335+
}
336+
337+
cmpIntString := func(v1 int, v2 string) int {
338+
return strings.Compare(string(rune(v1)-1+'a'), v2)
339+
}
340+
if got := CompareFunc(s1, s3, cmpIntString); got != 0 {
341+
t.Errorf("CompareFunc(%v, %v, cmpIntString) = %d, want 0", s1, s3, got)
342+
}
343+
}
344+
137345
var indexTests = []struct {
138346
s []int
139347
v int

0 commit comments

Comments
 (0)