Skip to content
This repository has been archived by the owner on Apr 19, 2022. It is now read-only.

Commit

Permalink
feature: add intersect and except (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
nrwiersma authored Jun 19, 2020
1 parent 11fe1f4 commit 1c24a99
Show file tree
Hide file tree
Showing 9 changed files with 994 additions and 14 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ cache:
- $GOPATH/pkg/mod

before_install:
- go generate ./... && git diff --exit-code; code=$?; (exit $code)
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $GOPATH/bin
- go mod download
- go get github.com/mattn/goveralls
Expand Down
46 changes: 39 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![GitHub release](https://img.shields.io/github/release/hamba/slices.svg)](https://github.com/hamba/slices/releases)
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/hamba/slices/master/LICENSE)

A fast Go slices codec
Go slice helper functions

## Overview

Expand Down Expand Up @@ -61,15 +61,47 @@ fmt.Println(slice)
// Outputs: [bar foo]
```

### Intersect

Intersect returns the intersection of 2 slices.

Supports: string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64

```go
slice := []string{"foo", "bar", "baz", "bat"}
other := []string{"bar", "baz", "test"}

fmt.Println(slices.Intersect(slice, other))
// Outputs: [bar baz]
```

### Except

Except returns the all elements in the first slice that are not in the second.

Supports: string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64

```go
slice := []string{"foo", "bar", "baz", "bat"}
other := []string{"bar", "baz", "test"}

fmt.Println(slices.Except(slice, other))
// Outputs: [foo bat]
```

## Benchmark

```
BenchmarkContains-8 36659943 30.0 ns/op 0 B/op 0 allocs/op
BenchmarkContainsNative-8 48539482 24.9 ns/op 0 B/op 0 allocs/op
BenchmarkGreaterOf-8 6257299 193 ns/op 80 B/op 3 allocs/op
BenchmarkGreaterOfNative-8 7963461 149 ns/op 64 B/op 2 allocs/op
BenchmarkLesserOf-8 6122317 192 ns/op 80 B/op 3 allocs/op
BenchmarkLesserOfNative-8 7947242 152 ns/op 64 B/op 2 allocs/op
BenchmarkContains-8 35621572 30.5 ns/op 0 B/op 0 allocs/op
BenchmarkContainsNative-8 50106157 23.9 ns/op 0 B/op 0 allocs/op
BenchmarkExcept-8 6070610 200 ns/op 96 B/op 2 allocs/op
BenchmarkExceptNative-8 5933550 193 ns/op 96 B/op 2 allocs/op
BenchmarkGreaterOf-8 6290626 189 ns/op 80 B/op 3 allocs/op
BenchmarkGreaterOfNative-8 8201284 149 ns/op 64 B/op 2 allocs/op
BenchmarkIntersect-8 6012298 196 ns/op 96 B/op 2 allocs/op
BenchmarkIntersectNative-8 6305799 198 ns/op 96 B/op 2 allocs/op
BenchmarkLesserOf-8 6449050 189 ns/op 80 B/op 3 allocs/op
BenchmarkLesserOfNative-8 8077785 149 ns/op 64 B/op 2 allocs/op
```

Always benchmark with your own workload. The result depends heavily on the data input.
16 changes: 16 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,19 @@ func ExampleLesserOf() {
fmt.Println(slice)
// Outputs: [foo baz bar]
}

func ExampleIntersect() {
slice := []string{"foo", "bar", "baz", "bat"}
other := []string{"bar", "baz", "test"}

fmt.Println(slices.Intersect(slice, other))
// Outputs: [bar baz]
}

func ExampleExcept() {
slice := []string{"foo", "bar", "baz", "bat"}
other := []string{"bar", "baz", "test"}

fmt.Println(slices.Except(slice, other))
// Outputs: [foo bat]
}
68 changes: 68 additions & 0 deletions except.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package slices

import (
"unsafe"
)

type exceptFn func(sptr, optr unsafe.Pointer) interface{}

// Except returns a slice with the elements of slice that are not in other.
// When the slices different, slice is returned.
func Except(slice, other interface{}) interface{} {
fn, ok := exceptOf(slice, other)
if fn == nil {
panic("slice is not a supported slice type")
}
if !ok {
panic("other is not the same type as slice")
}

sptr := noescape(ptrOf(slice))
optr := noescape(ptrOf(other))
return fn(sptr, optr)
}

func exceptOf(slice, other interface{}) (exceptFn, bool) {
switch slice.(type) {
case []string:
_, ok := other.([]string)
return stringExcept, ok
case []int:
_, ok := other.([]int)
return intExcept, ok
case []int8:
_, ok := other.([]int8)
return int8Except, ok
case []int16:
_, ok := other.([]int16)
return int16Except, ok
case []int32:
_, ok := other.([]int32)
return int32Except, ok
case []int64:
_, ok := other.([]int64)
return int64Except, ok
case []uint:
_, ok := other.([]uint)
return uintExcept, ok
case []uint8:
_, ok := other.([]uint8)
return uint8Except, ok
case []uint16:
_, ok := other.([]uint16)
return uint16Except, ok
case []uint32:
_, ok := other.([]uint32)
return uint32Except, ok
case []uint64:
_, ok := other.([]uint64)
return uint64Except, ok
case []float32:
_, ok := other.([]float32)
return float32Except, ok
case []float64:
_, ok := other.([]float64)
return float64Except, ok
}
return nil, false
}
153 changes: 153 additions & 0 deletions except_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package slices_test

import (
"testing"

"github.com/hamba/slices"
"github.com/stretchr/testify/assert"
)

func TestExcept(t *testing.T) {
tests := []struct {
name string
a interface{}
b interface{}
want interface{}
}{
{
name: "string contains",
a: []string{"foo", "bar", "baz", "bat"},
b: []string{"bar", "baz", "test"},
want: []string{"foo", "bat"},
},
{
name: "int contains",
a: []int{1, 2, 3, 4},
b: []int{2, 3, 5},
want: []int{1, 4},
},
{
name: "int8 contains",
a: []int8{1, 2, 3, 4},
b: []int8{2, 3, 5},
want: []int8{1, 4},
},
{
name: "int16 contains",
a: []int16{1, 2, 3, 4},
b: []int16{2, 3, 5},
want: []int16{1, 4},
},
{
name: "int32 contains",
a: []int32{1, 2, 3, 4},
b: []int32{2, 3, 5},
want: []int32{1, 4},
},
{
name: "int64 contains",
a: []int64{1, 2, 3, 4},
b: []int64{2, 3, 5},
want: []int64{1, 4},
},
{
name: "uint contains",
a: []uint{1, 2, 3, 4},
b: []uint{2, 3, 5},
want: []uint{1, 4},
},
{
name: "uint8 contains",
a: []uint8{1, 2, 3, 4},
b: []uint8{2, 3, 5},
want: []uint8{1, 4},
},
{
name: "uint16 contains",
a: []uint16{1, 2, 3, 4},
b: []uint16{2, 3, 5},
want: []uint16{1, 4},
},
{
name: "uint32 contains",
a: []uint32{1, 2, 3, 4},
b: []uint32{2, 3, 5},
want: []uint32{1, 4},
},
{
name: "uint64 contains",
a: []uint64{1, 2, 3, 4},
b: []uint64{2, 3, 5},
want: []uint64{1, 4},
},
{
name: "float32 contains",
a: []float32{1, 2, 3, 4},
b: []float32{2, 3, 5},
want: []float32{1, 4},
},
{
name: "float64 contains",
a: []float64{1, 2, 3, 4},
b: []float64{2, 3, 5},
want: []float64{1, 4},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := slices.Except(tt.a, tt.b)

assert.Equal(t, tt.want, got)
})
}
}

func TestExcept_ChecksSliceType(t *testing.T) {
assert.Panics(t, func() {
slices.Except("test", "test")
})
}

func TestExcept_ChecksValType(t *testing.T) {
assert.Panics(t, func() {
slices.Except([]string{"test"}, 1)
})
}

func BenchmarkExcept(b *testing.B) {
a := []string{"foo", "bar", "baz", "bat"}
c := []string{"bar", "baz", "test"}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
slices.Except(a, c)
}
}

func except(slice, other []string) interface{} {
s := make([]string, len(slice))
copy(s, slice)
for i := 0; i < len(s); i++ {
for _, v := range other {
if v == s[i] {
s = append(s[:i], s[i+1:]...)
i--
break
}
}
}
return s
}

func BenchmarkExceptNative(b *testing.B) {
slice := []string{"foo", "bar", "baz", "bat"}
other := []string{"bar", "baz", "test"}

b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
except(slice, other)
}
}
55 changes: 48 additions & 7 deletions internal/gen/gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 1c24a99

Please sign in to comment.