diff --git a/slice/remove.go b/slice/remove.go new file mode 100644 index 0000000..ad8e298 --- /dev/null +++ b/slice/remove.go @@ -0,0 +1,38 @@ +package slice + +// Remove removes the given index range from the given slice, preserving element order. +// It is the safer version of go/x/exp/slices.Delete() as it will +// not panic on invalid slice ranges. Nil and empty slices are also safe. +// Remove modifies the contents of the given slice and performs no allocations +// or copies. +func Remove[T comparable](slice []T, i, j int) []T { + // Nothing to do + if len(slice) == 0 { + return slice + } + // Prevent invalid slice indexing + if i < 0 || j > len(slice)-1 { + return slice + } + // Note: this modifies the slice + return append(slice[:i], slice[j:]...) +} + +// RemoveAll removes all instances of the given value in the given slice, in place, preserving element order. +// RemoveAll modifies the contents of the given slice. +// If value is not present in the slice, or if the slice is nil or empty, the slice is returned unaltered. +// RemoveAll performs no allocations or copies. +// Note: DO NOT use this for long-lived slices as the elements removed are not garbage collected +func RemoveAll[T comparable](slice []T, value T) []T { + // Nothing to do + if len(slice) == 0 { + return slice + } + b := slice[:0] + for _, v := range slice { + if v != value { + b = append(b, v) + } + } + return b +} diff --git a/slice/remove_test.go b/slice/remove_test.go new file mode 100644 index 0000000..9b67e63 --- /dev/null +++ b/slice/remove_test.go @@ -0,0 +1,144 @@ +package slice + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRemove(t *testing.T) { + for _, test := range []struct { + name string + i int + j int + inSlice []int + outSlice []int + }{{ + name: "empty slice", + inSlice: []int{}, + outSlice: []int{}, + }, { + name: "out of range should not panic or modify the slice", + i: 4, + j: 5, + inSlice: []int{2}, + outSlice: []int{2}, + }, { + name: "correct start index but invalid end index", + i: 0, + j: 2, + inSlice: []int{2}, + outSlice: []int{2}, + }, { + name: "single value remove", + i: 0, + j: 1, + inSlice: []int{2}, + outSlice: []int{}, + }, { + name: "middle value removed", + i: 1, + j: 2, + inSlice: []int{1, 2, 3}, + outSlice: []int{1, 3}, + }, { + name: "multi-value remove", + i: 1, + j: 3, + inSlice: []int{1, 2, 3}, + outSlice: []int{1}, + }, { + name: "nil check", + i: 1, + j: 3, + inSlice: nil, + outSlice: nil, + }, {}} { + t.Run(test.name, func(t *testing.T) { + got := Remove(test.inSlice, test.i, test.j) + for i, expected := range test.outSlice { + assert.Equal(t, expected, got[i]) + } + }) + } +} + +func TestRemoveAll(t *testing.T) { + for _, test := range []struct { + name string + remove string + inSlice []string + outSlice []string + }{{ + name: "empty slice", + remove: "localhost", + inSlice: []string{}, + outSlice: []string{}, + }, { + name: "nil check", + remove: "localhost", + inSlice: nil, + outSlice: nil, + }, { + name: "only value should be removed", + remove: "localhost", + inSlice: []string{"localhost"}, + outSlice: []string{}, + }, { + name: "beginning of list", + remove: "localhost", + inSlice: []string{"localhost", "foo"}, + outSlice: []string{"foo"}, + }, { + name: "end of list", + remove: "localhost", + inSlice: []string{"foo", "localhost"}, + outSlice: []string{"foo"}, + }, { + name: "middle of the list", + remove: "localhost", + inSlice: []string{"foo", "localhost", "bar"}, + outSlice: []string{"foo", "bar"}, + }, { + name: "all instances should be removed", + remove: "localhost", + inSlice: []string{"localhost", "foo", "localhost"}, + outSlice: []string{"foo"}, + }, { + name: "order and repeats shouldn't matter", + remove: "localhost", + inSlice: []string{"foo", "localhost", "bar", "localhost", "localhost", "baz.com"}, + outSlice: []string{"foo", "bar", "baz.com"}, + }} { + t.Run(test.name, func(t *testing.T) { + out := RemoveAll(test.inSlice, test.remove) + for i, expected := range test.outSlice { + assert.Equal(t, expected, out[i]) + } + }) + } +} + +var out []string + +func BenchmarkRemove(b *testing.B) { + o := []string{} + in := []string{"localhost", "mg.example.com", "testlabs.io", "localhost", "m.example.com.br", "localhost", "localhost"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + o = Remove(in, 2, 4) + } + out = o +} + +func BenchmarkRemoveAll(b *testing.B) { + o := []string{} + in := []string{"localhost", "mg.example.com", "testlabs.io", "localhost", "m.example.com.br", "localhost", "localhost"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + o = RemoveAll(in, "localhost") + } + out = o +}