Skip to content

Commit

Permalink
Pull request 124: add-set-and-url
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit 8d87787
Author: Ainar Garipov <[email protected]>
Date:   Fri Nov 1 12:16:54 2024 +0300

    errors: fix type

commit 986169f
Author: Ainar Garipov <[email protected]>
Date:   Thu Oct 31 19:30:53 2024 +0300

    all: add tests, imp code

commit 0393ff0
Author: Ainar Garipov <[email protected]>
Date:   Thu Oct 31 18:45:47 2024 +0300

    all: imp docs, tests

commit ff7d161
Author: Ainar Garipov <[email protected]>
Date:   Thu Oct 31 17:19:00 2024 +0300

    container: add sorted slice set; urlutil: add helpers
  • Loading branch information
ainar-g committed Nov 1, 2024
1 parent e119bb9 commit 0e55d3f
Show file tree
Hide file tree
Showing 21 changed files with 934 additions and 114 deletions.
8 changes: 7 additions & 1 deletion bamboo-specs/bamboo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
make VERBOSE=1 go-tools go-lint
Expand All @@ -70,6 +72,8 @@
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
make VERBOSE=1 go-tools md-lint sh-lint txt-lint
Expand All @@ -89,9 +93,11 @@
'interpreter': 'SHELL'
'scripts':
- |
#!/bin/sh
set -e -f -u -x
make VERBOSE=1 go-deps go-test go-fuzz
make VERBOSE=1 go-deps go-bench go-test go-fuzz
'final-tasks':
- 'clean'
'requirements':
Expand Down
34 changes: 34 additions & 0 deletions container/containter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package container_test

import (
"math/rand"
"time"
)

// Common sinks for benchmarks.
var (
sinkBool bool
)

// Common constants for tests.
const (
randStrLen = 8
setMaxLen = 100_000
)

// newRandStrs returns a slice of random strings of length l with each string
// being strLen bytes long.
func newRandStrs(l, strLen int) (strs []string) {
src := rand.NewSource(time.Now().UnixNano())
rng := rand.New(src)

strs = make([]string, 0, l)
for range l {
data := make([]byte, strLen)
_, _ = rng.Read(data)

strs = append(strs, string(data))
}

return strs
}
16 changes: 5 additions & 11 deletions container/mapset.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func NewMapSet[T comparable](values ...T) (set *MapSet[T]) {
return set
}

// Add adds v to set. Add panics if set is a nil set, just like a nil map does.
// Add adds v to set.
func (set *MapSet[T]) Add(v T) {
set.m[v] = unit{}
}
Expand Down Expand Up @@ -61,21 +61,15 @@ func (set *MapSet[T]) Delete(v T) {
}
}

// Equal returns true if set is equal to other.
// Equal returns true if set is equal to other. set and other may be nil; Equal
// returns true if both are nil, but a nil *MapSet is not equal to a non-nil
// empty one.
func (set *MapSet[T]) Equal(other *MapSet[T]) (ok bool) {
if set == nil || other == nil {
return set == other
} else if set.Len() != other.Len() {
return false
}

for v := range set.m {
if _, ok = other.m[v]; !ok {
return false
}
}

return true
return maps.Equal(set.m, other.m)
}

// Has returns true if v is in set. Calling Has on a nil set returns false,
Expand Down
4 changes: 1 addition & 3 deletions container/mapset_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,7 @@ func ExampleMapSet_nil() {

panicked := false
setPanicked := func() {
if v := recover(); v != nil {
panicked = true
}
panicked = recover() != nil
}

func() {
Expand Down
86 changes: 86 additions & 0 deletions container/mapset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package container_test

import (
"fmt"
"testing"
"time"

"github.com/AdguardTeam/golibs/container"
"github.com/stretchr/testify/require"
)

func BenchmarkMapSet_Add(b *testing.B) {
for n := 10; n <= setMaxLen; n *= 10 {
b.Run(fmt.Sprintf("%d_strings", n), func(b *testing.B) {
var set *container.MapSet[string]

values := newRandStrs(n, randStrLen)

b.ReportAllocs()
b.ResetTimer()
for range b.N {
set = container.NewMapSet[string]()
for _, v := range values {
set.Add(v)
}
}

perIter := b.Elapsed() / time.Duration(b.N)
b.ReportMetric(float64(perIter)/float64(n), "ns/add")

require.True(b, set.Has(values[0]))
})
}

// Most recent results:
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/golibs/container
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkMapSet_Add
// BenchmarkMapSet_Add/10_strings
// BenchmarkMapSet_Add/10_strings-16 702885 1514 ns/op 151.3 ns/add 491 B/op 4 allocs/op
// BenchmarkMapSet_Add/100_strings
// BenchmarkMapSet_Add/100_strings-16 66829 15960 ns/op 159.6 ns/add 5615 B/op 11 allocs/op
// BenchmarkMapSet_Add/1000_strings
// BenchmarkMapSet_Add/1000_strings-16 7400 206750 ns/op 206.7 ns/add 85713 B/op 36 allocs/op
// BenchmarkMapSet_Add/10000_strings
// BenchmarkMapSet_Add/10000_strings-16 592 1982110 ns/op 198.2 ns/add 676473 B/op 216 allocs/op
// BenchmarkMapSet_Add/100000_strings
// BenchmarkMapSet_Add/100000_strings-16 49 23063097 ns/op 230.6 ns/add 5597995 B/op 3903 allocs/op
}

func BenchmarkMapSet_Has(b *testing.B) {
for n := 10; n <= setMaxLen; n *= 10 {
b.Run(fmt.Sprintf("%d_strings", n), func(b *testing.B) {
values := newRandStrs(n, randStrLen)
set := container.NewMapSet(values...)
value := values[n/2]

b.ReportAllocs()
b.ResetTimer()
for range b.N {
sinkBool = set.Has(value)
}

require.True(b, sinkBool)
})
}

// Most recent results:
// goos: linux
// goarch: amd64
// pkg: github.com/AdguardTeam/golibs/container
// cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
// BenchmarkMapSet_Has
// BenchmarkMapSet_Has/10_strings
// BenchmarkMapSet_Has/10_strings-16 171413164 6.886 ns/op 0 B/op 0 allocs/op
// BenchmarkMapSet_Has/100_strings
// BenchmarkMapSet_Has/100_strings-16 166819746 6.607 ns/op 0 B/op 0 allocs/op
// BenchmarkMapSet_Has/1000_strings
// BenchmarkMapSet_Has/1000_strings-16 179336127 6.870 ns/op 0 B/op 0 allocs/op
// BenchmarkMapSet_Has/10000_strings
// BenchmarkMapSet_Has/10000_strings-16 164002748 6.831 ns/op 0 B/op 0 allocs/op
// BenchmarkMapSet_Has/100000_strings
// BenchmarkMapSet_Has/100000_strings-16 170170257 6.518 ns/op 0 B/op 0 allocs/op
}
131 changes: 131 additions & 0 deletions container/sortedsliceset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package container

import (
"cmp"
"fmt"
"slices"
)

// SortedSliceSet is a simple set implementation that has a sorted set of values
// as its underlying storage.
//
// TODO(a.garipov): Consider relaxing the type requirement or adding a version
// with a comparison function.
type SortedSliceSet[T cmp.Ordered] struct {
elems []T
}

// NewSortedSliceSet returns a new *SortedSliceSet. elems must not be modified
// after calling NewSortedSliceSet.
func NewSortedSliceSet[T cmp.Ordered](elems ...T) (set *SortedSliceSet[T]) {
slices.Sort(elems)

return &SortedSliceSet[T]{
elems: elems,
}
}

// Add adds v to set.
func (set *SortedSliceSet[T]) Add(v T) {
i, ok := slices.BinarySearch(set.elems, v)
if !ok {
set.elems = slices.Insert(set.elems, i, v)
}
}

// Clear clears set in a way that retains the internal storage for later reuse
// to reduce allocations. Calling Clear on a nil set has no effect, just like a
// clear on a nil slice doesn't.
func (set *SortedSliceSet[T]) Clear() {
if set != nil {
clear(set.elems)
set.elems = set.elems[:0]
}
}

// Clone returns a clone of set. If set is nil, clone is nil.
//
// NOTE: It calls [slices.Clone] on the underlying storage, so these elements
// are cloned shallowly.
func (set *SortedSliceSet[T]) Clone() (clone *SortedSliceSet[T]) {
if set == nil {
return nil
}

return NewSortedSliceSet(slices.Clone(set.elems)...)
}

// Delete deletes v from set.
func (set *SortedSliceSet[T]) Delete(v T) {
i, ok := slices.BinarySearch(set.elems, v)
if ok {
set.elems = slices.Delete(set.elems, i, i+1)
}
}

// Equal returns true if set is equal to other. set and other may be nil; Equal
// returns true if both are nil, but a nil *SortedSliceSet is not equal to a
// non-nil empty one.
func (set *SortedSliceSet[T]) Equal(other *SortedSliceSet[T]) (ok bool) {
if set == nil || other == nil {
return set == other
}

return slices.Equal(set.elems, other.elems)
}

// Has returns true if v is in set. Calling Has on a nil set returns false,
// just like iterating over a nil or empty slice does.
func (set *SortedSliceSet[T]) Has(v T) (ok bool) {
if set == nil {
return false
}

_, ok = slices.BinarySearch(set.elems, v)

return ok
}

// Len returns the length of set. A nil set has a length of zero, just like an
// nil or empty slice.
func (set *SortedSliceSet[T]) Len() (n int) {
if set == nil {
return 0
}

return len(set.elems)
}

// Range calls f with each value of set in their sorted order. If cont is
// false, Range stops the iteration. Calling Range on a nil *SortedSliceSet has
// no effect.
func (set *SortedSliceSet[T]) Range(f func(v T) (cont bool)) {
if set == nil {
return
}

for _, v := range set.elems {
if !f(v) {
break
}
}
}

// type check
var _ fmt.Stringer = (*SortedSliceSet[int])(nil)

// String implements the [fmt.Stringer] interface for *SortedSliceSet. Calling
// String on a nil *SortedSliceSet does not panic.
func (set *SortedSliceSet[T]) String() (s string) {
return fmt.Sprintf("%v", set.Values())
}

// Values returns the underlying slice of set. values must not be modified.
// Values returns nil if set is nil.
func (set *SortedSliceSet[T]) Values() (values []T) {
if set == nil {
return nil
}

return set.elems
}
Loading

0 comments on commit 0e55d3f

Please sign in to comment.