diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..de7afb1 --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,31 @@ +name: golangci-lint +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: +permissions: + contents: read + # Optional: allow read access to pull request. Use with `only-new-issues` option. + # pull-requests: read +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + go: [ '1.22' ] + steps: + - uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go }} + cache: false + - uses: actions/checkout@v4 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.55.1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b6677c5 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,40 @@ +name: Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + + build: + name: Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + go: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23' ] + steps: + + - name: Set up Go 1.x + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go }} + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v4 + + - name: Test + run: make tests + + - name: Test coverage + run: make code-coverage + + - name: Send coverage + uses: shogo82148/actions-goveralls@v1 + with: + path-to-profile: coverage.out + flag-name: Go-${{ matrix.go }} + continue-on-error: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1f7bfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +/coverage.out diff --git a/LICENSE b/LICENSE index 2b24d49..3bd6916 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 go-slice +Copyright (c) 2024 Bartlomiej Krukowski Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8e25b9a --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +tests: + go test -race -count=1 -coverprofile=coverage.out ./... + +code-coverage: + go tool cover -func=coverage.out diff --git a/benchmark_test.go b/benchmark_test.go new file mode 100644 index 0000000..aeac538 --- /dev/null +++ b/benchmark_test.go @@ -0,0 +1,36 @@ +package slice_test + +import ( + "testing" + + "github.com/go-slice/slice" +) + +func BenchmarkSlice_Unshift(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + s := slice.FromRaw(make([]int, 5, 10)) + s.Unshift(1, 2) + } +} + +func BenchmarkSlice_Unshift_native(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + s := make([]int, 5, 10) + s = append(append([]int(nil), 1, 2), s...) //nolint:ineffassign,staticcheck + } +} + +func BenchmarkSlice_Filter(b *testing.B) { + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + s := slice.FromRaw([]int{1, 2, 3, 4, 5}) + s.Filter(func(_ int, val int) bool { + return val%2 == 0 + }) + } +} diff --git a/clear_polyfill.go b/clear_polyfill.go new file mode 100644 index 0000000..941cb39 --- /dev/null +++ b/clear_polyfill.go @@ -0,0 +1,9 @@ +package slice + +func clear[T any, S ~[]T](in S) { + var x T + + for i := 0; i < len(in); i++ { + in[i] = x + } +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..f02ec93 --- /dev/null +++ b/example_test.go @@ -0,0 +1,306 @@ +package slice_test + +import ( + "fmt" + "math/rand" + + "github.com/go-slice/slice" +) + +func Example() { + var s slice.Slice[int] + + s.Push(4, 5, 6) + s.Unshift(1, 2, 3) + s.DeleteOne(0) + s.Pop() + + fmt.Println(s) + + // Output: [2 3 4 5] +} + +func ExampleSlice_Raw_nil() { + s := slice.Slice[int](nil) + fmt.Println(s.Raw() == nil) + + // Output: true +} + +func ExampleSlice_Raw() { + data := []int{1, 2, 3} + s := slice.Slice[int](data) + + fmt.Println(s.Raw()) + // Output: [1 2 3] +} + +func ExampleSlice_Empty() { + s1 := slice.FromRaw(make([]int, 0)) + s2 := slice.Slice[int](nil) + s3 := slice.Slice[int]([]int{1}) + + s3.Pop() + + fmt.Println(s1.Empty()) + fmt.Println(s2.Empty()) + fmt.Println(s3.Empty()) + + // Output: + // true + // true + // true +} + +func ExampleSlice_Shift() { + s := slice.FromRaw([]int{1, 2, 3}) + fmt.Println(s.Shift()) + fmt.Println(s) + + // Output: + // 1 true + // [2 3] +} + +func ExampleSlice_Shift_nil() { + s := slice.Slice[int](nil) + fmt.Println(s.Shift()) + + // Output: + // 0 false +} + +func ExampleSlice_Unshift() { + s := slice.FromRaw([]int{4, 5, 6}) + s.Unshift(2, 3) + s.Unshift(1) + s.Unshift() // do nothing + + fmt.Println(s) + + // Output: [1 2 3 4 5 6] +} + +func ExampleSlice_Unshift_nil() { + s := slice.FromRaw[int](nil) + s.Unshift(1, 2, 3) + + fmt.Println(s) + + // Output: [1 2 3] +} + +func ExampleSlice_Pop() { + s := slice.FromRaw([]int{1, 2}) + fmt.Println(s.Pop()) + fmt.Println(s.Pop()) + fmt.Println(s.Pop()) + + // Output: + // 2 true + // 1 true + // 0 false +} + +func ExampleSlice_Push() { + s := slice.FromRaw([]int{1, 2}) + s.Push(3) + s.Push(4, 5) + fmt.Println(s) + + // Output: + // [1 2 3 4 5] +} + +func ExampleSlice_Push_nil() { + s := slice.FromRaw[int](nil) + s.Push(1, 2, 3) + fmt.Println(s) + + // Output: + // [1 2 3] +} + +func ExampleSlice_DeleteOne() { + s := slice.FromRaw([]string{"one", "two", "three"}) + fmt.Println(s.DeleteOne(3)) // no element under the index 3 + fmt.Println(s.DeleteOne(1)) + fmt.Println(s) + + // Output: + // false + // true + // [one three] +} + +func ExampleSlice_Delete() { + s := slice.FromRaw([]string{"one", "two", "three", "four", "five", "six"}) + fmt.Println(s.Delete(0, 1)) // [two three four five six] + fmt.Println(s.Delete(4, 1)) // [two three four five] + fmt.Println(s.Delete(1, 2)) // [two five] + fmt.Println(s.Delete(0, 3)) // [two five] - do nothing, invalid input + fmt.Println(s) + + // Output: + // true + // true + // true + // false + // [two five] +} + +func ExampleSlice_Insert() { + s := slice.FromRaw([]string{"one", "four"}) + s.Insert(1, "two", "three") + fmt.Println(s) + + // Output: + // [one two three four] +} + +func ExampleSlice_Insert_false() { + s := slice.FromRaw[string](nil) + fmt.Println(s.Insert(1, "one")) // the highest possible index to insert == len(s) + + s = slice.FromRaw([]string{"zero", "one", "two"}) + fmt.Println(s.Insert(-1, "minus one")) // index MUST be >= 0 + + // Output: + // false + // false +} + +func ExampleSlice_Clone() { + original := make([]int, 2, 10) + original[0] = 1 + original[1] = 2 + + s := slice.FromRaw(original) + + clone := s.Clone() + + // modify the original slice + s.DeleteOne(1) + s.Push(5) + + fmt.Println(original) + fmt.Println(clone) // clone remains unchanged + + // Output: + // [1 5] + // [1 2] +} + +func ExampleSlice_Clone_nil() { + s := slice.FromRaw[int](nil) + fmt.Println(s.Clone() == nil) + + // Output: true +} + +func ExampleSlice_Cap() { + s1 := slice.Slice[int](nil) + s2 := slice.FromRaw(make([]int, 0, 100)) + + fmt.Println(s1.Cap()) + fmt.Println(s2.Cap()) + + // Output: + // 0 + // 100 +} + +func ExampleSlice_Len() { + s1 := slice.Slice[int](nil) + s2 := slice.FromRaw(make([]int, 100)) + + fmt.Println(s1.Len()) + fmt.Println(s2.Len()) + + // Output: + // 0 + // 100 +} + +func ExampleSlice_ExtendCap() { + s := slice.FromRaw(make([]int, 0, 100)) + s.ExtendCap(10) + + fmt.Println(s.Cap()) + + // Output: 110 +} + +func ExampleSlice_Filter() { + x := slice.FromRaw([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) + x.Filter(func(_ int, val int) bool { + return val%2 == 0 + }) + + fmt.Println(x) + + // Output: + // [2 4 6 8 10] +} + +func ExampleSlice_Filter_nil() { + x := slice.FromRaw[int](nil) + x.Filter(func(index int, val int) bool { // do nothing + return true + }) + fmt.Println(x.Raw() == nil) + + // Output: true +} + +func ExampleSlice_Get() { + x := slice.FromRaw([]int{1, 2, 3, 4, 5}) + fmt.Println(x.Get(0)) // 1 true + fmt.Println(x.Get(4)) // 5 true + fmt.Println(x.Get(-1)) // 5 true + fmt.Println(x.Get(-5)) // 1 true + fmt.Println(x.Get(-6)) // 0 false + fmt.Println(x.Get(5)) // 0 false + + // Output: + // 1 true + // 5 true + // 5 true + // 1 true + // 0 false + // 0 false +} + +func ExampleSlice_Reverse() { + s := slice.FromRaw([]int{5, 4, 3, 2, 1}) + s.Reverse() + fmt.Println(s) + + // Output: [1 2 3 4 5] +} + +func ExampleSlice_Sort() { + s := slice.FromRaw([]int{3, 2, 5, 4, 1}) + s.Sort(func(a int, b int) int { + return a - b + }) + fmt.Println(s) + + // Output: [1 2 3 4 5] +} + +func ExampleSlice_Sort_nil() { + var s slice.Slice[int] + s.Sort(func(a int, b int) int { // do nothing, there is nothing to sort + return a - b + }) + fmt.Println(s.Raw() == nil) + + // Output: true +} + +func ExampleSlice_Shuffle() { + s := slice.FromRaw([]int{1, 2, 3, 4, 5}) + s.Shuffle(rand.Intn) + fmt.Println(s) // e.g. [3 1 5 4 2] +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ff80a3f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/go-slice/slice + +go 1.18 diff --git a/slice.go b/slice.go new file mode 100644 index 0000000..869056b --- /dev/null +++ b/slice.go @@ -0,0 +1,233 @@ +package slice + +// Slice is a wrapper of any slice that allows performing basic operations over slices using an intuitive syntax. +// +// var s slice.Slice[int] +// s.Push(4, 5, 6) // [4 5 6] +// s.Unshift(1, 2, 3) // [1 2 3 4 5 6] +// s.Pop() // [1 2 3 4 5] +type Slice[T any] []T + +// FromRaw creates a new [Slice]. +func FromRaw[T any](in []T) Slice[T] { + return in +} + +// Raw returns the underlying slice. +func (s *Slice[T]) Raw() []T { + return *s +} + +// Empty returns false when the len of the given slice equals 0. +func (s *Slice[T]) Empty() bool { + return len(*s) == 0 +} + +// Shift returns the first element and removes it from the given slice. +func (s *Slice[T]) Shift() (_ T, ok bool) { + var r T + + if s.Empty() { + return r, false + } + + start := *s + defer clear(start[0:1]) + + r, *s = (*s)[0], (*s)[1:] + + return r, true +} + +// Unshift prepends the given input to the given slice. +func (s *Slice[T]) Unshift(v ...T) { + if len(v) == 0 { + return + } + + if *s == nil { + *s = make(Slice[T], len(v)) + copy(*s, v) + + return + } + + *s = append(*s, v...) + + copy( + (*s)[len(v):], + (*s)[:len(*s)-len(v)], + ) + copy( + (*s)[:len(v)], + v, + ) +} + +// Pop returns the last element and removes it from the given slice. +func (s *Slice[T]) Pop() (_ T, ok bool) { + var r T + + if s.Empty() { + return r, false + } + + start := *s + defer clear(start[len(start)-1:]) + + r, *s = (*s)[len(*s)-1], (*s)[:len(*s)-1] + + return r, true +} + +// Push appends the given input to the given slice. +func (s *Slice[T]) Push(v ...T) { + if *s == nil { + *s = make([]T, 0, len(v)) + } + + *s = append(*s, v...) +} + +// DeleteOne deletes a single element from the given slice. +// +// s.DeleteOne(index) // it's an equivalent of s.Delete(index, 1) +func (s *Slice[T]) DeleteOne(index int) (ok bool) { + return s.Delete(index, 1) +} + +// Delete deletes a vector of the given length under the given index from the given slice. +// +// s := slice.FromRaw([]int{1, 2, 3, 4, 5}) +// s.Delete(1, 3) +// fmt.Println(s) [1 5] +func (s *Slice[T]) Delete(index int, length int) (ok bool) { + if index < 0 || *s == nil || index+length > len(*s) { + return false + } + + start := *s + defer clear(start[len(start)-length:]) + + *s = append((*s)[:index], (*s)[index+length:]...) + + return true +} + +// Insert inserts the given element to the existing slice under the given index. +// +// s := slice.FromRaw([]string{"one", "four"}) +// s.Insert(1 "two", "three") +// fmt.Println(s) // ["one", "two", "three", "four"] +func (s *Slice[T]) Insert(index int, v ...T) (ok bool) { + if index < 0 || index > len(*s) { + return false + } + + *s = append(*s, v...) + copy( + (*s)[index+len(v):], + (*s)[index:index+len(v)], + ) + copy( + (*s)[index:index+len(v)], + v, + ) + + return true +} + +// Clone returns a new slice with the same length and copies to it all the elements from the existing slice. +func (s *Slice[T]) Clone() Slice[T] { + if *s == nil { + return nil + } + + r := make(Slice[T], s.Len()) + copy(r, *s) + + return r +} + +// Cap returns the capacity of the given slice. +func (s *Slice[T]) Cap() int { + return cap(*s) +} + +// Len returns the length of the given slice. +func (s *Slice[T]) Len() int { + return len(*s) +} + +// ExtendCap extends the capacity of the given slice. +func (s *Slice[T]) ExtendCap(i int) { + n := make(Slice[T], len(*s), cap(*s)+i) + copy(n, *s) + *s = n +} + +// Filter filters the given slice using the provided func. +func (s *Slice[T]) Filter(keep func(index int, val T) bool) { + if *s == nil { + return + } + + n := (*s)[:0] + + for i, x := range *s { + if keep(i, x) { + n = append(n, x) + } + } + + clear((*s)[len(n):]) + + *s = n +} + +// Get returns an element under the given index. +// It accepts negative indexes. +// +// x := slice.FromRaw([]int{1, 2, 3, 4, 5}) +// val, ok := x.Get(-1) +// fmt.Println(val) // 5 +func (s *Slice[T]) Get(index int) (_ T, ok bool) { + if index < 0 { + index += len(*s) + } + + if index < 0 || index >= len(*s) { + var r T + + return r, false + } + + return (*s)[index], true +} + +// Reverse reverses order of the given slice. +func (s *Slice[T]) Reverse() { + for i := len(*s)/2 - 1; i >= 0; i-- { + j := len(*s) - 1 - i + (*s)[i], (*s)[j] = (*s)[j], (*s)[i] + } +} + +// Sort sorts the given slice in ascending order as determined by the cmp function. +// It requires that cmp is a strict weak ordering. +// See https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings. +func (s *Slice[T]) Sort(cmp func(a T, b T) int) { + sort(*s, cmp) +} + +// Shuffle shuffles the given input. +// randIntN must generate a pseudo-random number in the half-open interval [0,n). +// +// s := slice.FromRaw([]int{1, 2, 3, 4, 5}) +// s.Shuffle(rand.Intn) +func (s *Slice[T]) Shuffle(randIntN func(n int) int) { + for i := len(*s) - 1; i > 0; i-- { + j := randIntN(i + 1) + (*s)[i], (*s)[j] = (*s)[j], (*s)[i] + } +} diff --git a/slice_sort_1.21.go b/slice_sort_1.21.go new file mode 100644 index 0000000..24cfe72 --- /dev/null +++ b/slice_sort_1.21.go @@ -0,0 +1,12 @@ +//go:build go1.21 +// +build go1.21 + +package slice + +import ( + "slices" +) + +func sort[T any](s []T, cmp func(a T, b T) int) { + slices.SortStableFunc(s, cmp) +} diff --git a/slice_sort_old.go b/slice_sort_old.go new file mode 100644 index 0000000..769d344 --- /dev/null +++ b/slice_sort_old.go @@ -0,0 +1,14 @@ +//go:build !go1.21 +// +build !go1.21 + +package slice + +import ( + pkgSort "sort" +) + +func sort[T any](s []T, cmp func(a T, b T) int) { + pkgSort.SliceStable(s, func(i, j int) bool { + return cmp(s[i], s[j]) < 0 + }) +} diff --git a/slice_test.go b/slice_test.go new file mode 100644 index 0000000..d6d1a33 --- /dev/null +++ b/slice_test.go @@ -0,0 +1,80 @@ +package slice_test + +import ( + "math/rand" + "reflect" + "runtime" + "testing" + + "github.com/go-slice/slice" +) + +func Test(t *testing.T) { + data := make([]int, 0, 11) + + s := slice.FromRaw(data) // [] + s.Push(2, 3, 3) // [2 3 3] + s.DeleteOne(2) // [2 3] + s.Unshift(1) // [1 2 3] + s.Unshift(0) // [0 1 2 3] + s.Shift() // [1 2 3] + s.Push(4, 4) // [1 2 3 4 4] + s.Pop() // [1 2 3 4] + s.Push(20) // [1 2 3 4 20] + s.Filter(func(_ int, val int) bool { // [1 2 3 4] + return val != 20 + }) + s.Push(5, 6, 7, 8, 9, 10) // [1 2 3 4 5 6 7 8 9 10] + + expected := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} + + for _, result := range [][]int{data[1:11], s.Raw()} { + if !reflect.DeepEqual(expected, result) { + t.Error("expected != result", result) + } + } +} + +func heapAlloc() uint64 { + runtime.GC() + + var m runtime.MemStats + + runtime.ReadMemStats(&m) + + return m.HeapAlloc +} + +func TestSlice_Clone(t *testing.T) { + s := slice.FromRaw(make([]int64, 0, 1024*1024)) + before := heapAlloc() + s = s.Clone() + after := heapAlloc() + + var expected float64 = 1024 * 1024 * 8 * .9 + + if float64(before-after) < expected { + t.Error("Memory usage has not decreased") + } + + _ = s +} + +func TestSlice_Shuffle(t *testing.T) { + in := slice.FromRaw([]int{1, 2, 3, 4, 5}) + notExpected := []int{1, 2, 3, 4, 5} + + differs := false + for i := 0; i < 10; i++ { + in.Shuffle(rand.Intn) + if !reflect.DeepEqual(notExpected, in.Raw()) { + differs = true + + break + } + } + + if !differs { + t.Error("Shuffle does not work") + } +}