From 8fae9457d0733a37d117889b3bc4f1ef1cbf56b3 Mon Sep 17 00:00:00 2001 From: Matthew Edge Date: Mon, 4 Dec 2023 16:48:27 -0500 Subject: [PATCH] slice.Remove added as a safer alternative to built-in slices.Delete --- slice/remove.go | 23 +++++++++ slice/remove_test.go | 108 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 slice/remove.go create mode 100644 slice/remove_test.go diff --git a/slice/remove.go b/slice/remove.go new file mode 100644 index 0000000..5c9ffc1 --- /dev/null +++ b/slice/remove.go @@ -0,0 +1,23 @@ +package slice + +// Remove removes the given index range from the given slice, preserving element order. +// It is the safer version of 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 for emtpy slices or starting indecies outside range + if len(slice) == 0 || i > len(slice) { + return slice + } + // Prevent invalid slice indexing + if i < 0 || j > len(slice) { + return slice + } + // Removing the last element is a simple re-slice + if i == len(slice)-1 && j >= len(slice) { + return slice[:i] + } + // Note: this modifies the slice + return append(slice[:i], slice[j:]...) +} diff --git a/slice/remove_test.go b/slice/remove_test.go new file mode 100644 index 0000000..74b8d51 --- /dev/null +++ b/slice/remove_test.go @@ -0,0 +1,108 @@ +package slice_test + +import ( + "fmt" + "testing" + + "github.com/mailgun/holster/v4/slice" + "github.com/stretchr/testify/assert" +) + +func ExampleRemove() { + s := []string{"1", "2", "3"} + i := []int{1, 2, 3} + + fmt.Println(slice.Remove(s, 0, 2)) + fmt.Println(slice.Remove(i, 1, 2)) + // Output: + // [3] + // [1 3] +} + +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: "nil check", + i: 1, + j: 3, + inSlice: nil, + outSlice: nil, + }, { + name: "start index out of range should not panic or modify the slice", + i: -2, + j: 2, + inSlice: []int{2}, + outSlice: []int{2}, + }, { + name: "end index out of range should not panic or modify the slice", + i: 0, + j: 5, + inSlice: []int{2}, + outSlice: []int{2}, + }, { + name: "invalid start and end index should not panic or modify the slice", + i: 4, + j: 7, + inSlice: []int{2}, + outSlice: []int{2}, + }, { + name: "single value remove", + i: 0, + j: 1, + inSlice: []int{1, 2, 3}, + outSlice: []int{2, 3}, + }, { + name: "middle value removed", + i: 1, + j: 2, + inSlice: []int{1, 2, 3}, + outSlice: []int{1, 3}, + }, { + name: "last value removed", + i: 2, + j: 3, + inSlice: []int{1, 2, 3}, + outSlice: []int{1, 2}, + }, { + name: "multi-value remove", + i: 1, + j: 3, + inSlice: []int{1, 2, 3}, + outSlice: []int{1}, + }, { + name: "end of slice gut check for invalid ending index", + i: 2, + j: 5, // This index being out of bounds shouldn't matter + inSlice: []int{1, 2, 3}, + outSlice: []int{1, 2}, + }} { + t.Run(test.name, func(t *testing.T) { + got := slice.Remove(test.inSlice, test.i, test.j) + for i := range test.outSlice { + assert.Equal(t, test.outSlice[i], got[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 = slice.Remove(in, 2, 4) + } + out = o +}