├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── collection ├── collection.go └── collection_test.go ├── constraints └── constraints.go ├── datastructs ├── buffers │ └── ring │ │ ├── ring.go │ │ ├── ring_test.go │ │ ├── syncring.go │ │ └── syncring_test.go ├── queue │ ├── queue.go │ ├── queue_test.go │ ├── syncqueue.go │ └── syncqueue_test.go ├── set │ ├── set.go │ ├── set_test.go │ ├── syncset.go │ └── syncset_test.go └── tuple │ ├── synctuple.go │ ├── synctuple_test.go │ ├── tuple.go │ └── tuple_test.go ├── functions └── slices │ ├── equals.go │ ├── equals_test.go │ ├── filter.go │ ├── filter_test.go │ ├── foreach.go │ ├── foreach_test.go │ ├── map.go │ ├── map_test.go │ ├── range.go │ └── range_test.go ├── go.mod ├── go.sum ├── images └── byteforge-banner.png └── internal ├── datastructs ├── buffers │ └── ring │ │ ├── ring.go │ │ └── ring_test.go └── tuple │ ├── tuple.go │ └── tuple_test.go └── functions ├── slices ├── range.go └── range_test.go └── utils ├── utils.go └── utils_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jean-Jacques 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | help: 2 | @echo -e "\nYou can run all tests using:\t\t\t\tmake test\n" 3 | @echo -e "You can benchmark the code using:\t\t\tmake benchmark\n" 4 | @echo -e "You can check test coverage using:\t\t\tmake coverage\n" 5 | @echo -e "You can generate a test coverage report using:\t\tmake report\n" 6 | @echo -e "You can clean everything up using:\t\t\tmake clean\n" 7 | 8 | test: 9 | @go test -v -race -timeout 30s ./... 10 | 11 | benchmark: 12 | @go test -bench=. -benchmem -v ./... 13 | 14 | coverage: 15 | @go test -cover ./... 16 | 17 | report: 18 | @go test -coverprofile=coverage.out ./... 19 | @go tool cover -html=coverage.out 20 | 21 | clean: 22 | rm ./coverage.out -------------------------------------------------------------------------------- /collection/collection.go: -------------------------------------------------------------------------------- 1 | // Package collection provides a fluent, chainable API for performing 2 | // functional-style operations like map, filter, and reduce on slices. 3 | // 4 | // It is inspired by collection helpers from other languages (like Laravel's Collections) 5 | // and works around Go's current generic limitations using reflection. 6 | package collection 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "reflect" 12 | ) 13 | 14 | // Collection represents a wrapper around a slice, allowing chained 15 | // operations like Map, Filter, and Reduce. It holds the underlying data 16 | // and tracks any errors that occur during chained operations. 17 | // 18 | // If any operation in the chain fails, the error is stored in the Collection 19 | // and subsequent operations are skipped until ToSlice or Reduce is called. 20 | type Collection struct { 21 | data any 22 | err error 23 | } 24 | 25 | // FromSlice creates a new Collection from a given slice. 26 | // 27 | // The input must be a slice type; otherwise, the returned Collection will 28 | // carry an error. This is the entry point for starting a chain of 29 | // collection operations. 30 | func FromSlice(s any) Collection { 31 | v := reflect.ValueOf(s) 32 | if v.Kind() != reflect.Slice { 33 | return Collection{data: nil, err: errors.New("FromSlice() expects a slice")} 34 | } 35 | 36 | return Collection{data: s, err: nil} 37 | } 38 | 39 | // Map applies the provided function to each element of the underlying slice, 40 | // returning a new Collection with the transformed elements. 41 | // 42 | // The provided function must: 43 | // - Be a function type 44 | // - Take one argument matching the element type of the slice 45 | // - Return exactly one value (the transformed element) 46 | // 47 | // The resulting Collection holds a slice of the new output type. 48 | // 49 | // Example: 50 | // 51 | // c := FromSlice([]int{1, 2, 3}).Map(func(n int) string { return strconv.Itoa(n) }) 52 | func (c Collection) Map(f any) Collection { 53 | if c.err != nil { 54 | return c 55 | } 56 | 57 | // Check to make sure data is a slice. 58 | v := reflect.ValueOf(c.data) 59 | if v.Kind() != reflect.Slice { 60 | return Collection{data: nil, err: errors.New("underlying data is not a slice")} 61 | } 62 | 63 | fVal := reflect.ValueOf(f) 64 | fType := fVal.Type() 65 | elemType := v.Type().Elem() 66 | 67 | // Check to make sure f is a function that takes one input and that it matches the slice element type. 68 | if fVal.Kind() != reflect.Func || fType.NumIn() != 1 || !fType.In(0).AssignableTo(elemType) { 69 | return Collection{data: c.data, err: fmt.Errorf("Map() function must take exactly one argument of type %s", elemType)} 70 | } 71 | 72 | // Check to make sure f returns one value. 73 | if fType.NumOut() != 1 { 74 | return Collection{data: c.data, err: errors.New("Map() function must return exactly one value")} 75 | } 76 | 77 | outputType := fType.Out(0) 78 | 79 | // Create a new slice of output type. 80 | resultSlice := reflect.MakeSlice(reflect.SliceOf(outputType), v.Len(), v.Len()) 81 | 82 | for i := 0; i < v.Len(); i++ { 83 | out := fVal.Call([]reflect.Value{v.Index(i)}) 84 | resultSlice.Index(i).Set(out[0]) 85 | } 86 | 87 | return Collection{data: resultSlice.Interface(), err: nil} 88 | } 89 | 90 | // Filter applies the provided function to each element of the underlying slice, 91 | // returning a new Collection containing only the elements for which the function returns true. 92 | // 93 | // The provided function must: 94 | // - Be a function type 95 | // - Take one argument matching the element type of the slice 96 | // - Return exactly one bool value 97 | // 98 | // Example: 99 | // 100 | // c := FromSlice([]int{1, 2, 3, 4}).Filter(func(n int) bool { return n%2 == 0 }) 101 | func (c Collection) Filter(f any) Collection { 102 | if c.err != nil { 103 | return c 104 | } 105 | 106 | v := reflect.ValueOf(c.data) 107 | if v.Kind() != reflect.Slice { 108 | return Collection{data: nil, err: errors.New("underlying data is not a slice")} 109 | } 110 | 111 | fVal := reflect.ValueOf(f) 112 | fType := fVal.Type() 113 | elemType := v.Type().Elem() 114 | 115 | // Check to make sure f is a function that takes one input and that it matches the slice element type. 116 | if fType.Kind() != reflect.Func || fType.NumIn() != 1 || !fType.In(0).AssignableTo(elemType) { 117 | return Collection{data: c.data, err: fmt.Errorf("Filter() function must take exactly one argument of type %s", elemType)} 118 | } 119 | 120 | // Check function returns one bool 121 | if fType.NumOut() != 1 || fType.Out(0).Kind() != reflect.Bool { 122 | return Collection{data: c.data, err: errors.New("Filter() function must return exactly one bool value")} 123 | } 124 | 125 | // Create a new slice to hold all values. 126 | resultSlice := reflect.MakeSlice(v.Type(), 0, v.Len()) 127 | 128 | for i := 0; i < v.Len(); i++ { 129 | out := fVal.Call([]reflect.Value{v.Index(i)}) 130 | if out[0].Bool() { 131 | resultSlice = reflect.Append(resultSlice, v.Index(i)) 132 | } 133 | } 134 | 135 | return Collection{data: resultSlice.Interface(), err: nil} 136 | } 137 | 138 | // ForEach applies the provided function to each element of the underlying slice, 139 | // allowing side effects like printing, logging, or collecting external results. 140 | // 141 | // The provided function must: 142 | // - Be a function type 143 | // - Take one argument matching the element type of the slice 144 | // - Return no value 145 | // 146 | // ForEach is intended for actions with side effects, not for transforming data. 147 | // The Collection returned is the same as the input, allowing further chaining. 148 | // 149 | // Example: 150 | // 151 | // FromSlice([]string{"a", "b", "c"}).ForEach(func(s string) { 152 | // fmt.Println("Value:", s) 153 | // }) 154 | func (c Collection) ForEach(f any) Collection { 155 | if c.err != nil { 156 | return c 157 | } 158 | 159 | v := reflect.ValueOf(c.data) 160 | if v.Kind() != reflect.Slice { 161 | return Collection{data: c.data, err: errors.New("underlying data is not a slice")} 162 | } 163 | 164 | fVal := reflect.ValueOf(f) 165 | fType := fVal.Type() 166 | elemType := v.Type().Elem() 167 | 168 | // Check to make sure f is a function that takes one input and that it matches the slice element type. 169 | if fType.Kind() != reflect.Func || fType.NumIn() != 1 || !fType.In(0).AssignableTo(elemType) { 170 | return Collection{data: c.data, err: fmt.Errorf("ForEach() function must take exactly one argument of type %s", elemType)} 171 | } 172 | 173 | // Check to make sure that f doesn't return anything. 174 | if fType.NumOut() != 0 { 175 | return Collection{data: c.data, err: errors.New("ForEach() function cannot return anything")} 176 | } 177 | 178 | for i := 0; i < v.Len(); i++ { 179 | fVal.Call([]reflect.Value{v.Index(i)}) 180 | } 181 | 182 | return c 183 | } 184 | 185 | // Reduce applies a reducer function over the slice, accumulating a single result. 186 | // 187 | // The reducer function must: 188 | // - Be a function type 189 | // - Take two arguments: (accumulator, element), where the accumulator type matches the type of 'initial' 190 | // - Return exactly one value, which must match the accumulator type 191 | // 192 | // Example: 193 | // 194 | // sum, err := FromSlice([]int{1, 2, 3}).Reduce(func(acc, n int) int { return acc + n }, 0) 195 | func (c Collection) Reduce(reducer any, initial any) (any, error) { 196 | if c.err != nil { 197 | return nil, c.err 198 | } 199 | 200 | v := reflect.ValueOf(c.data) 201 | if v.Kind() != reflect.Slice { 202 | return nil, errors.New("underlying data is not a slice") 203 | } 204 | 205 | reducerVal := reflect.ValueOf(reducer) 206 | reducerType := reducerVal.Type() 207 | initialVal := reflect.ValueOf(initial) 208 | initialType := initialVal.Type() 209 | elemType := v.Type().Elem() 210 | 211 | if reducerType.Kind() != reflect.Func || 212 | reducerType.NumIn() != 2 || 213 | !reducerType.In(0).AssignableTo(initialType) || 214 | !reducerType.In(1).AssignableTo(elemType) { 215 | return nil, fmt.Errorf("Reduce() function must take two arguments. First of type %s. Second of type %s.", initialType, elemType) 216 | } 217 | 218 | if reducerType.NumOut() != 1 || !reducerType.Out(0).AssignableTo(initialType) { 219 | return nil, fmt.Errorf("Reduce() function must return exactly one element of type %s", initialType) 220 | } 221 | 222 | acc := reflect.ValueOf(initial) 223 | 224 | for i := 0; i < v.Len(); i++ { 225 | acc = reducerVal.Call([]reflect.Value{acc, v.Index(i)})[0] 226 | } 227 | 228 | return acc.Interface(), nil 229 | } 230 | 231 | // ToSlice returns the underlying slice after all chained operations, 232 | // along with any accumulated error. 233 | // 234 | // The returned value is of type 'any', which can be type-asserted by the caller. 235 | // 236 | // Example: 237 | // 238 | // result, err := FromSlice([]int{1, 2, 3}).Map(...).ToSlice() 239 | func (c Collection) ToSlice() (any, error) { 240 | if c.err != nil { 241 | return nil, c.err 242 | } 243 | 244 | v := reflect.ValueOf(c.data) 245 | if v.Kind() != reflect.Slice { 246 | return nil, errors.New("underlying data is not a slice") 247 | } 248 | 249 | return c.data, nil 250 | } 251 | 252 | // ToTypedSlice casts the result of the Collection to a typed slice. 253 | // 254 | // It is a standalone generic function (not a method) due to Go's generic limitations. 255 | // The type parameter T specifies the element type. 256 | // 257 | // Example: 258 | // 259 | // strings, err := ToTypedSlice[string](c) 260 | // 261 | // This function will return an error if the underlying data cannot be cast to the requested 262 | // or if the provided Collection already contains an error. 263 | func ToTypedSlice[T any](c Collection) ([]T, error) { 264 | result, err := c.ToSlice() 265 | if err != nil { 266 | return nil, err 267 | } 268 | 269 | slice, ok := result.([]T) 270 | if !ok { 271 | return nil, fmt.Errorf("cannot cast slice to type []%T", *new(T)) 272 | } 273 | 274 | return slice, nil 275 | } 276 | -------------------------------------------------------------------------------- /constraints/constraints.go: -------------------------------------------------------------------------------- 1 | package constraints 2 | 3 | type Number interface { 4 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 5 | } 6 | -------------------------------------------------------------------------------- /datastructs/buffers/ring/ring.go: -------------------------------------------------------------------------------- 1 | // Package ring provides a generic ring buffer (circular buffer) implementation. 2 | package ring 3 | 4 | import "github.com/PsionicAlch/byteforge/internal/datastructs/buffers/ring" 5 | 6 | // RingBuffer is a generic dynamically resizable circular buffer. 7 | // It supports enqueue and dequeue operations in constant amortized time, 8 | // and grows or shrinks based on usage to optimize memory consumption. 9 | // 10 | // T represents the type of elements stored in the buffer. 11 | type RingBuffer[T any] struct { 12 | buffer *ring.InternalRingBuffer[T] 13 | } 14 | 15 | // New returns a new RingBuffer with an optional initial capacity. 16 | // If no capacity is provided or the provided value is <= 0, a default of 8 is used. 17 | func New[T any](capacity ...int) *RingBuffer[T] { 18 | return &RingBuffer[T]{ 19 | buffer: ring.New[T](capacity...), 20 | } 21 | } 22 | 23 | // FromSlice creates a new RingBuffer from a given slice. 24 | // An optional capacity may be provided. If the capacity is less than the slice length, 25 | // the slice length is used as the minimum capacity. 26 | func FromSlice[T any, A ~[]T](s A, capacity ...int) *RingBuffer[T] { 27 | return &RingBuffer[T]{ 28 | buffer: ring.FromSlice(s, capacity...), 29 | } 30 | } 31 | 32 | // FromSyncRingBuffer creates a new RingBuffer from a given SyncRingBuffer. 33 | // This results in a deep copy so the underlying buffer won't be connected 34 | // to the original SyncRingBuffer. 35 | func FromSyncRingBuffer[T any](src *SyncRingBuffer[T]) *RingBuffer[T] { 36 | return &RingBuffer[T]{ 37 | buffer: src.buffer.Clone(), 38 | } 39 | } 40 | 41 | // Len returns the number of elements currently stored in the buffer. 42 | func (rb *RingBuffer[T]) Len() int { 43 | return rb.buffer.Len() 44 | } 45 | 46 | // Cap returns the total capacity of the buffer. 47 | func (rb *RingBuffer[T]) Cap() int { 48 | return rb.buffer.Cap() 49 | } 50 | 51 | // IsEmpty returns true if the buffer contains no elements. 52 | func (rb *RingBuffer[T]) IsEmpty() bool { 53 | return rb.buffer.IsEmpty() 54 | } 55 | 56 | // Enqueue appends one or more values to the end of the buffer. 57 | // If necessary, the buffer is resized to accommodate the new values. 58 | func (rb *RingBuffer[T]) Enqueue(values ...T) { 59 | rb.buffer.Enqueue(values...) 60 | } 61 | 62 | // Dequeue removes and returns the element at the front of the buffer. 63 | // If the buffer is empty, it returns the zero value of T and false. 64 | // The buffer may shrink if usage falls below 25% of capacity. 65 | func (rb *RingBuffer[T]) Dequeue() (T, bool) { 66 | return rb.buffer.Dequeue() 67 | } 68 | 69 | // Peek returns the element at the front of the buffer without removing it. 70 | // If the buffer is empty, it returns the zero value of T and false. 71 | func (rb *RingBuffer[T]) Peek() (T, bool) { 72 | return rb.buffer.Peek() 73 | } 74 | 75 | // ToSlice returns a new slice containing all elements in the buffer in their logical order. 76 | // The returned slice is independent of the internal buffer state. 77 | func (rb *RingBuffer[T]) ToSlice() []T { 78 | return rb.buffer.ToSlice() 79 | } 80 | 81 | // Clone creates a deep copy of the source RingBuffer. 82 | func (rb *RingBuffer[T]) Clone() *RingBuffer[T] { 83 | return &RingBuffer[T]{ 84 | buffer: rb.buffer.Clone(), 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /datastructs/buffers/ring/ring_test.go: -------------------------------------------------------------------------------- 1 | package ring 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | ) 7 | 8 | func TestRingBuffer_New(t *testing.T) { 9 | scenarios := []struct { 10 | name string 11 | capacity []int 12 | expectedSize int 13 | expectedCap int 14 | }{ 15 | {"Empty capacity", []int{}, 0, 8}, 16 | {"Non-empty capacity", []int{5}, 0, 5}, 17 | {"Negative capacity", []int{-10}, 0, 8}, 18 | } 19 | 20 | for _, scenario := range scenarios { 21 | t.Run(scenario.name, func(t *testing.T) { 22 | buf := New[int](scenario.capacity...) 23 | 24 | if buf == nil { 25 | t.Fatal("Expected buf to not be nil") 26 | } 27 | 28 | if buf.buffer == nil { 29 | t.Fatal("Expected buf.buffer to not be nil") 30 | } 31 | 32 | if buf.Len() != scenario.expectedSize { 33 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.Len()) 34 | } 35 | 36 | if buf.Cap() != scenario.expectedCap { 37 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.Cap()) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestRingBuffer_FromSlice(t *testing.T) { 44 | scenarios := []struct { 45 | name string 46 | slice []int 47 | capacity []int 48 | expectedSize int 49 | expectedCap int 50 | }{ 51 | {"Empty slice and empty capacity", []int{}, []int{}, 0, 8}, 52 | {"Non-empty slice and empty capacity", []int{1, 2, 3}, []int{}, 3, 3}, 53 | {"Non-empty slice and non-empty capacity", []int{1, 2, 3}, []int{10}, 3, 10}, 54 | } 55 | 56 | for _, scenario := range scenarios { 57 | t.Run(scenario.name, func(t *testing.T) { 58 | buf := FromSlice(scenario.slice, scenario.capacity...) 59 | 60 | if buf == nil { 61 | t.Fatal("Expected buf to not be nil") 62 | } 63 | 64 | if buf.buffer == nil { 65 | t.Fatal("Expected buf.buffer to not be nil") 66 | } 67 | 68 | if buf.Len() != scenario.expectedSize { 69 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.Len()) 70 | } 71 | 72 | if buf.Cap() != scenario.expectedCap { 73 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.Cap()) 74 | } 75 | 76 | for _, item := range scenario.slice { 77 | if !slices.Contains(buf.buffer.ToSlice(), item) { 78 | t.Errorf("Expected buffer to contain %d", item) 79 | } 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestRingBuffer_FromSyncRingBuffer(t *testing.T) { 86 | src := SyncFromSlice([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 87 | dst := FromSyncRingBuffer(src) 88 | 89 | if dst == nil { 90 | t.Fatal("Expected dst to not be nil") 91 | } 92 | 93 | if dst.buffer == nil { 94 | t.Fatal("Expected dst.buffer to not be nil") 95 | } 96 | 97 | if dst.Len() != src.Len() { 98 | t.Error("Expected dst.Len() to be equal to src.Len()") 99 | } 100 | 101 | if dst.Cap() != src.Cap() { 102 | t.Error("Expected dst.Cap() to be equal to src.Cap()") 103 | } 104 | 105 | if !slices.Equal(src.ToSlice(), dst.ToSlice()) { 106 | t.Error("Expected src.ToSlice to be equal to dst.ToSlice.") 107 | } 108 | 109 | src.Enqueue(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 110 | 111 | if dst.Len() == src.Len() { 112 | t.Error("Did not expect dst.Len() to be equal to src.Len()") 113 | } 114 | 115 | if dst.Cap() == src.Cap() { 116 | t.Error("Did not expect dst.Cap() to be equal to src.Cap()") 117 | } 118 | 119 | if slices.Equal(src.ToSlice(), dst.ToSlice()) { 120 | t.Error("Did not expect src.ToSlice to be equal to dst.ToSlice.") 121 | } 122 | } 123 | 124 | func TestRingBuffer_Len(t *testing.T) { 125 | scenarios := []struct { 126 | name string 127 | data []int 128 | expectedLen int 129 | }{ 130 | {"Lenght with 0 items", nil, 0}, 131 | {"Lenght with 3 items", []int{1, 2, 3}, 3}, 132 | } 133 | 134 | for _, scenario := range scenarios { 135 | t.Run(scenario.name, func(t *testing.T) { 136 | buf := New[int]() 137 | 138 | if len(scenario.data) > 0 { 139 | buf.Enqueue(scenario.data...) 140 | } 141 | 142 | if buf.Len() != scenario.expectedLen { 143 | t.Errorf("Expected buf.Len() to be %d. Got %d", scenario.expectedLen, buf.Len()) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func TestRingBuffer_Cap(t *testing.T) { 150 | scenarios := []struct { 151 | name string 152 | data []int 153 | desiredCapacity []int 154 | expectedCapacity int 155 | }{ 156 | {"Capacity with 0 items and no desired capacity", nil, nil, 8}, 157 | {"Capacity with 0 items and desired capacity of 1", nil, []int{1}, 1}, 158 | {"Capacity with 0 items and desired capacity of -1", nil, []int{-1}, 8}, 159 | {"Capacity with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, 16}, 160 | {"Capacity with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, 6}, 161 | {"Capacity with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, 16}, 162 | } 163 | 164 | for _, scenario := range scenarios { 165 | t.Run(scenario.name, func(t *testing.T) { 166 | buf := New[int](scenario.desiredCapacity...) 167 | buf.Enqueue(scenario.data...) 168 | 169 | if buf.Cap() != scenario.expectedCapacity { 170 | t.Errorf("Expected buf.Cap() to be %d. Got %d", scenario.expectedCapacity, buf.Cap()) 171 | } 172 | }) 173 | } 174 | } 175 | 176 | func TestRingBuffer_IsEmpty(t *testing.T) { 177 | scenarios := []struct { 178 | name string 179 | data []int 180 | desiredCapacity []int 181 | isEmpty bool 182 | }{ 183 | {"IsEmpty with 0 items and no desired capacity", nil, nil, true}, 184 | {"IsEmpty with 0 items and desired capacity of 1", nil, []int{1}, true}, 185 | {"IsEmpty with 0 items and desired capacity of -1", nil, []int{-1}, true}, 186 | {"IsEmpty with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, false}, 187 | {"IsEmpty with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, false}, 188 | {"IsEmpty with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, false}, 189 | } 190 | 191 | for _, scenario := range scenarios { 192 | t.Run(scenario.name, func(t *testing.T) { 193 | buf := New[int](scenario.desiredCapacity...) 194 | buf.Enqueue(scenario.data...) 195 | 196 | if buf.IsEmpty() != scenario.isEmpty { 197 | t.Errorf("Expected buf.IsEmpty() to be %t. Got %t", scenario.isEmpty, buf.IsEmpty()) 198 | } 199 | }) 200 | } 201 | } 202 | 203 | func TestRingBuffer_Enqueue(t *testing.T) { 204 | scenarios := []struct { 205 | name string 206 | initial []int 207 | enqueue []int 208 | expectedOrder []int 209 | expectedSize int 210 | expectedCapAtLeast int 211 | }{ 212 | { 213 | name: "Append within capacity", 214 | initial: []int{1, 2}, 215 | enqueue: []int{3, 4}, 216 | expectedOrder: []int{1, 2, 3, 4}, 217 | expectedSize: 4, 218 | expectedCapAtLeast: 4, 219 | }, 220 | { 221 | name: "Trigger resize on append", 222 | initial: []int{1, 2, 3, 4}, 223 | enqueue: []int{5, 6, 7}, 224 | expectedOrder: []int{1, 2, 3, 4, 5, 6, 7}, 225 | expectedSize: 7, 226 | expectedCapAtLeast: 7, 227 | }, 228 | { 229 | name: "Append nothing (no-op)", 230 | initial: []int{9, 8, 7}, 231 | enqueue: []int{}, 232 | expectedOrder: []int{9, 8, 7}, 233 | expectedSize: 3, 234 | expectedCapAtLeast: 3, 235 | }, 236 | { 237 | name: "Append to empty buffer", 238 | initial: []int{}, 239 | enqueue: []int{10, 20, 30}, 240 | expectedOrder: []int{10, 20, 30}, 241 | expectedSize: 3, 242 | expectedCapAtLeast: 3, 243 | }, 244 | { 245 | name: "Large append exceeds multiple resizes", 246 | initial: []int{}, 247 | enqueue: makeRange(1, 50), 248 | expectedOrder: makeRange(1, 50), 249 | expectedSize: 50, 250 | expectedCapAtLeast: 50, 251 | }, 252 | } 253 | 254 | for _, scenario := range scenarios { 255 | t.Run(scenario.name, func(t *testing.T) { 256 | buf := New[int](len(scenario.initial)) 257 | buf.Enqueue(scenario.initial...) 258 | buf.Enqueue(scenario.enqueue...) 259 | 260 | if buf.Len() != scenario.expectedSize { 261 | t.Errorf("Expected size %d, got %d", scenario.expectedSize, buf.Len()) 262 | } 263 | 264 | if buf.Cap() < scenario.expectedCapAtLeast { 265 | t.Errorf("Expected capacity >= %d, got %d", scenario.expectedCapAtLeast, buf.Cap()) 266 | } 267 | 268 | for _, want := range scenario.expectedOrder { 269 | got, ok := buf.Dequeue() 270 | 271 | if !ok { 272 | t.Fatalf("Expected to dequeue %d but got nothing", want) 273 | } 274 | 275 | if got != want { 276 | t.Errorf("Expected dequeue value %d, got %d", want, got) 277 | } 278 | } 279 | 280 | if !buf.IsEmpty() { 281 | t.Errorf("Expected buffer to be empty after all dequeues") 282 | } 283 | }) 284 | } 285 | } 286 | 287 | func TestRingBuffer_Dequeue(t *testing.T) { 288 | scenarios := []struct { 289 | name string 290 | initial []int 291 | expectedDequeues []int 292 | expectedEmpty bool 293 | expectedCapAtMost int 294 | }{ 295 | { 296 | name: "Dequeue from empty buffer", 297 | initial: []int{}, 298 | expectedDequeues: []int{}, 299 | expectedEmpty: true, 300 | expectedCapAtMost: 8, 301 | }, 302 | { 303 | name: "Dequeue single element", 304 | initial: []int{42}, 305 | expectedDequeues: []int{42}, 306 | expectedEmpty: true, 307 | expectedCapAtMost: 8, 308 | }, 309 | { 310 | name: "Dequeue multiple elements", 311 | initial: []int{1, 2, 3, 4}, 312 | expectedDequeues: []int{1, 2, 3, 4}, 313 | expectedEmpty: true, 314 | expectedCapAtMost: 8, 315 | }, 316 | { 317 | name: "Dequeue triggers downsize", 318 | initial: makeRange(1, 16), 319 | expectedDequeues: makeRange(1, 14), 320 | expectedEmpty: false, 321 | expectedCapAtMost: 16, 322 | }, 323 | { 324 | name: "Dequeue wraparound case", 325 | initial: []int{10, 20, 30}, 326 | expectedDequeues: []int{10, 20, 30}, 327 | expectedEmpty: true, 328 | expectedCapAtMost: 8, 329 | }, 330 | } 331 | 332 | for _, scenario := range scenarios { 333 | t.Run(scenario.name, func(t *testing.T) { 334 | buf := New[int]() 335 | buf.Enqueue(scenario.initial...) 336 | 337 | for i, expected := range scenario.expectedDequeues { 338 | val, ok := buf.Dequeue() 339 | 340 | if !ok { 341 | t.Fatalf("Expected Dequeue #%d to return value %d, got nothing", i, expected) 342 | } 343 | 344 | if val != expected { 345 | t.Errorf("Dequeue #%d: expected %d, got %d", i, expected, val) 346 | } 347 | } 348 | 349 | if buf.IsEmpty() != scenario.expectedEmpty { 350 | t.Errorf("Expected IsEmpty to be %v, got %v", scenario.expectedEmpty, buf.IsEmpty()) 351 | } 352 | 353 | if buf.Cap() > scenario.expectedCapAtMost { 354 | t.Errorf("Expected capacity <= %d, got %d", scenario.expectedCapAtMost, buf.Cap()) 355 | } 356 | }) 357 | } 358 | } 359 | 360 | func TestRingBuffer_Peek(t *testing.T) { 361 | scenarios := []struct { 362 | name string 363 | initial []int 364 | expectedPeek int 365 | expectOK bool 366 | expectSize int 367 | }{ 368 | { 369 | name: "Peek empty buffer", 370 | initial: []int{}, 371 | expectedPeek: 0, 372 | expectOK: false, 373 | expectSize: 0, 374 | }, 375 | { 376 | name: "Peek one item", 377 | initial: []int{7}, 378 | expectedPeek: 7, 379 | expectOK: true, 380 | expectSize: 1, 381 | }, 382 | { 383 | name: "Peek multiple items", 384 | initial: []int{3, 4, 5}, 385 | expectedPeek: 3, 386 | expectOK: true, 387 | expectSize: 3, 388 | }, 389 | { 390 | name: "Peek after internal wraparound", 391 | initial: makeRange(1, 10), 392 | expectedPeek: 1, 393 | expectOK: true, 394 | expectSize: 10, 395 | }, 396 | } 397 | 398 | for _, scenario := range scenarios { 399 | t.Run(scenario.name, func(t *testing.T) { 400 | buf := New[int]() 401 | buf.Enqueue(scenario.initial...) 402 | 403 | val, ok := buf.Peek() 404 | 405 | if ok != scenario.expectOK { 406 | t.Fatalf("Expected Peek ok=%v, got %v", scenario.expectOK, ok) 407 | } 408 | 409 | if ok && val != scenario.expectedPeek { 410 | t.Errorf("Expected Peek value %d, got %d", scenario.expectedPeek, val) 411 | } 412 | 413 | if buf.Len() != scenario.expectSize { 414 | t.Errorf("Expected buffer size %d after Peek, got %d", scenario.expectSize, buf.Len()) 415 | } 416 | }) 417 | } 418 | } 419 | 420 | func TestRingBuffer_ToSlice(t *testing.T) { 421 | scenarios := []struct { 422 | name string 423 | setup func() *RingBuffer[int] 424 | expectedSlice []int 425 | expectedSize int 426 | expectedBuffer []int 427 | }{ 428 | { 429 | name: "Empty buffer", 430 | setup: func() *RingBuffer[int] { 431 | return New[int]() 432 | }, 433 | expectedSlice: []int{}, 434 | expectedSize: 0, 435 | expectedBuffer: []int{}, 436 | }, 437 | { 438 | name: "Buffer with single element", 439 | setup: func() *RingBuffer[int] { 440 | buf := New[int]() 441 | buf.Enqueue(42) 442 | return buf 443 | }, 444 | expectedSlice: []int{42}, 445 | expectedSize: 1, 446 | expectedBuffer: []int{42}, 447 | }, 448 | { 449 | name: "Buffer with multiple elements", 450 | setup: func() *RingBuffer[int] { 451 | buf := New[int]() 452 | buf.Enqueue(1, 2, 3, 4, 5) 453 | return buf 454 | }, 455 | expectedSlice: []int{1, 2, 3, 4, 5}, 456 | expectedSize: 5, 457 | expectedBuffer: []int{1, 2, 3, 4, 5}, 458 | }, 459 | { 460 | name: "Buffer with wrapped elements (head > 0)", 461 | setup: func() *RingBuffer[int] { 462 | buf := New[int](5) 463 | buf.Enqueue(1, 2, 3, 4, 5) 464 | buf.Dequeue() 465 | buf.Dequeue() 466 | buf.Enqueue(6, 7) 467 | return buf 468 | }, 469 | expectedSlice: []int{3, 4, 5, 6, 7}, 470 | expectedSize: 5, 471 | expectedBuffer: []int{3, 4, 5, 6, 7}, 472 | }, 473 | { 474 | name: "Buffer after resize", 475 | setup: func() *RingBuffer[int] { 476 | buf := New[int](4) 477 | buf.Enqueue(1, 2, 3, 4) 478 | buf.Enqueue(5) 479 | return buf 480 | }, 481 | expectedSlice: []int{1, 2, 3, 4, 5}, 482 | expectedSize: 5, 483 | expectedBuffer: []int{1, 2, 3, 4, 5}, 484 | }, 485 | { 486 | name: "Buffer after many enqueue/dequeue operations", 487 | setup: func() *RingBuffer[int] { 488 | buf := New[int](3) 489 | buf.Enqueue(1, 2, 3) 490 | buf.Dequeue() 491 | buf.Dequeue() 492 | buf.Enqueue(4, 5) 493 | buf.Dequeue() 494 | buf.Enqueue(6) 495 | return buf 496 | }, 497 | expectedSlice: []int{4, 5, 6}, 498 | expectedSize: 3, 499 | expectedBuffer: []int{4, 5, 6}, 500 | }, 501 | { 502 | name: "FromSlice initialization", 503 | setup: func() *RingBuffer[int] { 504 | return FromSlice([]int{10, 20, 30, 40}) 505 | }, 506 | expectedSlice: []int{10, 20, 30, 40}, 507 | expectedSize: 4, 508 | expectedBuffer: []int{10, 20, 30, 40}, 509 | }, 510 | } 511 | 512 | for _, scenario := range scenarios { 513 | t.Run(scenario.name, func(t *testing.T) { 514 | buf := scenario.setup() 515 | 516 | if buf.Len() != scenario.expectedSize { 517 | t.Errorf("Before ToSlice: Expected size %d, got %d", scenario.expectedSize, buf.Len()) 518 | } 519 | 520 | result := buf.ToSlice() 521 | 522 | if len(result) != len(scenario.expectedSlice) { 523 | t.Errorf("ToSlice result length: Expected %d, got %d", 524 | len(scenario.expectedSlice), len(result)) 525 | } 526 | 527 | for i, expected := range scenario.expectedSlice { 528 | if i >= len(result) { 529 | t.Errorf("Missing expected element at index %d: %v", i, expected) 530 | continue 531 | } 532 | if result[i] != expected { 533 | t.Errorf("Element mismatch at index %d: Expected %v, got %v", 534 | i, expected, result[i]) 535 | } 536 | } 537 | 538 | bufferSlice := []int{} 539 | originalSize := buf.Len() 540 | for i := 0; i < originalSize; i++ { 541 | val, ok := buf.Dequeue() 542 | if !ok { 543 | t.Fatalf("Failed to dequeue element %d", i) 544 | } 545 | bufferSlice = append(bufferSlice, val) 546 | } 547 | 548 | if len(bufferSlice) != len(scenario.expectedBuffer) { 549 | t.Errorf("After ToSlice - Buffer size changed: Expected %d, got %d", 550 | len(scenario.expectedBuffer), len(bufferSlice)) 551 | } 552 | 553 | for i, expected := range scenario.expectedBuffer { 554 | if i >= len(bufferSlice) { 555 | t.Errorf("After ToSlice - Missing expected element at index %d: %v", i, expected) 556 | continue 557 | } 558 | if bufferSlice[i] != expected { 559 | t.Errorf("After ToSlice - Element mismatch at index %d: Expected %v, got %v", 560 | i, expected, bufferSlice[i]) 561 | } 562 | } 563 | }) 564 | } 565 | } 566 | 567 | func TestRingBuffer_Clone(t *testing.T) { 568 | src := FromSlice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 569 | dst := src.Clone() 570 | 571 | if !slices.Equal(src.ToSlice(), dst.ToSlice()) { 572 | t.Error("Expected src and destination to have the same underlying data.") 573 | } 574 | 575 | src.Enqueue(11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 576 | 577 | if slices.Equal(src.ToSlice(), dst.ToSlice()) { 578 | t.Error("Expected src and destination to have the varying underlying data.") 579 | } 580 | 581 | dstSlice := dst.ToSlice() 582 | for _, num := range []int{11, 12, 13, 14, 15, 16, 17, 18, 19, 20} { 583 | if slices.Contains(dstSlice, num) { 584 | t.Errorf("dstSlice was not supposed to contain %d", num) 585 | } 586 | } 587 | } 588 | 589 | func makeRange(start, end int) []int { 590 | out := make([]int, end-start+1) 591 | for i := range out { 592 | out[i] = start + i 593 | } 594 | 595 | return out 596 | } 597 | -------------------------------------------------------------------------------- /datastructs/buffers/ring/syncring.go: -------------------------------------------------------------------------------- 1 | // Package ring provides a generic ring buffer (circular buffer) implementation with thread-safety. 2 | package ring 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/PsionicAlch/byteforge/internal/datastructs/buffers/ring" 8 | ) 9 | 10 | // SyncRingBuffer is a generic dynamically resizable circular buffer 11 | // with thread-safety. It supports enqueue and dequeue operations in 12 | // constant amortized time, and grows or shrinks based on usage to 13 | // optimize memory consumption. 14 | // 15 | // T represents the type of elements stored in the buffer. 16 | type SyncRingBuffer[T any] struct { 17 | buffer *ring.InternalRingBuffer[T] 18 | mu sync.RWMutex 19 | } 20 | 21 | // SyncNew returns a new SyncRingBuffer with an optional initial capacity. 22 | // If no capacity is provided or the provided value is <= 0, a default of 8 is used. 23 | func NewSync[T any](capacity ...int) *SyncRingBuffer[T] { 24 | return &SyncRingBuffer[T]{ 25 | buffer: ring.New[T](capacity...), 26 | } 27 | } 28 | 29 | // SyncFromSlice creates a new SyncRingBuffer from a given slice. 30 | // An optional capacity may be provided. If the capacity is less than the slice length, 31 | // the slice length is used as the minimum capacity. 32 | func SyncFromSlice[T any, A ~[]T](s A, capacity ...int) *SyncRingBuffer[T] { 33 | return &SyncRingBuffer[T]{ 34 | buffer: ring.FromSlice(s, capacity...), 35 | } 36 | } 37 | 38 | // SyncFromRingBuffer creates a new SyncRingBuffer from a given RingBuffer. 39 | // This results in a deep copy so the underlying buffer won't be connected 40 | // to the original RingBuffer. 41 | func SyncFromRingBuffer[T any](src *RingBuffer[T]) *SyncRingBuffer[T] { 42 | return &SyncRingBuffer[T]{ 43 | buffer: src.buffer.Clone(), 44 | } 45 | } 46 | 47 | // Len returns the number of elements currently stored in the buffer. 48 | func (rb *SyncRingBuffer[T]) Len() int { 49 | rb.mu.RLock() 50 | defer rb.mu.RUnlock() 51 | 52 | return rb.buffer.Len() 53 | } 54 | 55 | // Cap returns the total capacity of the buffer. 56 | func (rb *SyncRingBuffer[T]) Cap() int { 57 | rb.mu.RLock() 58 | defer rb.mu.RUnlock() 59 | 60 | return rb.buffer.Cap() 61 | } 62 | 63 | // IsEmpty returns true if the buffer contains no elements. 64 | func (rb *SyncRingBuffer[T]) IsEmpty() bool { 65 | rb.mu.RLock() 66 | defer rb.mu.RUnlock() 67 | 68 | return rb.buffer.IsEmpty() 69 | } 70 | 71 | // Enqueue appends one or more values to the end of the buffer. 72 | // If necessary, the buffer is resized to accommodate the new values. 73 | func (rb *SyncRingBuffer[T]) Enqueue(values ...T) { 74 | rb.mu.Lock() 75 | defer rb.mu.Unlock() 76 | 77 | rb.buffer.Enqueue(values...) 78 | } 79 | 80 | // Dequeue removes and returns the element at the front of the buffer. 81 | // If the buffer is empty, it returns the zero value of T and false. 82 | // The buffer may shrink if usage falls below 25% of capacity. 83 | func (rb *SyncRingBuffer[T]) Dequeue() (T, bool) { 84 | rb.mu.Lock() 85 | defer rb.mu.Unlock() 86 | 87 | return rb.buffer.Dequeue() 88 | } 89 | 90 | // Peek returns the element at the front of the buffer without removing it. 91 | // If the buffer is empty, it returns the zero value of T and false. 92 | func (rb *SyncRingBuffer[T]) Peek() (T, bool) { 93 | rb.mu.Lock() 94 | defer rb.mu.Unlock() 95 | 96 | return rb.buffer.Peek() 97 | } 98 | 99 | // ToSlice returns a new slice containing all elements in the buffer in their logical order. 100 | // The returned slice is independent of the internal buffer state. 101 | func (rb *SyncRingBuffer[T]) ToSlice() []T { 102 | rb.mu.Lock() 103 | defer rb.mu.Unlock() 104 | 105 | return rb.buffer.ToSlice() 106 | } 107 | 108 | // Clone creates a deep copy of the source SyncRingBuffer. 109 | func (rb *SyncRingBuffer[T]) Clone() *SyncRingBuffer[T] { 110 | return &SyncRingBuffer[T]{ 111 | buffer: rb.buffer.Clone(), 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /datastructs/buffers/ring/syncring_test.go: -------------------------------------------------------------------------------- 1 | package ring 2 | 3 | import ( 4 | "slices" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | func TestSyncRingBuffer_New(t *testing.T) { 10 | scenarios := []struct { 11 | name string 12 | capacity []int 13 | expectedSize int 14 | expectedCap int 15 | }{ 16 | {"Empty capacity", []int{}, 0, 8}, 17 | {"Non-empty capacity", []int{5}, 0, 5}, 18 | {"Negative capacity", []int{-10}, 0, 8}, 19 | } 20 | 21 | for _, scenario := range scenarios { 22 | t.Run(scenario.name, func(t *testing.T) { 23 | buf := NewSync[int](scenario.capacity...) 24 | 25 | if buf == nil { 26 | t.Fatal("Expected buf to not be nil") 27 | } 28 | 29 | if buf.buffer == nil { 30 | t.Fatal("Expected buf.buffer to not be nil") 31 | } 32 | 33 | if buf.Len() != scenario.expectedSize { 34 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.Len()) 35 | } 36 | 37 | if buf.Cap() != scenario.expectedCap { 38 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.Cap()) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestSyncRingBuffer_FromSlice(t *testing.T) { 45 | scenarios := []struct { 46 | name string 47 | slice []int 48 | capacity []int 49 | expectedSize int 50 | expectedCap int 51 | }{ 52 | {"Empty slice and empty capacity", []int{}, []int{}, 0, 8}, 53 | {"Non-empty slice and empty capacity", []int{1, 2, 3}, []int{}, 3, 3}, 54 | {"Non-empty slice and non-empty capacity", []int{1, 2, 3}, []int{10}, 3, 10}, 55 | } 56 | 57 | for _, scenario := range scenarios { 58 | t.Run(scenario.name, func(t *testing.T) { 59 | buf := SyncFromSlice(scenario.slice, scenario.capacity...) 60 | 61 | if buf == nil { 62 | t.Fatal("Expected buf to not be nil") 63 | } 64 | 65 | if buf.buffer == nil { 66 | t.Fatal("Expected buf.buffer to not be nil") 67 | } 68 | 69 | if buf.Len() != scenario.expectedSize { 70 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.Len()) 71 | } 72 | 73 | if buf.Cap() != scenario.expectedCap { 74 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.Cap()) 75 | } 76 | 77 | for _, item := range scenario.slice { 78 | if !slices.Contains(buf.buffer.ToSlice(), item) { 79 | t.Errorf("Expected buffer to contain %d", item) 80 | } 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func TestSyncRingBuffer_SyncFromRingBuffer(t *testing.T) { 87 | src := FromSlice([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 88 | dst := SyncFromRingBuffer(src) 89 | 90 | if dst == nil { 91 | t.Fatal("Expected dst to not be nil") 92 | } 93 | 94 | if dst.buffer == nil { 95 | t.Fatal("Expected dst.buffer to not be nil") 96 | } 97 | 98 | if dst.Len() != src.Len() { 99 | t.Error("Expected dst.Len() to be equal to src.Len()") 100 | } 101 | 102 | if dst.Cap() != src.Cap() { 103 | t.Error("Expected dst.Cap() to be equal to src.Cap()") 104 | } 105 | 106 | if !slices.Equal(src.ToSlice(), dst.ToSlice()) { 107 | t.Error("Expected src.ToSlice to be equal to dst.ToSlice.") 108 | } 109 | 110 | src.Enqueue(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 111 | 112 | if dst.Len() == src.Len() { 113 | t.Error("Did not expect dst.Len() to be equal to src.Len()") 114 | } 115 | 116 | if dst.Cap() == src.Cap() { 117 | t.Error("Did not expect dst.Cap() to be equal to src.Cap()") 118 | } 119 | 120 | if slices.Equal(src.ToSlice(), dst.ToSlice()) { 121 | t.Error("Did not expect src.ToSlice to be equal to dst.ToSlice.") 122 | } 123 | } 124 | 125 | func TestSyncRingBuffer_Len(t *testing.T) { 126 | scenarios := []struct { 127 | name string 128 | data []int 129 | expectedLen int 130 | }{ 131 | {"Lenght with 0 items", nil, 0}, 132 | {"Lenght with 3 items", []int{1, 2, 3}, 3}, 133 | } 134 | 135 | for _, scenario := range scenarios { 136 | t.Run(scenario.name, func(t *testing.T) { 137 | buf := New[int]() 138 | buf.Enqueue(scenario.data...) 139 | 140 | var wg sync.WaitGroup 141 | 142 | for i := 0; i < 1000; i++ { 143 | wg.Add(1) 144 | go func() { 145 | defer wg.Done() 146 | 147 | if buf.Len() != scenario.expectedLen { 148 | t.Errorf("Expected buf.Len() to be %d. Got %d", scenario.expectedLen, buf.Len()) 149 | } 150 | }() 151 | } 152 | 153 | wg.Wait() 154 | }) 155 | } 156 | } 157 | 158 | func TestSyncRingBuffer_Cap(t *testing.T) { 159 | scenarios := []struct { 160 | name string 161 | data []int 162 | desiredCapacity []int 163 | expectedCapacity int 164 | }{ 165 | {"Capacity with 0 items and no desired capacity", nil, nil, 8}, 166 | {"Capacity with 0 items and desired capacity of 1", nil, []int{1}, 1}, 167 | {"Capacity with 0 items and desired capacity of -1", nil, []int{-1}, 8}, 168 | {"Capacity with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, 16}, 169 | {"Capacity with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, 6}, 170 | {"Capacity with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, 16}, 171 | } 172 | 173 | for _, scenario := range scenarios { 174 | t.Run(scenario.name, func(t *testing.T) { 175 | buf := New[int](scenario.desiredCapacity...) 176 | buf.Enqueue(scenario.data...) 177 | 178 | var wg sync.WaitGroup 179 | 180 | for i := 0; i < 1000; i++ { 181 | wg.Add(1) 182 | go func() { 183 | defer wg.Done() 184 | 185 | if buf.Cap() != scenario.expectedCapacity { 186 | t.Errorf("Expected buf.Cap() to be %d. Got %d", scenario.expectedCapacity, buf.Cap()) 187 | } 188 | }() 189 | } 190 | 191 | wg.Wait() 192 | }) 193 | } 194 | } 195 | 196 | func TestSyncRingBuffer_IsEmpty(t *testing.T) { 197 | scenarios := []struct { 198 | name string 199 | data []int 200 | desiredCapacity []int 201 | isEmpty bool 202 | }{ 203 | {"IsEmpty with 0 items and no desired capacity", nil, nil, true}, 204 | {"IsEmpty with 0 items and desired capacity of 1", nil, []int{1}, true}, 205 | {"IsEmpty with 0 items and desired capacity of -1", nil, []int{-1}, true}, 206 | {"IsEmpty with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, false}, 207 | {"IsEmpty with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, false}, 208 | {"IsEmpty with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, false}, 209 | } 210 | 211 | for _, scenario := range scenarios { 212 | t.Run(scenario.name, func(t *testing.T) { 213 | buf := NewSync[int](scenario.desiredCapacity...) 214 | buf.Enqueue(scenario.data...) 215 | 216 | var wg sync.WaitGroup 217 | 218 | for i := 0; i < 1000; i++ { 219 | wg.Add(1) 220 | go func() { 221 | defer wg.Done() 222 | 223 | if buf.IsEmpty() != scenario.isEmpty { 224 | t.Errorf("Expected buf.IsEmpty() to be %t. Got %t", scenario.isEmpty, buf.IsEmpty()) 225 | } 226 | }() 227 | } 228 | 229 | wg.Wait() 230 | }) 231 | } 232 | } 233 | 234 | func TestSyncRingBuffer_Enqueue(t *testing.T) { 235 | const max = 1000 236 | 237 | buf := NewSync[int]() 238 | 239 | var wg sync.WaitGroup 240 | 241 | for i := 0; i < max; i++ { 242 | wg.Add(1) 243 | go func() { 244 | defer wg.Done() 245 | 246 | buf.Enqueue(i) 247 | }() 248 | } 249 | 250 | wg.Wait() 251 | 252 | if buf.Len() != max { 253 | t.Errorf("Expected buf.Len() to be %d. Got %d", max, buf.Len()) 254 | } 255 | } 256 | 257 | func TestSyncRingBuffer_Dequeue(t *testing.T) { 258 | const max = 1000 259 | 260 | buf := NewSync[int]() 261 | 262 | var wg sync.WaitGroup 263 | 264 | for i := 1; i <= max; i++ { 265 | wg.Add(1) 266 | go func() { 267 | defer wg.Done() 268 | 269 | buf.Enqueue(i) 270 | }() 271 | } 272 | 273 | wg.Wait() 274 | 275 | for i := 1; i <= max; i++ { 276 | wg.Add(1) 277 | go func() { 278 | defer wg.Done() 279 | 280 | value, found := buf.Dequeue() 281 | 282 | if !found { 283 | t.Error("Expected to find value from buf.Dequeue") 284 | } 285 | 286 | if value < 1 || value > max { 287 | t.Errorf("Expected value to be in range [0, %d]. Got %d", max, value) 288 | } 289 | }() 290 | } 291 | } 292 | 293 | func TestSyncRingBuffer_Peek(t *testing.T) { 294 | buf := SyncFromSlice([]int{1, 2, 3, 4, 5}) 295 | expectedValue := 1 296 | 297 | var wg sync.WaitGroup 298 | 299 | for i := 0; i < 1000; i++ { 300 | wg.Add(1) 301 | go func() { 302 | defer wg.Done() 303 | 304 | value, found := buf.Peek() 305 | 306 | if !found { 307 | t.Error("Expected buf.Peek() to return value") 308 | } 309 | 310 | if value != expectedValue { 311 | t.Errorf("Expected to find %d. Got %d", expectedValue, value) 312 | } 313 | }() 314 | } 315 | 316 | wg.Wait() 317 | } 318 | 319 | func TestSyncRingBuffer_ToSlice(t *testing.T) { 320 | data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 321 | buf := SyncFromSlice(data) 322 | 323 | var wg sync.WaitGroup 324 | 325 | for i := 0; i < 1000; i++ { 326 | wg.Add(1) 327 | 328 | go func() { 329 | defer wg.Done() 330 | 331 | if !slices.Equal(data, buf.ToSlice()) { 332 | t.Error("Expected buf.ToSlice() to be equal to data.") 333 | } 334 | }() 335 | } 336 | 337 | wg.Wait() 338 | } 339 | 340 | func TestSyncRingBuffer_Clone(t *testing.T) { 341 | src := SyncFromSlice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 342 | 343 | var wg sync.WaitGroup 344 | 345 | for i := 0; i < 1000; i++ { 346 | wg.Add(1) 347 | 348 | go func() { 349 | defer wg.Done() 350 | 351 | dst := src.Clone() 352 | 353 | if !slices.Equal(src.ToSlice(), dst.ToSlice()) { 354 | t.Error("Expected src and destination to have the same underlying data.") 355 | } 356 | 357 | dst.Enqueue(11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 358 | 359 | if slices.Equal(src.ToSlice(), dst.ToSlice()) { 360 | t.Error("Expected src and destination to have the varying underlying data.") 361 | } 362 | 363 | srcSlice := src.ToSlice() 364 | for _, num := range []int{11, 12, 13, 14, 15, 16, 17, 18, 19, 20} { 365 | if slices.Contains(srcSlice, num) { 366 | t.Errorf("srcSlice was not supposed to contain %d", num) 367 | } 368 | } 369 | }() 370 | } 371 | 372 | wg.Wait() 373 | } 374 | -------------------------------------------------------------------------------- /datastructs/queue/queue.go: -------------------------------------------------------------------------------- 1 | // Queue is a generic dynamically resizable FIFO Queue. It supports enqueue and dequeue operations in constant amortized time, and grows or shrinks based on usage to optimize memory consumption. 2 | package queue 3 | 4 | import ( 5 | "slices" 6 | 7 | "github.com/PsionicAlch/byteforge/internal/datastructs/buffers/ring" 8 | ) 9 | 10 | type Queue[T comparable] struct { 11 | buffer *ring.InternalRingBuffer[T] 12 | } 13 | 14 | // New returns a new Queue with an optional initial capacity. 15 | // If no capacity is provided or the provided value is <= 0, a default of 8 is used. 16 | func New[T comparable](capacity ...int) *Queue[T] { 17 | return &Queue[T]{ 18 | buffer: ring.New[T](capacity...), 19 | } 20 | } 21 | 22 | // FromSlice creates a new Queue from a given slice. 23 | // An optional capacity may be provided. If the capacity is less than the slice length, 24 | // the slice length is used as the minimum capacity. 25 | func FromSlice[T comparable, A ~[]T](s A, capacity ...int) *Queue[T] { 26 | return &Queue[T]{ 27 | buffer: ring.FromSlice(s, capacity...), 28 | } 29 | } 30 | 31 | // FromSyncQueue creates a new Queue from a given SyncQueue. 32 | // This results in a deep copy so the underlying buffer won't be connected 33 | // to the original SyncQueue. 34 | func FromSyncQueue[T comparable](src *SyncQueue[T]) *Queue[T] { 35 | return &Queue[T]{ 36 | buffer: src.buffer.Clone(), 37 | } 38 | } 39 | 40 | // Len returns the number of elements currently stored in the buffer. 41 | func (q *Queue[T]) Len() int { 42 | return q.buffer.Len() 43 | } 44 | 45 | // Cap returns the total capacity of the buffer. 46 | func (q *Queue[T]) Cap() int { 47 | return q.buffer.Cap() 48 | } 49 | 50 | // IsEmpty returns true if the buffer contains no elements. 51 | func (q *Queue[T]) IsEmpty() bool { 52 | return q.buffer.IsEmpty() 53 | } 54 | 55 | // Enqueue appends one or more values to the end of the buffer. 56 | // If necessary, the buffer is resized to accommodate the new values. 57 | func (q *Queue[T]) Enqueue(values ...T) { 58 | q.buffer.Enqueue(values...) 59 | } 60 | 61 | // Dequeue removes and returns the element at the front of the buffer. 62 | // If the buffer is empty, it returns the zero value of T and false. 63 | // The buffer may shrink if usage falls below 25% of capacity. 64 | func (q *Queue[T]) Dequeue() (T, bool) { 65 | return q.buffer.Dequeue() 66 | } 67 | 68 | // Peek returns the element at the front of the buffer without removing it. 69 | // If the buffer is empty, it returns the zero value of T and false. 70 | func (q *Queue[T]) Peek() (T, bool) { 71 | return q.buffer.Peek() 72 | } 73 | 74 | // ToSlice returns a new slice containing all elements in the buffer in their logical order. 75 | // The returned slice is independent of the internal buffer state. 76 | func (q *Queue[T]) ToSlice() []T { 77 | return q.buffer.ToSlice() 78 | } 79 | 80 | // Clone creates a deep copy of the source Queue. 81 | func (q *Queue[T]) Clone() *Queue[T] { 82 | return &Queue[T]{ 83 | buffer: q.buffer.Clone(), 84 | } 85 | } 86 | 87 | // Equals compares the lenght and elements in the Queue to the other Queue. 88 | func (q *Queue[T]) Equals(other *Queue[T]) bool { 89 | s1 := q.ToSlice() 90 | s2 := other.ToSlice() 91 | 92 | return slices.Equal(s1, s2) 93 | } 94 | -------------------------------------------------------------------------------- /datastructs/queue/queue_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | ) 7 | 8 | func TestQueue_New(t *testing.T) { 9 | scenarios := []struct { 10 | name string 11 | capacity []int 12 | expectedSize int 13 | expectedCap int 14 | }{ 15 | {"Empty capacity", []int{}, 0, 8}, 16 | {"Non-empty capacity", []int{5}, 0, 5}, 17 | {"Negative capacity", []int{-10}, 0, 8}, 18 | } 19 | 20 | for _, scenario := range scenarios { 21 | t.Run(scenario.name, func(t *testing.T) { 22 | buf := New[int](scenario.capacity...) 23 | 24 | if buf == nil { 25 | t.Fatal("Expected buf to not be nil") 26 | } 27 | 28 | if buf.buffer == nil { 29 | t.Fatal("Expected buf.buffer to not be nil") 30 | } 31 | 32 | if buf.Len() != scenario.expectedSize { 33 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.Len()) 34 | } 35 | 36 | if buf.Cap() != scenario.expectedCap { 37 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.Cap()) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestQueue_FromSlice(t *testing.T) { 44 | scenarios := []struct { 45 | name string 46 | slice []int 47 | capacity []int 48 | expectedSize int 49 | expectedCap int 50 | }{ 51 | {"Empty slice and empty capacity", []int{}, []int{}, 0, 8}, 52 | {"Non-empty slice and empty capacity", []int{1, 2, 3}, []int{}, 3, 3}, 53 | {"Non-empty slice and non-empty capacity", []int{1, 2, 3}, []int{10}, 3, 10}, 54 | } 55 | 56 | for _, scenario := range scenarios { 57 | t.Run(scenario.name, func(t *testing.T) { 58 | buf := FromSlice(scenario.slice, scenario.capacity...) 59 | 60 | if buf == nil { 61 | t.Fatal("Expected buf to not be nil") 62 | } 63 | 64 | if buf.buffer == nil { 65 | t.Fatal("Expected buf.buffer to not be nil") 66 | } 67 | 68 | if buf.Len() != scenario.expectedSize { 69 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.Len()) 70 | } 71 | 72 | if buf.Cap() != scenario.expectedCap { 73 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.Cap()) 74 | } 75 | 76 | for _, item := range scenario.slice { 77 | if !slices.Contains(buf.buffer.ToSlice(), item) { 78 | t.Errorf("Expected buffer to contain %d", item) 79 | } 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func TestQueue_FromSyncQueue(t *testing.T) { 86 | src := SyncFromSlice([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 87 | dst := FromSyncQueue(src) 88 | 89 | if dst == nil { 90 | t.Fatal("Expected dst to not be nil") 91 | } 92 | 93 | if dst.buffer == nil { 94 | t.Fatal("Expected dst.buffer to not be nil") 95 | } 96 | 97 | if dst.Len() != src.Len() { 98 | t.Error("Expected dst.Len() to be equal to src.Len()") 99 | } 100 | 101 | if dst.Cap() != src.Cap() { 102 | t.Error("Expected dst.Cap() to be equal to src.Cap()") 103 | } 104 | 105 | if !slices.Equal(src.ToSlice(), dst.ToSlice()) { 106 | t.Error("Expected src.ToSlice to be equal to dst.ToSlice.") 107 | } 108 | 109 | src.Enqueue(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 110 | 111 | if dst.Len() == src.Len() { 112 | t.Error("Did not expect dst.Len() to be equal to src.Len()") 113 | } 114 | 115 | if dst.Cap() == src.Cap() { 116 | t.Error("Did not expect dst.Cap() to be equal to src.Cap()") 117 | } 118 | 119 | if slices.Equal(src.ToSlice(), dst.ToSlice()) { 120 | t.Error("Did not expect src.ToSlice to be equal to dst.ToSlice.") 121 | } 122 | } 123 | 124 | func TestQueue_Len(t *testing.T) { 125 | scenarios := []struct { 126 | name string 127 | data []int 128 | expectedLen int 129 | }{ 130 | {"Lenght with 0 items", nil, 0}, 131 | {"Lenght with 3 items", []int{1, 2, 3}, 3}, 132 | } 133 | 134 | for _, scenario := range scenarios { 135 | t.Run(scenario.name, func(t *testing.T) { 136 | buf := New[int]() 137 | 138 | if len(scenario.data) > 0 { 139 | buf.Enqueue(scenario.data...) 140 | } 141 | 142 | if buf.Len() != scenario.expectedLen { 143 | t.Errorf("Expected buf.Len() to be %d. Got %d", scenario.expectedLen, buf.Len()) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func TestQueue_Cap(t *testing.T) { 150 | scenarios := []struct { 151 | name string 152 | data []int 153 | desiredCapacity []int 154 | expectedCapacity int 155 | }{ 156 | {"Capacity with 0 items and no desired capacity", nil, nil, 8}, 157 | {"Capacity with 0 items and desired capacity of 1", nil, []int{1}, 1}, 158 | {"Capacity with 0 items and desired capacity of -1", nil, []int{-1}, 8}, 159 | {"Capacity with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, 16}, 160 | {"Capacity with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, 6}, 161 | {"Capacity with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, 16}, 162 | } 163 | 164 | for _, scenario := range scenarios { 165 | t.Run(scenario.name, func(t *testing.T) { 166 | buf := New[int](scenario.desiredCapacity...) 167 | buf.Enqueue(scenario.data...) 168 | 169 | if buf.Cap() != scenario.expectedCapacity { 170 | t.Errorf("Expected buf.Cap() to be %d. Got %d", scenario.expectedCapacity, buf.Cap()) 171 | } 172 | }) 173 | } 174 | } 175 | 176 | func TestQueue_IsEmpty(t *testing.T) { 177 | scenarios := []struct { 178 | name string 179 | data []int 180 | desiredCapacity []int 181 | isEmpty bool 182 | }{ 183 | {"IsEmpty with 0 items and no desired capacity", nil, nil, true}, 184 | {"IsEmpty with 0 items and desired capacity of 1", nil, []int{1}, true}, 185 | {"IsEmpty with 0 items and desired capacity of -1", nil, []int{-1}, true}, 186 | {"IsEmpty with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, false}, 187 | {"IsEmpty with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, false}, 188 | {"IsEmpty with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, false}, 189 | } 190 | 191 | for _, scenario := range scenarios { 192 | t.Run(scenario.name, func(t *testing.T) { 193 | buf := New[int](scenario.desiredCapacity...) 194 | buf.Enqueue(scenario.data...) 195 | 196 | if buf.IsEmpty() != scenario.isEmpty { 197 | t.Errorf("Expected buf.IsEmpty() to be %t. Got %t", scenario.isEmpty, buf.IsEmpty()) 198 | } 199 | }) 200 | } 201 | } 202 | 203 | func TestQueue_Enqueue(t *testing.T) { 204 | scenarios := []struct { 205 | name string 206 | initial []int 207 | enqueue []int 208 | expectedOrder []int 209 | expectedSize int 210 | expectedCapAtLeast int 211 | }{ 212 | { 213 | name: "Append within capacity", 214 | initial: []int{1, 2}, 215 | enqueue: []int{3, 4}, 216 | expectedOrder: []int{1, 2, 3, 4}, 217 | expectedSize: 4, 218 | expectedCapAtLeast: 4, 219 | }, 220 | { 221 | name: "Trigger resize on append", 222 | initial: []int{1, 2, 3, 4}, 223 | enqueue: []int{5, 6, 7}, 224 | expectedOrder: []int{1, 2, 3, 4, 5, 6, 7}, 225 | expectedSize: 7, 226 | expectedCapAtLeast: 7, 227 | }, 228 | { 229 | name: "Append nothing (no-op)", 230 | initial: []int{9, 8, 7}, 231 | enqueue: []int{}, 232 | expectedOrder: []int{9, 8, 7}, 233 | expectedSize: 3, 234 | expectedCapAtLeast: 3, 235 | }, 236 | { 237 | name: "Append to empty buffer", 238 | initial: []int{}, 239 | enqueue: []int{10, 20, 30}, 240 | expectedOrder: []int{10, 20, 30}, 241 | expectedSize: 3, 242 | expectedCapAtLeast: 3, 243 | }, 244 | { 245 | name: "Large append exceeds multiple resizes", 246 | initial: []int{}, 247 | enqueue: makeRange(1, 50), 248 | expectedOrder: makeRange(1, 50), 249 | expectedSize: 50, 250 | expectedCapAtLeast: 50, 251 | }, 252 | } 253 | 254 | for _, scenario := range scenarios { 255 | t.Run(scenario.name, func(t *testing.T) { 256 | buf := New[int](len(scenario.initial)) 257 | buf.Enqueue(scenario.initial...) 258 | buf.Enqueue(scenario.enqueue...) 259 | 260 | if buf.Len() != scenario.expectedSize { 261 | t.Errorf("Expected size %d, got %d", scenario.expectedSize, buf.Len()) 262 | } 263 | 264 | if buf.Cap() < scenario.expectedCapAtLeast { 265 | t.Errorf("Expected capacity >= %d, got %d", scenario.expectedCapAtLeast, buf.Cap()) 266 | } 267 | 268 | for _, want := range scenario.expectedOrder { 269 | got, ok := buf.Dequeue() 270 | 271 | if !ok { 272 | t.Fatalf("Expected to dequeue %d but got nothing", want) 273 | } 274 | 275 | if got != want { 276 | t.Errorf("Expected dequeue value %d, got %d", want, got) 277 | } 278 | } 279 | 280 | if !buf.IsEmpty() { 281 | t.Errorf("Expected buffer to be empty after all dequeues") 282 | } 283 | }) 284 | } 285 | } 286 | 287 | func TestQueue_Dequeue(t *testing.T) { 288 | scenarios := []struct { 289 | name string 290 | initial []int 291 | expectedDequeues []int 292 | expectedEmpty bool 293 | expectedCapAtMost int 294 | }{ 295 | { 296 | name: "Dequeue from empty buffer", 297 | initial: []int{}, 298 | expectedDequeues: []int{}, 299 | expectedEmpty: true, 300 | expectedCapAtMost: 8, 301 | }, 302 | { 303 | name: "Dequeue single element", 304 | initial: []int{42}, 305 | expectedDequeues: []int{42}, 306 | expectedEmpty: true, 307 | expectedCapAtMost: 8, 308 | }, 309 | { 310 | name: "Dequeue multiple elements", 311 | initial: []int{1, 2, 3, 4}, 312 | expectedDequeues: []int{1, 2, 3, 4}, 313 | expectedEmpty: true, 314 | expectedCapAtMost: 8, 315 | }, 316 | { 317 | name: "Dequeue triggers downsize", 318 | initial: makeRange(1, 16), 319 | expectedDequeues: makeRange(1, 14), 320 | expectedEmpty: false, 321 | expectedCapAtMost: 16, 322 | }, 323 | { 324 | name: "Dequeue wraparound case", 325 | initial: []int{10, 20, 30}, 326 | expectedDequeues: []int{10, 20, 30}, 327 | expectedEmpty: true, 328 | expectedCapAtMost: 8, 329 | }, 330 | } 331 | 332 | for _, scenario := range scenarios { 333 | t.Run(scenario.name, func(t *testing.T) { 334 | buf := New[int]() 335 | buf.Enqueue(scenario.initial...) 336 | 337 | for i, expected := range scenario.expectedDequeues { 338 | val, ok := buf.Dequeue() 339 | 340 | if !ok { 341 | t.Fatalf("Expected Dequeue #%d to return value %d, got nothing", i, expected) 342 | } 343 | 344 | if val != expected { 345 | t.Errorf("Dequeue #%d: expected %d, got %d", i, expected, val) 346 | } 347 | } 348 | 349 | if buf.IsEmpty() != scenario.expectedEmpty { 350 | t.Errorf("Expected IsEmpty to be %v, got %v", scenario.expectedEmpty, buf.IsEmpty()) 351 | } 352 | 353 | if buf.Cap() > scenario.expectedCapAtMost { 354 | t.Errorf("Expected capacity <= %d, got %d", scenario.expectedCapAtMost, buf.Cap()) 355 | } 356 | }) 357 | } 358 | } 359 | 360 | func TestQueue_Peek(t *testing.T) { 361 | scenarios := []struct { 362 | name string 363 | initial []int 364 | expectedPeek int 365 | expectOK bool 366 | expectSize int 367 | }{ 368 | { 369 | name: "Peek empty buffer", 370 | initial: []int{}, 371 | expectedPeek: 0, 372 | expectOK: false, 373 | expectSize: 0, 374 | }, 375 | { 376 | name: "Peek one item", 377 | initial: []int{7}, 378 | expectedPeek: 7, 379 | expectOK: true, 380 | expectSize: 1, 381 | }, 382 | { 383 | name: "Peek multiple items", 384 | initial: []int{3, 4, 5}, 385 | expectedPeek: 3, 386 | expectOK: true, 387 | expectSize: 3, 388 | }, 389 | { 390 | name: "Peek after internal wraparound", 391 | initial: makeRange(1, 10), 392 | expectedPeek: 1, 393 | expectOK: true, 394 | expectSize: 10, 395 | }, 396 | } 397 | 398 | for _, scenario := range scenarios { 399 | t.Run(scenario.name, func(t *testing.T) { 400 | buf := New[int]() 401 | buf.Enqueue(scenario.initial...) 402 | 403 | val, ok := buf.Peek() 404 | 405 | if ok != scenario.expectOK { 406 | t.Fatalf("Expected Peek ok=%v, got %v", scenario.expectOK, ok) 407 | } 408 | 409 | if ok && val != scenario.expectedPeek { 410 | t.Errorf("Expected Peek value %d, got %d", scenario.expectedPeek, val) 411 | } 412 | 413 | if buf.Len() != scenario.expectSize { 414 | t.Errorf("Expected buffer size %d after Peek, got %d", scenario.expectSize, buf.Len()) 415 | } 416 | }) 417 | } 418 | } 419 | 420 | func TestQueue_ToSlice(t *testing.T) { 421 | scenarios := []struct { 422 | name string 423 | setup func() *Queue[int] 424 | expectedSlice []int 425 | expectedSize int 426 | expectedBuffer []int 427 | }{ 428 | { 429 | name: "Empty buffer", 430 | setup: func() *Queue[int] { 431 | return New[int]() 432 | }, 433 | expectedSlice: []int{}, 434 | expectedSize: 0, 435 | expectedBuffer: []int{}, 436 | }, 437 | { 438 | name: "Buffer with single element", 439 | setup: func() *Queue[int] { 440 | buf := New[int]() 441 | buf.Enqueue(42) 442 | return buf 443 | }, 444 | expectedSlice: []int{42}, 445 | expectedSize: 1, 446 | expectedBuffer: []int{42}, 447 | }, 448 | { 449 | name: "Buffer with multiple elements", 450 | setup: func() *Queue[int] { 451 | buf := New[int]() 452 | buf.Enqueue(1, 2, 3, 4, 5) 453 | return buf 454 | }, 455 | expectedSlice: []int{1, 2, 3, 4, 5}, 456 | expectedSize: 5, 457 | expectedBuffer: []int{1, 2, 3, 4, 5}, 458 | }, 459 | { 460 | name: "Buffer with wrapped elements (head > 0)", 461 | setup: func() *Queue[int] { 462 | buf := New[int](5) 463 | buf.Enqueue(1, 2, 3, 4, 5) 464 | buf.Dequeue() 465 | buf.Dequeue() 466 | buf.Enqueue(6, 7) 467 | return buf 468 | }, 469 | expectedSlice: []int{3, 4, 5, 6, 7}, 470 | expectedSize: 5, 471 | expectedBuffer: []int{3, 4, 5, 6, 7}, 472 | }, 473 | { 474 | name: "Buffer after resize", 475 | setup: func() *Queue[int] { 476 | buf := New[int](4) 477 | buf.Enqueue(1, 2, 3, 4) 478 | buf.Enqueue(5) 479 | return buf 480 | }, 481 | expectedSlice: []int{1, 2, 3, 4, 5}, 482 | expectedSize: 5, 483 | expectedBuffer: []int{1, 2, 3, 4, 5}, 484 | }, 485 | { 486 | name: "Buffer after many enqueue/dequeue operations", 487 | setup: func() *Queue[int] { 488 | buf := New[int](3) 489 | buf.Enqueue(1, 2, 3) 490 | buf.Dequeue() 491 | buf.Dequeue() 492 | buf.Enqueue(4, 5) 493 | buf.Dequeue() 494 | buf.Enqueue(6) 495 | return buf 496 | }, 497 | expectedSlice: []int{4, 5, 6}, 498 | expectedSize: 3, 499 | expectedBuffer: []int{4, 5, 6}, 500 | }, 501 | { 502 | name: "FromSlice initialization", 503 | setup: func() *Queue[int] { 504 | return FromSlice([]int{10, 20, 30, 40}) 505 | }, 506 | expectedSlice: []int{10, 20, 30, 40}, 507 | expectedSize: 4, 508 | expectedBuffer: []int{10, 20, 30, 40}, 509 | }, 510 | } 511 | 512 | for _, scenario := range scenarios { 513 | t.Run(scenario.name, func(t *testing.T) { 514 | buf := scenario.setup() 515 | 516 | if buf.Len() != scenario.expectedSize { 517 | t.Errorf("Before ToSlice: Expected size %d, got %d", scenario.expectedSize, buf.Len()) 518 | } 519 | 520 | result := buf.ToSlice() 521 | 522 | if len(result) != len(scenario.expectedSlice) { 523 | t.Errorf("ToSlice result length: Expected %d, got %d", 524 | len(scenario.expectedSlice), len(result)) 525 | } 526 | 527 | for i, expected := range scenario.expectedSlice { 528 | if i >= len(result) { 529 | t.Errorf("Missing expected element at index %d: %v", i, expected) 530 | continue 531 | } 532 | if result[i] != expected { 533 | t.Errorf("Element mismatch at index %d: Expected %v, got %v", 534 | i, expected, result[i]) 535 | } 536 | } 537 | 538 | bufferSlice := []int{} 539 | originalSize := buf.Len() 540 | for i := 0; i < originalSize; i++ { 541 | val, ok := buf.Dequeue() 542 | if !ok { 543 | t.Fatalf("Failed to dequeue element %d", i) 544 | } 545 | bufferSlice = append(bufferSlice, val) 546 | } 547 | 548 | if len(bufferSlice) != len(scenario.expectedBuffer) { 549 | t.Errorf("After ToSlice - Buffer size changed: Expected %d, got %d", 550 | len(scenario.expectedBuffer), len(bufferSlice)) 551 | } 552 | 553 | for i, expected := range scenario.expectedBuffer { 554 | if i >= len(bufferSlice) { 555 | t.Errorf("After ToSlice - Missing expected element at index %d: %v", i, expected) 556 | continue 557 | } 558 | if bufferSlice[i] != expected { 559 | t.Errorf("After ToSlice - Element mismatch at index %d: Expected %v, got %v", 560 | i, expected, bufferSlice[i]) 561 | } 562 | } 563 | }) 564 | } 565 | } 566 | 567 | func TestQueue_Clone(t *testing.T) { 568 | src := FromSlice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 569 | dst := src.Clone() 570 | 571 | if !slices.Equal(src.ToSlice(), dst.ToSlice()) { 572 | t.Error("Expected src and destination to have the same underlying data.") 573 | } 574 | 575 | src.Enqueue(11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 576 | 577 | if slices.Equal(src.ToSlice(), dst.ToSlice()) { 578 | t.Error("Expected src and destination to have the varying underlying data.") 579 | } 580 | 581 | dstSlice := dst.ToSlice() 582 | for _, num := range []int{11, 12, 13, 14, 15, 16, 17, 18, 19, 20} { 583 | if slices.Contains(dstSlice, num) { 584 | t.Errorf("dstSlice was not supposed to contain %d", num) 585 | } 586 | } 587 | } 588 | 589 | func TestQueue_Equals(t *testing.T) { 590 | q1 := FromSlice([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 591 | q2 := q1.Clone() 592 | q3 := FromSlice([]int{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}) 593 | 594 | if !q1.Equals(q2) { 595 | t.Error("Expected q1 to be equal to q2") 596 | } 597 | 598 | if q1.Equals(q3) { 599 | t.Error("Did not expect q1 to be equal to q3") 600 | } 601 | } 602 | 603 | func makeRange(start, end int) []int { 604 | out := make([]int, end-start+1) 605 | for i := range out { 606 | out[i] = start + i 607 | } 608 | 609 | return out 610 | } 611 | -------------------------------------------------------------------------------- /datastructs/queue/syncqueue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "slices" 5 | "sync" 6 | 7 | "github.com/PsionicAlch/byteforge/internal/datastructs/buffers/ring" 8 | "github.com/PsionicAlch/byteforge/internal/functions/utils" 9 | ) 10 | 11 | type SyncQueue[T comparable] struct { 12 | buffer *ring.InternalRingBuffer[T] 13 | mu sync.RWMutex 14 | } 15 | 16 | // New returns a new Queue with an optional initial capacity. 17 | // If no capacity is provided or the provided value is <= 0, a default of 8 is used. 18 | func NewSync[T comparable](capacity ...int) *SyncQueue[T] { 19 | return &SyncQueue[T]{ 20 | buffer: ring.New[T](capacity...), 21 | } 22 | } 23 | 24 | // FromSlice creates a new Queue from a given slice. 25 | // An optional capacity may be provided. If the capacity is less than the slice length, 26 | // the slice length is used as the minimum capacity. 27 | func SyncFromSlice[T comparable, A ~[]T](s A, capacity ...int) *SyncQueue[T] { 28 | return &SyncQueue[T]{ 29 | buffer: ring.FromSlice(s, capacity...), 30 | } 31 | } 32 | 33 | // FromSyncQueue creates a new Queue from a given SyncQueue. 34 | // This results in a deep copy so the underlying buffer won't be connected 35 | // to the original SyncQueue. 36 | func SyncFromQueue[T comparable](src *Queue[T]) *SyncQueue[T] { 37 | return &SyncQueue[T]{ 38 | buffer: src.buffer.Clone(), 39 | } 40 | } 41 | 42 | // Len returns the number of elements currently stored in the buffer. 43 | func (q *SyncQueue[T]) Len() int { 44 | q.mu.RLock() 45 | defer q.mu.RUnlock() 46 | 47 | return q.buffer.Len() 48 | } 49 | 50 | // Cap returns the total capacity of the buffer. 51 | func (q *SyncQueue[T]) Cap() int { 52 | q.mu.RLock() 53 | defer q.mu.RUnlock() 54 | 55 | return q.buffer.Cap() 56 | } 57 | 58 | // IsEmpty returns true if the buffer contains no elements. 59 | func (q *SyncQueue[T]) IsEmpty() bool { 60 | q.mu.RLock() 61 | defer q.mu.RUnlock() 62 | 63 | return q.buffer.IsEmpty() 64 | } 65 | 66 | // Enqueue appends one or more values to the end of the buffer. 67 | // If necessary, the buffer is resized to accommodate the new values. 68 | func (q *SyncQueue[T]) Enqueue(values ...T) { 69 | q.mu.Lock() 70 | defer q.mu.Unlock() 71 | 72 | q.buffer.Enqueue(values...) 73 | } 74 | 75 | // Dequeue removes and returns the element at the front of the buffer. 76 | // If the buffer is empty, it returns the zero value of T and false. 77 | // The buffer may shrink if usage falls below 25% of capacity. 78 | func (q *SyncQueue[T]) Dequeue() (T, bool) { 79 | q.mu.Lock() 80 | defer q.mu.Unlock() 81 | 82 | return q.buffer.Dequeue() 83 | } 84 | 85 | // Peek returns the element at the front of the buffer without removing it. 86 | // If the buffer is empty, it returns the zero value of T and false. 87 | func (q *SyncQueue[T]) Peek() (T, bool) { 88 | q.mu.Lock() 89 | defer q.mu.Unlock() 90 | 91 | return q.buffer.Peek() 92 | } 93 | 94 | // ToSlice returns a new slice containing all elements in the buffer in their logical order. 95 | // The returned slice is independent of the internal buffer state. 96 | func (q *SyncQueue[T]) ToSlice() []T { 97 | q.mu.Lock() 98 | defer q.mu.Unlock() 99 | 100 | return q.buffer.ToSlice() 101 | } 102 | 103 | // Clone creates a deep copy of the source Queue. 104 | func (q *SyncQueue[T]) Clone() *SyncQueue[T] { 105 | q.mu.Lock() 106 | defer q.mu.Unlock() 107 | 108 | return &SyncQueue[T]{ 109 | buffer: q.buffer.Clone(), 110 | } 111 | } 112 | 113 | // Equals compares the lenght and elements in the Queue to the other Queue. 114 | func (q *SyncQueue[T]) Equals(other *SyncQueue[T]) bool { 115 | q1, q2 := utils.SortByAddress(q, other) 116 | 117 | q1.mu.Lock() 118 | defer q1.mu.Unlock() 119 | 120 | q2.mu.Lock() 121 | defer q2.mu.Unlock() 122 | 123 | return slices.Equal(q1.buffer.ToSlice(), q2.buffer.ToSlice()) 124 | } 125 | -------------------------------------------------------------------------------- /datastructs/queue/syncqueue_test.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "slices" 5 | "sync" 6 | "testing" 7 | ) 8 | 9 | func TestSyncQueue_New(t *testing.T) { 10 | scenarios := []struct { 11 | name string 12 | capacity []int 13 | expectedSize int 14 | expectedCap int 15 | }{ 16 | {"Empty capacity", []int{}, 0, 8}, 17 | {"Non-empty capacity", []int{5}, 0, 5}, 18 | {"Negative capacity", []int{-10}, 0, 8}, 19 | } 20 | 21 | for _, scenario := range scenarios { 22 | t.Run(scenario.name, func(t *testing.T) { 23 | buf := NewSync[int](scenario.capacity...) 24 | 25 | if buf == nil { 26 | t.Fatal("Expected buf to not be nil") 27 | } 28 | 29 | if buf.buffer == nil { 30 | t.Fatal("Expected buf.buffer to not be nil") 31 | } 32 | 33 | if buf.Len() != scenario.expectedSize { 34 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.Len()) 35 | } 36 | 37 | if buf.Cap() != scenario.expectedCap { 38 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.Cap()) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestSyncQueue_FromSlice(t *testing.T) { 45 | scenarios := []struct { 46 | name string 47 | slice []int 48 | capacity []int 49 | expectedSize int 50 | expectedCap int 51 | }{ 52 | {"Empty slice and empty capacity", []int{}, []int{}, 0, 8}, 53 | {"Non-empty slice and empty capacity", []int{1, 2, 3}, []int{}, 3, 3}, 54 | {"Non-empty slice and non-empty capacity", []int{1, 2, 3}, []int{10}, 3, 10}, 55 | } 56 | 57 | for _, scenario := range scenarios { 58 | t.Run(scenario.name, func(t *testing.T) { 59 | buf := SyncFromSlice(scenario.slice, scenario.capacity...) 60 | 61 | if buf == nil { 62 | t.Fatal("Expected buf to not be nil") 63 | } 64 | 65 | if buf.buffer == nil { 66 | t.Fatal("Expected buf.buffer to not be nil") 67 | } 68 | 69 | if buf.Len() != scenario.expectedSize { 70 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.Len()) 71 | } 72 | 73 | if buf.Cap() != scenario.expectedCap { 74 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.Cap()) 75 | } 76 | 77 | for _, item := range scenario.slice { 78 | if !slices.Contains(buf.buffer.ToSlice(), item) { 79 | t.Errorf("Expected buffer to contain %d", item) 80 | } 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func TestSyncQueue_SyncFromRingBuffer(t *testing.T) { 87 | src := FromSlice([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 88 | dst := SyncFromQueue(src) 89 | 90 | if dst == nil { 91 | t.Fatal("Expected dst to not be nil") 92 | } 93 | 94 | if dst.buffer == nil { 95 | t.Fatal("Expected dst.buffer to not be nil") 96 | } 97 | 98 | if dst.Len() != src.Len() { 99 | t.Error("Expected dst.Len() to be equal to src.Len()") 100 | } 101 | 102 | if dst.Cap() != src.Cap() { 103 | t.Error("Expected dst.Cap() to be equal to src.Cap()") 104 | } 105 | 106 | if !slices.Equal(src.ToSlice(), dst.ToSlice()) { 107 | t.Error("Expected src.ToSlice to be equal to dst.ToSlice.") 108 | } 109 | 110 | src.Enqueue(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 111 | 112 | if dst.Len() == src.Len() { 113 | t.Error("Did not expect dst.Len() to be equal to src.Len()") 114 | } 115 | 116 | if dst.Cap() == src.Cap() { 117 | t.Error("Did not expect dst.Cap() to be equal to src.Cap()") 118 | } 119 | 120 | if slices.Equal(src.ToSlice(), dst.ToSlice()) { 121 | t.Error("Did not expect src.ToSlice to be equal to dst.ToSlice.") 122 | } 123 | } 124 | 125 | func TestSyncQueue_Len(t *testing.T) { 126 | scenarios := []struct { 127 | name string 128 | data []int 129 | expectedLen int 130 | }{ 131 | {"Lenght with 0 items", nil, 0}, 132 | {"Lenght with 3 items", []int{1, 2, 3}, 3}, 133 | } 134 | 135 | for _, scenario := range scenarios { 136 | t.Run(scenario.name, func(t *testing.T) { 137 | buf := New[int]() 138 | buf.Enqueue(scenario.data...) 139 | 140 | var wg sync.WaitGroup 141 | 142 | for i := 0; i < 1000; i++ { 143 | wg.Add(1) 144 | go func() { 145 | defer wg.Done() 146 | 147 | if buf.Len() != scenario.expectedLen { 148 | t.Errorf("Expected buf.Len() to be %d. Got %d", scenario.expectedLen, buf.Len()) 149 | } 150 | }() 151 | } 152 | 153 | wg.Wait() 154 | }) 155 | } 156 | } 157 | 158 | func TestSyncQueue_Cap(t *testing.T) { 159 | scenarios := []struct { 160 | name string 161 | data []int 162 | desiredCapacity []int 163 | expectedCapacity int 164 | }{ 165 | {"Capacity with 0 items and no desired capacity", nil, nil, 8}, 166 | {"Capacity with 0 items and desired capacity of 1", nil, []int{1}, 1}, 167 | {"Capacity with 0 items and desired capacity of -1", nil, []int{-1}, 8}, 168 | {"Capacity with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, 16}, 169 | {"Capacity with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, 6}, 170 | {"Capacity with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, 16}, 171 | } 172 | 173 | for _, scenario := range scenarios { 174 | t.Run(scenario.name, func(t *testing.T) { 175 | buf := New[int](scenario.desiredCapacity...) 176 | buf.Enqueue(scenario.data...) 177 | 178 | var wg sync.WaitGroup 179 | 180 | for i := 0; i < 1000; i++ { 181 | wg.Add(1) 182 | go func() { 183 | defer wg.Done() 184 | 185 | if buf.Cap() != scenario.expectedCapacity { 186 | t.Errorf("Expected buf.Cap() to be %d. Got %d", scenario.expectedCapacity, buf.Cap()) 187 | } 188 | }() 189 | } 190 | 191 | wg.Wait() 192 | }) 193 | } 194 | } 195 | 196 | func TestSyncQueue_IsEmpty(t *testing.T) { 197 | scenarios := []struct { 198 | name string 199 | data []int 200 | desiredCapacity []int 201 | isEmpty bool 202 | }{ 203 | {"IsEmpty with 0 items and no desired capacity", nil, nil, true}, 204 | {"IsEmpty with 0 items and desired capacity of 1", nil, []int{1}, true}, 205 | {"IsEmpty with 0 items and desired capacity of -1", nil, []int{-1}, true}, 206 | {"IsEmpty with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, false}, 207 | {"IsEmpty with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, false}, 208 | {"IsEmpty with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, false}, 209 | } 210 | 211 | for _, scenario := range scenarios { 212 | t.Run(scenario.name, func(t *testing.T) { 213 | buf := NewSync[int](scenario.desiredCapacity...) 214 | buf.Enqueue(scenario.data...) 215 | 216 | var wg sync.WaitGroup 217 | 218 | for i := 0; i < 1000; i++ { 219 | wg.Add(1) 220 | go func() { 221 | defer wg.Done() 222 | 223 | if buf.IsEmpty() != scenario.isEmpty { 224 | t.Errorf("Expected buf.IsEmpty() to be %t. Got %t", scenario.isEmpty, buf.IsEmpty()) 225 | } 226 | }() 227 | } 228 | 229 | wg.Wait() 230 | }) 231 | } 232 | } 233 | 234 | func TestSyncQueue_Enqueue(t *testing.T) { 235 | const max = 1000 236 | 237 | buf := NewSync[int]() 238 | 239 | var wg sync.WaitGroup 240 | 241 | for i := 0; i < max; i++ { 242 | wg.Add(1) 243 | go func() { 244 | defer wg.Done() 245 | 246 | buf.Enqueue(i) 247 | }() 248 | } 249 | 250 | wg.Wait() 251 | 252 | if buf.Len() != max { 253 | t.Errorf("Expected buf.Len() to be %d. Got %d", max, buf.Len()) 254 | } 255 | } 256 | 257 | func TestSyncQueue_Dequeue(t *testing.T) { 258 | const max = 1000 259 | 260 | buf := NewSync[int]() 261 | 262 | var wg sync.WaitGroup 263 | 264 | for i := 1; i <= max; i++ { 265 | wg.Add(1) 266 | go func() { 267 | defer wg.Done() 268 | 269 | buf.Enqueue(i) 270 | }() 271 | } 272 | 273 | wg.Wait() 274 | 275 | for i := 1; i <= max; i++ { 276 | wg.Add(1) 277 | go func() { 278 | defer wg.Done() 279 | 280 | value, found := buf.Dequeue() 281 | 282 | if !found { 283 | t.Error("Expected to find value from buf.Dequeue") 284 | } 285 | 286 | if value < 1 || value > max { 287 | t.Errorf("Expected value to be in range [0, %d]. Got %d", max, value) 288 | } 289 | }() 290 | } 291 | } 292 | 293 | func TestSyncQueue_Peek(t *testing.T) { 294 | buf := SyncFromSlice([]int{1, 2, 3, 4, 5}) 295 | expectedValue := 1 296 | 297 | var wg sync.WaitGroup 298 | 299 | for i := 0; i < 1000; i++ { 300 | wg.Add(1) 301 | go func() { 302 | defer wg.Done() 303 | 304 | value, found := buf.Peek() 305 | 306 | if !found { 307 | t.Error("Expected buf.Peek() to return value") 308 | } 309 | 310 | if value != expectedValue { 311 | t.Errorf("Expected to find %d. Got %d", expectedValue, value) 312 | } 313 | }() 314 | } 315 | 316 | wg.Wait() 317 | } 318 | 319 | func TestSyncQueue_ToSlice(t *testing.T) { 320 | data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 321 | buf := SyncFromSlice(data) 322 | 323 | var wg sync.WaitGroup 324 | 325 | for i := 0; i < 1000; i++ { 326 | wg.Add(1) 327 | 328 | go func() { 329 | defer wg.Done() 330 | 331 | if !slices.Equal(data, buf.ToSlice()) { 332 | t.Error("Expected buf.ToSlice() to be equal to data.") 333 | } 334 | }() 335 | } 336 | 337 | wg.Wait() 338 | } 339 | 340 | func TestSyncQueue_Clone(t *testing.T) { 341 | src := SyncFromSlice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 342 | 343 | var wg sync.WaitGroup 344 | 345 | for i := 0; i < 1000; i++ { 346 | wg.Add(1) 347 | 348 | go func() { 349 | defer wg.Done() 350 | 351 | dst := src.Clone() 352 | 353 | if !slices.Equal(src.ToSlice(), dst.ToSlice()) { 354 | t.Error("Expected src and destination to have the same underlying data.") 355 | } 356 | 357 | dst.Enqueue(11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 358 | 359 | if slices.Equal(src.ToSlice(), dst.ToSlice()) { 360 | t.Error("Expected src and destination to have the varying underlying data.") 361 | } 362 | 363 | srcSlice := src.ToSlice() 364 | for _, num := range []int{11, 12, 13, 14, 15, 16, 17, 18, 19, 20} { 365 | if slices.Contains(srcSlice, num) { 366 | t.Errorf("srcSlice was not supposed to contain %d", num) 367 | } 368 | } 369 | }() 370 | } 371 | 372 | wg.Wait() 373 | } 374 | 375 | func TestSyncQueue_Equal(t *testing.T) { 376 | q1 := SyncFromSlice([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 377 | q2 := q1.Clone() 378 | q3 := SyncFromSlice([]int{10, 11, 12, 13, 14, 15, 16, 17, 18, 19}) 379 | 380 | var wg sync.WaitGroup 381 | 382 | for i := 0; i < 1000; i++ { 383 | wg.Add(1) 384 | 385 | go func() { 386 | wg.Done() 387 | 388 | if !q1.Equals(q2) { 389 | t.Error("Expected q1 to be equal to q2") 390 | } 391 | 392 | if q1.Equals(q3) { 393 | t.Error("Did not expect q1 to be equal to q3") 394 | } 395 | }() 396 | } 397 | 398 | wg.Wait() 399 | } 400 | -------------------------------------------------------------------------------- /datastructs/set/set.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import "iter" 4 | 5 | // Set implements a generic set data structure 6 | type Set[T comparable] struct { 7 | items map[T]struct{} 8 | } 9 | 10 | // New creates a new empty Set with an optional initial capacity 11 | func New[T comparable](size ...int) *Set[T] { 12 | itemSize := 0 13 | 14 | if len(size) > 0 { 15 | itemSize = size[0] 16 | } 17 | 18 | return &Set[T]{ 19 | items: make(map[T]struct{}, itemSize), 20 | } 21 | } 22 | 23 | // FromSlice creates a new Set from a slice of items 24 | func FromSlice[T comparable](data []T) *Set[T] { 25 | items := make(map[T]struct{}, len(data)) 26 | 27 | for _, item := range data { 28 | items[item] = struct{}{} 29 | } 30 | 31 | return &Set[T]{ 32 | items: items, 33 | } 34 | } 35 | 36 | // FromSyncSet creates a new Set from a SyncSet. 37 | func FromSyncSet[T comparable](set *SyncSet[T]) *Set[T] { 38 | clone := set.Clone() 39 | 40 | return clone.set 41 | } 42 | 43 | // Contains checks if the Set contains the specified item 44 | func (s *Set[T]) Contains(item T) bool { 45 | _, has := s.items[item] 46 | 47 | return has 48 | } 49 | 50 | // Push adds one or more items to the Set 51 | func (s *Set[T]) Push(items ...T) { 52 | for _, item := range items { 53 | s.items[item] = struct{}{} 54 | } 55 | } 56 | 57 | // Pop removes and returns an arbitrary element from the Set 58 | // 59 | // Note: The selection of which element to pop is non-deterministic due to Go's map iteration order 60 | func (s *Set[T]) Pop() (T, bool) { 61 | for item := range s.items { 62 | delete(s.items, item) 63 | return item, true 64 | } 65 | 66 | var zero T 67 | return zero, false 68 | } 69 | 70 | // Peek returns an arbitrary element from the Set without removing it 71 | // 72 | // Note: The selection of which element to peek is non-deterministic due to Go's map iteration order 73 | func (s *Set[T]) Peek() (T, bool) { 74 | for item := range s.items { 75 | return item, true 76 | } 77 | 78 | var zero T 79 | return zero, false 80 | } 81 | 82 | // Size returns the number of elements in the Set 83 | func (s *Set[T]) Size() int { 84 | return len(s.items) 85 | } 86 | 87 | // IsEmpty returns true if the Set contains no elements 88 | func (s *Set[T]) IsEmpty() bool { 89 | return len(s.items) == 0 90 | } 91 | 92 | // Iter returns an iterator over the Set's elements 93 | func (s *Set[T]) Iter() iter.Seq[T] { 94 | return func(yield func(T) bool) { 95 | for item := range s.items { 96 | if !yield(item) { 97 | return 98 | } 99 | } 100 | } 101 | } 102 | 103 | // Remove deletes an item from the Set and returns whether it was present 104 | func (s *Set[T]) Remove(item T) bool { 105 | if s.Contains(item) { 106 | delete(s.items, item) 107 | return true 108 | } 109 | 110 | return false 111 | } 112 | 113 | // Clear removes all elements from the Set 114 | func (s *Set[T]) Clear() { 115 | s.items = make(map[T]struct{}) 116 | } 117 | 118 | // Clone creates a new Set with the same elements 119 | func (s *Set[T]) Clone() *Set[T] { 120 | clone := &Set[T]{items: make(map[T]struct{}, len(s.items))} 121 | for item := range s.items { 122 | clone.items[item] = struct{}{} 123 | } 124 | 125 | return clone 126 | } 127 | 128 | // Union returns a new Set containing all elements from both Sets 129 | func (s *Set[T]) Union(other *Set[T]) *Set[T] { 130 | result := s.Clone() 131 | for item := range other.items { 132 | result.items[item] = struct{}{} 133 | } 134 | return result 135 | } 136 | 137 | // Intersection returns a new Set containing elements present in both Sets 138 | func (s *Set[T]) Intersection(other *Set[T]) *Set[T] { 139 | result := New[T]() 140 | 141 | // Determine which set is smaller to optimize iteration 142 | if s.Size() > other.Size() { 143 | s, other = other, s 144 | } 145 | 146 | for item := range s.items { 147 | if other.Contains(item) { 148 | result.items[item] = struct{}{} 149 | } 150 | } 151 | 152 | return result 153 | } 154 | 155 | // Difference returns a new Set containing elements in s that are not in other 156 | func (s *Set[T]) Difference(other *Set[T]) *Set[T] { 157 | result := New[T]() 158 | for item := range s.items { 159 | if !other.Contains(item) { 160 | result.items[item] = struct{}{} 161 | } 162 | } 163 | 164 | return result 165 | } 166 | 167 | // SymmetricDifference returns a new Set with elements in either Set but not in both 168 | func (s *Set[T]) SymmetricDifference(other *Set[T]) *Set[T] { 169 | result := New[T](s.Size() + other.Size()) 170 | 171 | // Add elements from s that are not in other 172 | for item := range s.items { 173 | if !other.Contains(item) { 174 | result.items[item] = struct{}{} 175 | } 176 | } 177 | 178 | // Add elements from other that are not in s 179 | for item := range other.items { 180 | if !s.Contains(item) { 181 | result.items[item] = struct{}{} 182 | } 183 | } 184 | 185 | return result 186 | } 187 | 188 | // IsSubsetOf returns true if all elements in s are also in other 189 | func (s *Set[T]) IsSubsetOf(other *Set[T]) bool { 190 | for item := range s.items { 191 | if !other.Contains(item) { 192 | return false 193 | } 194 | } 195 | 196 | return true 197 | } 198 | 199 | // Equals returns true if both Sets contain exactly the same elements 200 | func (s *Set[T]) Equals(other *Set[T]) bool { 201 | if s.Size() != other.Size() { 202 | return false 203 | } 204 | 205 | // Since sizes are equal, we only need to check in one direction 206 | // If every element in s is in other, and counts are equal, they must be the same set 207 | for item := range s.items { 208 | if !other.Contains(item) { 209 | return false 210 | } 211 | } 212 | 213 | return true 214 | } 215 | 216 | // ToSlice returns all elements of the Set as a slice 217 | func (s *Set[T]) ToSlice() []T { 218 | items := make([]T, 0, len(s.items)) 219 | 220 | for item := range s.items { 221 | items = append(items, item) 222 | } 223 | 224 | return items 225 | } 226 | -------------------------------------------------------------------------------- /datastructs/set/set_test.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSet_New(t *testing.T) { 8 | s := New[int]() 9 | 10 | if s == nil { 11 | t.Fatal("New() returned nil") 12 | } 13 | 14 | if s.items == nil { 15 | t.Fatal("New().items is nil") 16 | } 17 | 18 | if len(s.items) != 0 { 19 | t.Errorf("Expected empty set, got size %d", len(s.items)) 20 | } 21 | } 22 | 23 | func TestSet_FromSlice(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | input []int 27 | want []int 28 | }{ 29 | {"empty slice", []int{}, []int{}}, 30 | {"slice with unique elements", []int{1, 2, 3}, []int{1, 2, 3}}, 31 | {"slice with duplicate elements", []int{1, 2, 2, 3, 1}, []int{1, 2, 3}}, 32 | } 33 | 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | s := FromSlice(tt.input) 37 | 38 | if s.Size() != len(tt.want) { 39 | t.Errorf("FromSlice(%v).Size() = %d, want %d", tt.input, s.Size(), len(tt.want)) 40 | } 41 | 42 | for _, item := range tt.want { 43 | if !s.Contains(item) { 44 | t.Errorf("FromSlice(%v) does not contain %d", tt.input, item) 45 | } 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func TestSet_FromSyncSet(t *testing.T) { 52 | t.Run("From non-empty SyncSet", func(t *testing.T) { 53 | syncS := NewSync[int]() 54 | syncS.Push(1, 2, 3) 55 | 56 | s := FromSyncSet(syncS) 57 | if s.Size() != 3 { 58 | t.Errorf("Expected size 3, got %d", s.Size()) 59 | } 60 | if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) { 61 | t.Error("Set does not contain expected elements") 62 | } 63 | // Ensure it's a clone 64 | s.Push(4) 65 | if syncS.set.Contains(4) { 66 | t.Error("Original SyncSet's internal set was modified") 67 | } 68 | }) 69 | 70 | t.Run("From empty SyncSet", func(t *testing.T) { 71 | syncS := NewSync[string]() 72 | s := FromSyncSet(syncS) 73 | if !s.IsEmpty() { 74 | t.Error("Expected empty set from empty SyncSet") 75 | } 76 | }) 77 | } 78 | 79 | func TestSet_Contains(t *testing.T) { 80 | s := FromSlice([]string{"a", "b", "c"}) 81 | 82 | tests := []struct { 83 | item string 84 | want bool 85 | }{ 86 | {"a", true}, 87 | {"d", false}, 88 | {"", false}, 89 | } 90 | 91 | for _, tt := range tests { 92 | if got := s.Contains(tt.item); got != tt.want { 93 | t.Errorf("s.Contains(%q) = %v, want %v", tt.item, got, tt.want) 94 | } 95 | } 96 | 97 | emptySet := New[int]() 98 | if emptySet.Contains(1) { 99 | t.Error("Empty set should not contain any element") 100 | } 101 | } 102 | 103 | func TestSet_Push(t *testing.T) { 104 | s := New[int]() 105 | s.Push(1) 106 | 107 | if !s.Contains(1) || s.Size() != 1 { 108 | t.Errorf("Push(1) failed. Set: %v", s.ToSlice()) 109 | } 110 | 111 | s.Push(2, 3) 112 | if !s.Contains(2) || !s.Contains(3) || s.Size() != 3 { 113 | t.Errorf("Push(2, 3) failed. Set: %v", s.ToSlice()) 114 | } 115 | 116 | // Push existing items 117 | s.Push(1, 2) 118 | if s.Size() != 3 { 119 | t.Errorf("Pushing existing items changed size. Set: %v, Size: %d", s.ToSlice(), s.Size()) 120 | } 121 | 122 | s.Push() // Push no items 123 | if s.Size() != 3 { 124 | t.Errorf("Pushing no items changed size. Set: %v, Size: %d", s.ToSlice(), s.Size()) 125 | } 126 | } 127 | 128 | func TestSet_Pop(t *testing.T) { 129 | t.Run("Pop from non-empty set", func(t *testing.T) { 130 | s := FromSlice([]int{10, 20, 30}) 131 | initialSize := s.Size() 132 | 133 | poppedItem, ok := s.Pop() 134 | if !ok { 135 | t.Fatal("Pop() returned ok=false for non-empty set") 136 | } 137 | if s.Size() != initialSize-1 { 138 | t.Errorf("Size after Pop() = %d, want %d", s.Size(), initialSize-1) 139 | } 140 | if s.Contains(poppedItem) { 141 | t.Errorf("Popped item %d still present in set", poppedItem) 142 | } 143 | 144 | // Pop all elements 145 | _, _ = s.Pop() 146 | _, _ = s.Pop() 147 | if !s.IsEmpty() { 148 | t.Error("Set should be empty after popping all elements") 149 | } 150 | }) 151 | 152 | t.Run("Pop from empty set", func(t *testing.T) { 153 | s := New[string]() 154 | item, ok := s.Pop() 155 | if ok { 156 | t.Error("Pop() returned ok=true for empty set") 157 | } 158 | var zeroString string 159 | if item != zeroString { 160 | t.Errorf("Pop() from empty set returned item %q, want zero value %q", item, zeroString) 161 | } 162 | }) 163 | } 164 | 165 | func TestSet_Peek(t *testing.T) { 166 | t.Run("Peek from non-empty set", func(t *testing.T) { 167 | s := FromSlice([]int{10, 20, 30}) 168 | initialSize := s.Size() 169 | initialSet := s.Clone() 170 | 171 | peekedItem, ok := s.Peek() 172 | 173 | if !ok { 174 | t.Fatal("Peek() returned ok=false for non-empty set") 175 | } 176 | 177 | if s.Size() != initialSize { 178 | t.Errorf("Size after Peek() = %d, want %d", s.Size(), initialSize) 179 | } 180 | 181 | // Should still be there 182 | if !s.Contains(peekedItem) { 183 | t.Errorf("Peeked item %d not found in set after peeking", peekedItem) 184 | } 185 | 186 | // Verify set content remains unchanged 187 | if !s.Equals(initialSet) { 188 | t.Errorf("Set content changed after Peek(). Initial: %v, Current: %v", initialSet.ToSlice(), s.ToSlice()) 189 | } 190 | }) 191 | 192 | t.Run("Peek from empty set", func(t *testing.T) { 193 | s := New[string]() 194 | item, ok := s.Peek() 195 | 196 | if ok { 197 | t.Error("Peek() returned ok=true for empty set") 198 | } 199 | 200 | var zeroString string 201 | if item != zeroString { 202 | t.Errorf("Peek() from empty set returned item %q, want zero value %q", item, zeroString) 203 | } 204 | }) 205 | } 206 | 207 | func TestSet_Size(t *testing.T) { 208 | s := New[int]() 209 | 210 | if s.Size() != 0 { 211 | t.Errorf("New set size = %d, want 0", s.Size()) 212 | } 213 | 214 | s.Push(1, 2) 215 | if s.Size() != 2 { 216 | t.Errorf("Set size after Push(1,2) = %d, want 2", s.Size()) 217 | } 218 | 219 | s.Remove(1) 220 | if s.Size() != 1 { 221 | t.Errorf("Set size after Remove(1) = %d, want 1", s.Size()) 222 | } 223 | } 224 | 225 | func TestSet_IsEmpty(t *testing.T) { 226 | s := New[int]() 227 | 228 | if !s.IsEmpty() { 229 | t.Error("New set IsEmpty() = false, want true") 230 | } 231 | 232 | s.Push(1) 233 | if s.IsEmpty() { 234 | t.Error("Set with 1 element IsEmpty() = true, want false") 235 | } 236 | 237 | s.Remove(1) 238 | if !s.IsEmpty() { 239 | t.Error("Set after removing last element IsEmpty() = false, want true") 240 | } 241 | } 242 | 243 | func TestSet_Iter(t *testing.T) { 244 | t.Run("Iterate over non-empty set", func(t *testing.T) { 245 | inputItems := []string{"apple", "banana", "cherry"} 246 | s := FromSlice(inputItems) 247 | iteratedItems := make([]string, 0, s.Size()) 248 | 249 | for item := range s.Iter() { 250 | iteratedItems = append(iteratedItems, item) 251 | } 252 | 253 | if !s.Equals(FromSlice(iteratedItems)) { 254 | t.Errorf("Iter() did not yield all items. Got: %v, Want (any order): %v", iteratedItems, inputItems) 255 | } 256 | }) 257 | 258 | t.Run("Iterate over empty set", func(t *testing.T) { 259 | s := New[int]() 260 | count := 0 261 | 262 | for range s.Iter() { 263 | count++ 264 | } 265 | 266 | if count != 0 { 267 | t.Errorf("Iter() on empty set yielded %d items, want 0", count) 268 | } 269 | }) 270 | 271 | t.Run("Iterate with early exit", func(t *testing.T) { 272 | s := FromSlice([]int{1, 2, 3, 4, 5}) 273 | count := 0 274 | 275 | for item := range s.Iter() { 276 | // just an example condition 277 | if item > 0 { 278 | count++ 279 | } 280 | 281 | // Stop after 2 items 282 | if count == 2 { 283 | break 284 | } 285 | } 286 | 287 | if count != 2 { 288 | t.Errorf("Iter() with early exit: expected to process 2 items, got %d", count) 289 | } 290 | }) 291 | } 292 | 293 | func TestSet_Remove(t *testing.T) { 294 | s := FromSlice([]int{1, 2, 3}) 295 | 296 | if !s.Remove(2) { 297 | t.Error("Remove(2) returned false, want true") 298 | } 299 | 300 | if s.Contains(2) { 301 | t.Error("Set still contains 2 after Remove(2)") 302 | } 303 | 304 | if s.Size() != 2 { 305 | t.Errorf("Size after Remove(2) = %d, want 2", s.Size()) 306 | } 307 | 308 | // Remove non-existent item 309 | if s.Remove(4) { 310 | t.Error("Remove(4) returned true, want false") 311 | } 312 | 313 | if s.Size() != 2 { 314 | t.Errorf("Size after Remove(4) = %d, want 2", s.Size()) 315 | } 316 | 317 | s.Remove(1) 318 | s.Remove(3) 319 | 320 | if !s.IsEmpty() { 321 | t.Error("Set not empty after removing all items") 322 | } 323 | 324 | // Remove from empty set 325 | if s.Remove(1) { 326 | t.Error("Remove(1) from empty set returned true, want false") 327 | } 328 | } 329 | 330 | func TestSet_Clear(t *testing.T) { 331 | s := FromSlice([]int{1, 2, 3, 4, 5}) 332 | s.Clear() 333 | 334 | if !s.IsEmpty() { 335 | t.Error("Set IsEmpty() = false after Clear(), want true") 336 | } 337 | 338 | if s.Size() != 0 { 339 | t.Errorf("Set Size() = %d after Clear(), want 0", s.Size()) 340 | } 341 | 342 | if s.Contains(1) { 343 | t.Error("Set Contains(1) = true after Clear(), want false") 344 | } 345 | 346 | emptySet := New[string]() 347 | 348 | // Clear an already empty set 349 | emptySet.Clear() 350 | 351 | if !emptySet.IsEmpty() { 352 | t.Error("Empty set IsEmpty() = false after Clear(), want true") 353 | } 354 | } 355 | 356 | func TestSet_Clone(t *testing.T) { 357 | original := FromSlice([]string{"x", "y", "z"}) 358 | clone := original.Clone() 359 | 360 | if !original.Equals(clone) { 361 | t.Errorf("Clone() content mismatch. Original: %v, Clone: %v", original.ToSlice(), clone.ToSlice()) 362 | } 363 | 364 | if original.Size() != clone.Size() { 365 | t.Errorf("Clone() size mismatch. Original: %d, Clone: %d", original.Size(), clone.Size()) 366 | } 367 | 368 | // Modify clone and check original 369 | clone.Push("a") 370 | 371 | if original.Contains("a") { 372 | t.Error("Original set modified when clone was changed (Push)") 373 | } 374 | 375 | if original.Size() == clone.Size() { 376 | t.Error("Original set size changed when clone was changed (Push)") 377 | } 378 | 379 | clone.Remove("x") 380 | 381 | if !original.Contains("x") { 382 | t.Error("Original set modified when clone was changed (Remove)") 383 | } 384 | 385 | // Test cloning an empty set 386 | emptyOriginal := New[int]() 387 | emptyClone := emptyOriginal.Clone() 388 | 389 | if !emptyClone.IsEmpty() { 390 | t.Error("Clone of empty set is not empty") 391 | } 392 | 393 | emptyClone.Push(100) 394 | 395 | if !emptyOriginal.IsEmpty() { 396 | t.Error("Original empty set modified when its clone was changed") 397 | } 398 | } 399 | 400 | func TestSet_Union(t *testing.T) { 401 | s1 := FromSlice([]int{1, 2, 3}) 402 | s2 := FromSlice([]int{3, 4, 5}) 403 | expectedUnion := FromSlice([]int{1, 2, 3, 4, 5}) 404 | resultUnion := s1.Union(s2) 405 | 406 | if !resultUnion.Equals(expectedUnion) { 407 | t.Errorf("s1.Union(s2) = %v, want %v", resultUnion.ToSlice(), expectedUnion.ToSlice()) 408 | } 409 | 410 | // Union with empty set 411 | sEmpty := New[int]() 412 | resultUnionEmpty := s1.Union(sEmpty) 413 | 414 | if !resultUnionEmpty.Equals(s1) { 415 | t.Errorf("s1.Union(empty) = %v, want %v", resultUnionEmpty.ToSlice(), s1.ToSlice()) 416 | } 417 | 418 | resultEmptyUnionS1 := sEmpty.Union(s1) 419 | 420 | if !resultEmptyUnionS1.Equals(s1) { 421 | t.Errorf("empty.Union(s1) = %v, want %v", resultEmptyUnionS1.ToSlice(), s1.ToSlice()) 422 | } 423 | 424 | // Ensure original sets are not modified 425 | if !s1.Equals(FromSlice([]int{1, 2, 3})) { 426 | t.Error("Original set s1 modified by Union operation") 427 | } 428 | 429 | if !s2.Equals(FromSlice([]int{3, 4, 5})) { 430 | t.Error("Original set s2 modified by Union operation") 431 | } 432 | } 433 | 434 | func TestSet_Intersection(t *testing.T) { 435 | s1 := FromSlice([]int{1, 2, 3, 6}) 436 | s2 := FromSlice([]int{3, 4, 5, 6}) 437 | expectedIntersection := FromSlice([]int{3, 6}) 438 | resultIntersection := s1.Intersection(s2) 439 | 440 | if !resultIntersection.Equals(expectedIntersection) { 441 | t.Errorf("s1.Intersection(s2) = %v, want %v", resultIntersection.ToSlice(), expectedIntersection.ToSlice()) 442 | } 443 | 444 | // Intersection with no common elements 445 | s3 := FromSlice([]int{7, 8}) 446 | expectedNoIntersection := New[int]() 447 | resultNoIntersection := s1.Intersection(s3) 448 | 449 | if !resultNoIntersection.Equals(expectedNoIntersection) { 450 | t.Errorf("s1.Intersection(s3) = %v, want %v (empty set)", resultNoIntersection.ToSlice(), expectedNoIntersection.ToSlice()) 451 | } 452 | 453 | // Intersection with empty set 454 | sEmpty := New[int]() 455 | resultIntersectionEmpty := s1.Intersection(sEmpty) 456 | 457 | if !resultIntersectionEmpty.Equals(sEmpty) { 458 | t.Errorf("s1.Intersection(empty) = %v, want %v (empty set)", resultIntersectionEmpty.ToSlice(), sEmpty.ToSlice()) 459 | } 460 | 461 | // Test optimization: s1 smaller than s2 462 | sSmall := FromSlice([]int{3}) 463 | sLarge := FromSlice([]int{1, 2, 3, 4, 5}) 464 | expectedSmallLarge := FromSlice([]int{3}) 465 | resultSmallLarge := sSmall.Intersection(sLarge) 466 | 467 | if !resultSmallLarge.Equals(expectedSmallLarge) { 468 | t.Errorf("sSmall.Intersection(sLarge) = %v, want %v", resultSmallLarge.ToSlice(), expectedSmallLarge.ToSlice()) 469 | } 470 | 471 | // Test the other way too 472 | resultLargeSmall := sLarge.Intersection(sSmall) 473 | 474 | if !resultLargeSmall.Equals(expectedSmallLarge) { 475 | t.Errorf("sLarge.Intersection(sSmall) = %v, want %v", resultLargeSmall.ToSlice(), expectedSmallLarge.ToSlice()) 476 | } 477 | 478 | // Ensure original sets are not modified 479 | if !s1.Equals(FromSlice([]int{1, 2, 3, 6})) { 480 | t.Error("Original set s1 modified by Intersection operation") 481 | } 482 | 483 | if !s2.Equals(FromSlice([]int{3, 4, 5, 6})) { 484 | t.Error("Original set s2 modified by Intersection operation") 485 | } 486 | } 487 | 488 | func TestSet_Difference(t *testing.T) { 489 | s1 := FromSlice([]int{1, 2, 3, 4}) 490 | s2 := FromSlice([]int{3, 4, 5, 6}) 491 | 492 | // s1 - s2 493 | expectedDifference := FromSlice([]int{1, 2}) 494 | resultDifference := s1.Difference(s2) 495 | 496 | if !resultDifference.Equals(expectedDifference) { 497 | t.Errorf("s1.Difference(s2) = %v, want %v", resultDifference.ToSlice(), expectedDifference.ToSlice()) 498 | } 499 | 500 | // s2 - s1 501 | expectedDifference2 := FromSlice([]int{5, 6}) 502 | resultDifference2 := s2.Difference(s1) 503 | 504 | if !resultDifference2.Equals(expectedDifference2) { 505 | t.Errorf("s2.Difference(s1) = %v, want %v", resultDifference2.ToSlice(), expectedDifference2.ToSlice()) 506 | } 507 | 508 | // Difference with empty set 509 | sEmpty := New[int]() 510 | resultDiffEmpty := s1.Difference(sEmpty) 511 | 512 | if !resultDiffEmpty.Equals(s1) { 513 | t.Errorf("s1.Difference(empty) = %v, want %v", resultDiffEmpty.ToSlice(), s1.ToSlice()) 514 | } 515 | 516 | resultEmptyDiffS1 := sEmpty.Difference(s1) 517 | 518 | if !resultEmptyDiffS1.IsEmpty() { 519 | t.Errorf("empty.Difference(s1) = %v, want empty set", resultEmptyDiffS1.ToSlice()) 520 | } 521 | 522 | // Difference with no common elements 523 | s3 := FromSlice([]int{10, 11}) 524 | resultDiffNoCommon := s1.Difference(s3) 525 | 526 | if !resultDiffNoCommon.Equals(s1) { 527 | t.Errorf("s1.Difference(s3_no_common) = %v, want %v", resultDiffNoCommon.ToSlice(), s1.ToSlice()) 528 | } 529 | 530 | // Ensure original sets are not modified 531 | if !s1.Equals(FromSlice([]int{1, 2, 3, 4})) { 532 | t.Error("Original set s1 modified by Difference operation") 533 | } 534 | 535 | if !s2.Equals(FromSlice([]int{3, 4, 5, 6})) { 536 | t.Error("Original set s2 modified by Difference operation") 537 | } 538 | } 539 | 540 | func TestSet_SymmetricDifference(t *testing.T) { 541 | s1 := FromSlice([]int{1, 2, 3, 4}) 542 | s2 := FromSlice([]int{3, 4, 5, 6}) 543 | 544 | // (s1 U s2) - (s1 I s2) = {1,2,3,4,5,6} - {3,4} = {1,2,5,6} 545 | expectedSymDiff := FromSlice([]int{1, 2, 5, 6}) 546 | resultSymDiff := s1.SymmetricDifference(s2) 547 | 548 | if !resultSymDiff.Equals(expectedSymDiff) { 549 | t.Errorf("s1.SymmetricDifference(s2) = %v, want %v", resultSymDiff.ToSlice(), expectedSymDiff.ToSlice()) 550 | } 551 | 552 | // Symmetric difference with empty set 553 | sEmpty := New[int]() 554 | resultSymDiffEmpty := s1.SymmetricDifference(sEmpty) 555 | 556 | if !resultSymDiffEmpty.Equals(s1) { 557 | t.Errorf("s1.SymmetricDifference(empty) = %v, want %v", resultSymDiffEmpty.ToSlice(), s1.ToSlice()) 558 | } 559 | 560 | resultEmptySymDiffS1 := sEmpty.SymmetricDifference(s1) 561 | 562 | if !resultEmptySymDiffS1.Equals(s1) { 563 | t.Errorf("empty.SymmetricDifference(s1) = %v, want %v", resultEmptySymDiffS1.ToSlice(), s1.ToSlice()) 564 | } 565 | 566 | // Symmetric difference with disjoint sets 567 | s3 := FromSlice([]int{10, 11}) // Disjoint from s1 568 | expectedDisjointSymDiff := FromSlice([]int{1, 2, 3, 4, 10, 11}) 569 | resultDisjointSymDiff := s1.SymmetricDifference(s3) 570 | 571 | if !resultDisjointSymDiff.Equals(expectedDisjointSymDiff) { 572 | t.Errorf("s1.SymmetricDifference(s3_disjoint) = %v, want %v", resultDisjointSymDiff.ToSlice(), expectedDisjointSymDiff.ToSlice()) 573 | } 574 | 575 | // Ensure original sets are not modified 576 | if !s1.Equals(FromSlice([]int{1, 2, 3, 4})) { 577 | t.Error("Original set s1 modified by SymmetricDifference operation") 578 | } 579 | 580 | if !s2.Equals(FromSlice([]int{3, 4, 5, 6})) { 581 | t.Error("Original set s2 modified by SymmetricDifference operation") 582 | } 583 | } 584 | 585 | func TestSet_IsSubsetOf(t *testing.T) { 586 | s1 := FromSlice([]int{1, 2}) 587 | s2 := FromSlice([]int{1, 2, 3}) 588 | s3 := FromSlice([]int{1, 3, 4}) 589 | sEmpty := New[int]() 590 | 591 | if !s1.IsSubsetOf(s2) { 592 | t.Errorf("%v.IsSubsetOf(%v) = false, want true", s1.ToSlice(), s2.ToSlice()) 593 | } 594 | 595 | if s2.IsSubsetOf(s1) { 596 | t.Errorf("%v.IsSubsetOf(%v) = true, want false", s2.ToSlice(), s1.ToSlice()) 597 | } 598 | 599 | if s1.IsSubsetOf(s3) { 600 | t.Errorf("%v.IsSubsetOf(%v) = true, want false", s1.ToSlice(), s3.ToSlice()) 601 | } 602 | 603 | // Empty set is subset of any set 604 | if !sEmpty.IsSubsetOf(s1) { 605 | t.Errorf("empty.IsSubsetOf(%v) = false, want true", s1.ToSlice()) 606 | } 607 | 608 | // Set is subset of itself 609 | if !s1.IsSubsetOf(s1) { 610 | t.Errorf("%v.IsSubsetOf(%v) = false, want true", s1.ToSlice(), s1.ToSlice()) 611 | } 612 | 613 | // Non-empty set cannot be subset of empty set 614 | if s1.IsSubsetOf(sEmpty) && s1.Size() > 0 { 615 | t.Errorf("%v.IsSubsetOf(empty) = true, want false", s1.ToSlice()) 616 | } 617 | } 618 | 619 | func TestSet_Equals(t *testing.T) { 620 | s1 := FromSlice([]int{1, 2, 3}) 621 | s2 := FromSlice([]int{3, 2, 1}) // Same elements, different order 622 | s3 := FromSlice([]int{1, 2, 4}) 623 | s4 := FromSlice([]int{1, 2}) 624 | sEmpty1 := New[int]() 625 | sEmpty2 := New[int]() 626 | 627 | if !s1.Equals(s2) { 628 | t.Errorf("%v.Equals(%v) = false, want true", s1.ToSlice(), s2.ToSlice()) 629 | } 630 | 631 | if s1.Equals(s3) { 632 | t.Errorf("%v.Equals(%v) = true, want false", s1.ToSlice(), s3.ToSlice()) 633 | } 634 | 635 | // Different size 636 | if s1.Equals(s4) { 637 | t.Errorf("%v.Equals(%v) = true, want false", s1.ToSlice(), s4.ToSlice()) 638 | } 639 | 640 | // Different size (other way) 641 | if s4.Equals(s1) { 642 | t.Errorf("%v.Equals(%v) = true, want false", s4.ToSlice(), s1.ToSlice()) 643 | } 644 | 645 | if !sEmpty1.Equals(sEmpty2) { 646 | t.Error("emptySet1.Equals(emptySet2) = false, want true") 647 | } 648 | 649 | if s1.Equals(sEmpty1) { 650 | t.Errorf("%v.Equals(empty) = true, want false", s1.ToSlice()) 651 | } 652 | } 653 | 654 | func TestSet_ToSlice(t *testing.T) { 655 | s := New[string]() 656 | s.Push("hello", "world", "go") 657 | expectedElements := []string{"hello", "world", "go"} 658 | sliceResult := s.ToSlice() 659 | 660 | if len(sliceResult) != len(expectedElements) { 661 | t.Fatalf("ToSlice() length = %d, want %d. Got: %v", len(sliceResult), len(expectedElements), sliceResult) 662 | } 663 | 664 | // Check if all expected elements are in the slice (order doesn't matter) 665 | tempSet := FromSlice(sliceResult) 666 | 667 | for _, item := range expectedElements { 668 | if !tempSet.Contains(item) { 669 | t.Errorf("ToSlice() result %v missing element %q from original set %v", sliceResult, item, expectedElements) 670 | } 671 | } 672 | 673 | // Test on empty set 674 | emptyS := New[int]() 675 | emptySliceResult := emptyS.ToSlice() 676 | 677 | if len(emptySliceResult) != 0 { 678 | t.Errorf("ToSlice() on empty set returned slice of length %d, want 0. Got: %v", len(emptySliceResult), emptySliceResult) 679 | } 680 | } 681 | -------------------------------------------------------------------------------- /datastructs/set/syncset.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/PsionicAlch/byteforge/internal/functions/utils" 7 | ) 8 | 9 | // SyncSet implements a generic set data structure with thread-safety 10 | type SyncSet[T comparable] struct { 11 | mu sync.RWMutex 12 | set *Set[T] 13 | } 14 | 15 | // NewSync creates a new empty SyncSet with an optional initial capacity 16 | func NewSync[T comparable](size ...int) *SyncSet[T] { 17 | return &SyncSet[T]{ 18 | set: New[T](size...), 19 | } 20 | } 21 | 22 | // SyncFromSlice creates a new SyncSet from a slice of items 23 | func SyncFromSlice[T comparable](data []T) *SyncSet[T] { 24 | return &SyncSet[T]{ 25 | set: FromSlice(data), 26 | } 27 | } 28 | 29 | // FromSet creates a new SyncSet from a Set 30 | func FromSet[T comparable](set *Set[T]) *SyncSet[T] { 31 | return &SyncSet[T]{ 32 | set: set.Clone(), 33 | } 34 | } 35 | 36 | // Contains checks if the SyncSet contains the specific item 37 | func (s *SyncSet[T]) Contains(item T) bool { 38 | s.mu.RLock() 39 | defer s.mu.RUnlock() 40 | 41 | return s.set.Contains(item) 42 | } 43 | 44 | // Push adds one or more items to the SyncSet 45 | func (s *SyncSet[T]) Push(items ...T) { 46 | s.mu.Lock() 47 | defer s.mu.Unlock() 48 | 49 | s.set.Push(items...) 50 | } 51 | 52 | // Pop removes and returns an arbitrary element from the SyncSet 53 | // 54 | // Note: The selection of which element to pop is non-deterministic due to Go's map iteration order 55 | func (s *SyncSet[T]) Pop() (T, bool) { 56 | s.mu.Lock() 57 | defer s.mu.Unlock() 58 | 59 | return s.set.Pop() 60 | } 61 | 62 | // Peek returns an arbitrary element from the SyncSet without removing it 63 | // 64 | // Note: The selection of which element to peek is non-deterministic due to Go's map iteration order 65 | func (s *SyncSet[T]) Peek() (T, bool) { 66 | s.mu.RLock() 67 | defer s.mu.RUnlock() 68 | 69 | return s.set.Peek() 70 | } 71 | 72 | // Size returns the number of elements in the SyncSet 73 | func (s *SyncSet[T]) Size() int { 74 | s.mu.RLock() 75 | defer s.mu.RUnlock() 76 | 77 | return s.set.Size() 78 | } 79 | 80 | // IsEmpty returns true if the SyncSet contains no elements 81 | func (s *SyncSet[T]) IsEmpty() bool { 82 | s.mu.RLock() 83 | defer s.mu.RUnlock() 84 | 85 | return s.set.IsEmpty() 86 | } 87 | 88 | // Iter returns an iterator over the Set's elements 89 | // 90 | // Note: Iter returns a snapshot iterator (not live-updated) 91 | func (s *SyncSet[T]) Iter() func(func(T) bool) { 92 | s.mu.RLock() 93 | defer s.mu.RUnlock() 94 | 95 | // Take a snapshot slice and return its iterator 96 | snapshot := s.set.ToSlice() 97 | return func(yield func(T) bool) { 98 | for _, item := range snapshot { 99 | if !yield(item) { 100 | return 101 | } 102 | } 103 | } 104 | } 105 | 106 | // Remove deletes an item from the SyncSet and returns whether it was present 107 | func (s *SyncSet[T]) Remove(item T) bool { 108 | s.mu.Lock() 109 | defer s.mu.Unlock() 110 | 111 | return s.set.Remove(item) 112 | } 113 | 114 | // Clear removes all elements from the SyncSet 115 | func (s *SyncSet[T]) Clear() { 116 | s.mu.Lock() 117 | defer s.mu.Unlock() 118 | 119 | s.set.Clear() 120 | } 121 | 122 | // Clone creates a new Set with the same elements 123 | func (s *SyncSet[T]) Clone() *SyncSet[T] { 124 | s.mu.RLock() 125 | defer s.mu.RUnlock() 126 | 127 | return &SyncSet[T]{ 128 | set: s.set.Clone(), 129 | } 130 | } 131 | 132 | // Union returns a new SyncSet containing all elements from both SyncSets 133 | func (s *SyncSet[T]) Union(other *SyncSet[T]) *SyncSet[T] { 134 | // Lock both in address order to avoid deadlock 135 | first, second := utils.SortByAddress(s, other) 136 | 137 | first.mu.RLock() 138 | defer first.mu.RUnlock() 139 | 140 | second.mu.RLock() 141 | defer second.mu.RUnlock() 142 | 143 | return FromSet(s.set.Union(other.set)) 144 | } 145 | 146 | // Intersection returns a new SyncSet containing elements present in both SyncSets 147 | func (s *SyncSet[T]) Intersection(other *SyncSet[T]) *SyncSet[T] { 148 | // Lock both in address order to avoid deadlock 149 | first, second := utils.SortByAddress(s, other) 150 | 151 | first.mu.RLock() 152 | defer first.mu.RUnlock() 153 | 154 | second.mu.RLock() 155 | defer second.mu.RUnlock() 156 | 157 | return FromSet(s.set.Intersection(other.set)) 158 | } 159 | 160 | // Difference returns a new SyncSet containing elements in s that are not in other 161 | func (s *SyncSet[T]) Difference(other *SyncSet[T]) *SyncSet[T] { 162 | // Lock both in address order to avoid deadlock 163 | first, second := utils.SortByAddress(s, other) 164 | 165 | first.mu.RLock() 166 | defer first.mu.RUnlock() 167 | 168 | second.mu.RLock() 169 | defer second.mu.RUnlock() 170 | 171 | return FromSet(s.set.Difference(other.set)) 172 | } 173 | 174 | // SymmetricDifference returns a new SyncSet with elements in either SyncSet but not in both 175 | func (s *SyncSet[T]) SymmetricDifference(other *SyncSet[T]) *SyncSet[T] { 176 | // Lock both in address order to avoid deadlock 177 | first, second := utils.SortByAddress(s, other) 178 | 179 | first.mu.RLock() 180 | defer first.mu.RUnlock() 181 | 182 | second.mu.RLock() 183 | defer second.mu.RUnlock() 184 | 185 | return FromSet(s.set.SymmetricDifference(other.set)) 186 | } 187 | 188 | // IsSubsetOf returns true if all elements in s are also in other 189 | func (s *SyncSet[T]) IsSubsetOf(other *SyncSet[T]) bool { 190 | // Lock both in address order to avoid deadlock 191 | first, second := utils.SortByAddress(s, other) 192 | 193 | first.mu.RLock() 194 | defer first.mu.RUnlock() 195 | 196 | second.mu.RLock() 197 | defer second.mu.RUnlock() 198 | 199 | return s.set.IsSubsetOf(other.set) 200 | } 201 | 202 | // Equals returns true if both sets contain exactly the same elements 203 | func (s *SyncSet[T]) Equals(other *SyncSet[T]) bool { 204 | // Lock both in address order to avoid deadlock 205 | first, second := utils.SortByAddress(s, other) 206 | 207 | first.mu.RLock() 208 | defer first.mu.RUnlock() 209 | 210 | second.mu.RLock() 211 | defer second.mu.RUnlock() 212 | 213 | return s.set.Equals(other.set) 214 | } 215 | 216 | // ToSlice returns all elements of the Set as a slice 217 | func (s *SyncSet[T]) ToSlice() []T { 218 | s.mu.RLock() 219 | defer s.mu.RUnlock() 220 | 221 | return s.set.ToSlice() 222 | } 223 | -------------------------------------------------------------------------------- /datastructs/set/syncset_test.go: -------------------------------------------------------------------------------- 1 | package set 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "testing" 7 | ) 8 | 9 | // TODO: Some of these tests don't check to ensure that the underlying 10 | // structure is truly thread-safe. It's hard to test because the data would 11 | // need to change on the fly. Hopefully older me will be capable of writing 12 | // better tests. 13 | 14 | func TestSyncSet_NewSync(t *testing.T) { 15 | s := NewSync[int]() 16 | 17 | if s == nil { 18 | t.Error("Expected s to be non-nil.") 19 | } 20 | 21 | if s.set == nil { 22 | t.Error("Expected s.set to be non-nil.") 23 | } 24 | } 25 | 26 | func TestSyncSet_SyncFromSlice(t *testing.T) { 27 | data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 28 | s := SyncFromSlice(data) 29 | 30 | if s == nil { 31 | t.Error("Expected s to be non-nil.") 32 | } 33 | 34 | if s.set == nil { 35 | t.Error("Expected s.set to be non-nil.") 36 | } 37 | 38 | for _, num := range data { 39 | if !s.Contains(num) { 40 | t.Errorf("Expected s to contain %d but didn't.", num) 41 | } 42 | } 43 | } 44 | 45 | func TestSyncSet_FromSet(t *testing.T) { 46 | s1 := FromSlice([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) 47 | s2 := FromSet(s1) 48 | 49 | if !s2.set.Equals(s1) { 50 | t.Error("Expected s1 and s2 to be equal but they are not.") 51 | } 52 | } 53 | 54 | func TestSyncSet_Contains(t *testing.T) { 55 | const max = 100 56 | 57 | s := NewSync[int]() 58 | for i := 0; i < max; i++ { 59 | s.Push(i) 60 | } 61 | 62 | wg := sync.WaitGroup{} 63 | 64 | for i := 0; i < max; i++ { 65 | wg.Add(1) 66 | go func(i int) { 67 | defer wg.Done() 68 | if !s.Contains(i) { 69 | t.Errorf("Expected to contain %d", i) 70 | } 71 | }(i) 72 | } 73 | wg.Wait() 74 | } 75 | 76 | func TestSyncSet_Push(t *testing.T) { 77 | s := NewSync[int]() 78 | elements := []int{} 79 | wg := sync.WaitGroup{} 80 | 81 | for i := 0; i < 100; i++ { 82 | wg.Add(1) 83 | elements = append(elements, i) 84 | go func(i int) { 85 | defer wg.Done() 86 | s.Push(i) 87 | }(i) 88 | } 89 | 90 | wg.Wait() 91 | 92 | if s.Size() != 100 { 93 | t.Errorf("Expected size 100, got %d", s.Size()) 94 | } 95 | 96 | if !s.set.Equals(FromSlice(elements)) { 97 | t.Errorf("Expected s to contain %#v", elements) 98 | } 99 | } 100 | 101 | func TestSyncSet_Pop(t *testing.T) { 102 | var elements []int 103 | for i := 0; i < 100; i++ { 104 | elements = append(elements, i) 105 | } 106 | 107 | s1 := SyncFromSlice(elements) 108 | s2 := SyncFromSlice(elements) 109 | 110 | var wg sync.WaitGroup 111 | 112 | for range elements { 113 | wg.Add(1) 114 | go func() { 115 | defer wg.Done() 116 | 117 | element, found := s1.Pop() 118 | if !found { 119 | t.Error("Expected to receive element when popping off s1.") 120 | } else { 121 | if !s2.Contains(element) { 122 | t.Errorf("Expected s1 to contain %d but it did not.", element) 123 | } 124 | } 125 | }() 126 | } 127 | 128 | wg.Wait() 129 | 130 | if s1.Size() != 0 { 131 | t.Errorf("Expected s1 size to be 0. Got %d", s1.Size()) 132 | } 133 | } 134 | 135 | func TestSyncSet_Peek(t *testing.T) { 136 | var elements []int 137 | for i := 0; i < 100; i++ { 138 | elements = append(elements, i) 139 | } 140 | 141 | s := SyncFromSlice(elements) 142 | initialSize := s.Size() 143 | 144 | var wg sync.WaitGroup 145 | 146 | for range elements { 147 | wg.Add(1) 148 | go func() { 149 | defer wg.Done() 150 | 151 | element, found := s.Peek() 152 | if !found { 153 | t.Error("Expected to receive element when peeping s.") 154 | } else { 155 | if !s.Contains(element) { 156 | t.Error("Peeping returned an element that's no longer in s.") 157 | } 158 | } 159 | }() 160 | } 161 | 162 | wg.Wait() 163 | 164 | if initialSize != s.Size() { 165 | t.Errorf("Expected s.Size() to be %d. Found: %d", initialSize, s.Size()) 166 | } 167 | } 168 | 169 | func TestSyncSet_Size(t *testing.T) { 170 | var elements []int 171 | for i := 0; i < 100; i++ { 172 | elements = append(elements, i) 173 | } 174 | 175 | s := SyncFromSlice(elements) 176 | initialSize := s.Size() 177 | 178 | var wg sync.WaitGroup 179 | 180 | for range elements { 181 | wg.Add(1) 182 | go func() { 183 | defer wg.Done() 184 | 185 | if s.Size() != initialSize { 186 | t.Errorf("Expected s.Size() to be %d. Found: %d", initialSize, s.Size()) 187 | } 188 | }() 189 | } 190 | 191 | wg.Wait() 192 | } 193 | 194 | func TestSyncSet_IsEmpty(t *testing.T) { 195 | var elements []int 196 | for i := 0; i < 100; i++ { 197 | elements = append(elements, i) 198 | } 199 | 200 | s := NewSync[int]() 201 | 202 | var wg sync.WaitGroup 203 | 204 | for range elements { 205 | wg.Add(1) 206 | go func() { 207 | defer wg.Done() 208 | 209 | if !s.IsEmpty() { 210 | t.Error("Expected s.IsEmpty() to be true") 211 | } 212 | }() 213 | } 214 | 215 | wg.Wait() 216 | } 217 | 218 | func TestSyncSet_Iter(t *testing.T) { 219 | var elements []int 220 | for i := 0; i < 100; i++ { 221 | elements = append(elements, i) 222 | } 223 | 224 | s := SyncFromSlice(elements) 225 | 226 | // Take snapshot iterator before concurrent writes happen. 227 | iter := s.Iter() 228 | seen := map[int]bool{} 229 | iter(func(i int) bool { 230 | seen[i] = true 231 | return true 232 | }) 233 | 234 | // Start concurrent writes. 235 | var wg sync.WaitGroup 236 | for i := 100; i < 200; i++ { 237 | wg.Add(1) 238 | go func(i int) { 239 | defer wg.Done() 240 | s.Push(i) 241 | }(i) 242 | } 243 | 244 | wg.Wait() 245 | 246 | // Validate snapshot has only values from the original [0, 99] range. 247 | for i := 0; i < 100; i++ { 248 | if !seen[i] { 249 | t.Errorf("Snapshot missing expected item: %d", i) 250 | } 251 | } 252 | 253 | for i := 100; i < 200; i++ { 254 | if seen[i] { 255 | t.Errorf("Snapshot unexpectedly included item: %d", i) 256 | } 257 | } 258 | } 259 | 260 | func TestSyncSet_Remove(t *testing.T) { 261 | const goroutines = 50 262 | const target = 42 263 | 264 | s := NewSync[int]() 265 | s.Push(target) 266 | 267 | var wg sync.WaitGroup 268 | successCount := int32(0) 269 | 270 | for i := 0; i < goroutines; i++ { 271 | wg.Add(1) 272 | go func() { 273 | defer wg.Done() 274 | if s.Remove(target) { 275 | atomic.AddInt32(&successCount, 1) 276 | } 277 | }() 278 | } 279 | 280 | wg.Wait() 281 | 282 | if successCount != 1 { 283 | t.Errorf("Expected exactly one successful removal, got %d", successCount) 284 | } 285 | 286 | if s.Contains(target) { 287 | t.Error("Item should no longer be in the set after removal") 288 | } 289 | } 290 | 291 | func TestSyncSet_Clear(t *testing.T) { 292 | const max = 1000 293 | var elements []int 294 | for i := 0; i < max; i++ { 295 | elements = append(elements, i) 296 | } 297 | 298 | s := SyncFromSlice(elements) 299 | 300 | var wg sync.WaitGroup 301 | clears := 10 302 | pushes := 100 303 | 304 | // Start concurrent Clear calls 305 | for i := 0; i < clears; i++ { 306 | wg.Add(1) 307 | go func() { 308 | defer wg.Done() 309 | s.Clear() 310 | }() 311 | } 312 | 313 | // Start concurrent Push calls 314 | for i := 1000; i < max+pushes; i++ { 315 | wg.Add(1) 316 | go func(i int) { 317 | defer wg.Done() 318 | s.Push(i) 319 | }(i) 320 | } 321 | 322 | wg.Wait() 323 | 324 | // Validate that all items were either cleared or the push happened after the final clear 325 | items := s.ToSlice() 326 | for _, v := range items { 327 | if v < 1000 { 328 | t.Errorf("Old item %d should have been cleared", v) 329 | } 330 | } 331 | } 332 | 333 | func TestSyncSet_Clone(t *testing.T) { 334 | var elements []int 335 | for i := 0; i < 100; i++ { 336 | elements = append(elements, i) 337 | } 338 | 339 | s := SyncFromSlice(elements) 340 | 341 | var clones []*SyncSet[int] 342 | var wg sync.WaitGroup 343 | var mu sync.Mutex 344 | 345 | for range elements { 346 | wg.Add(1) 347 | go func() { 348 | defer wg.Done() 349 | 350 | mu.Lock() 351 | clones = append(clones, s.Clone()) 352 | mu.Unlock() 353 | }() 354 | } 355 | 356 | wg.Wait() 357 | 358 | for _, clone := range clones { 359 | if !s.Equals(clone) { 360 | t.Error("Clone does not match original set.") 361 | } 362 | } 363 | } 364 | 365 | func TestSyncSet_Union(t *testing.T) { 366 | s1 := SyncFromSlice([]int{1, 2, 3}) 367 | s2 := SyncFromSlice([]int{3, 4, 5}) 368 | expectedUnion := SyncFromSlice([]int{1, 2, 3, 4, 5}) 369 | 370 | var wg sync.WaitGroup 371 | 372 | for i := 0; i < 100; i++ { 373 | wg.Add(1) 374 | go func() { 375 | defer wg.Done() 376 | if !s1.Union(s2).Equals(expectedUnion) { 377 | t.Error("Created union doesn't match expected union.") 378 | } 379 | }() 380 | } 381 | 382 | wg.Wait() 383 | } 384 | 385 | func TestSyncSet_Intersection(t *testing.T) { 386 | s1 := SyncFromSlice([]int{1, 2, 3, 6}) 387 | s2 := SyncFromSlice([]int{3, 4, 5, 6}) 388 | expectedIntersection := SyncFromSlice([]int{3, 6}) 389 | 390 | var wg sync.WaitGroup 391 | 392 | for i := 0; i < 100; i++ { 393 | wg.Add(1) 394 | go func() { 395 | defer wg.Done() 396 | if !s1.Intersection(s2).Equals(expectedIntersection) { 397 | t.Error("Created intersection doesn't match expected intersection.") 398 | } 399 | }() 400 | } 401 | 402 | wg.Wait() 403 | } 404 | 405 | func TestSyncSet_Difference(t *testing.T) { 406 | s1 := SyncFromSlice([]int{1, 2, 3, 4}) 407 | s2 := SyncFromSlice([]int{3, 4, 5, 6}) 408 | expectedDifference := SyncFromSlice([]int{1, 2}) 409 | 410 | var wg sync.WaitGroup 411 | 412 | for i := 0; i < 100; i++ { 413 | wg.Add(1) 414 | go func() { 415 | defer wg.Done() 416 | if !s1.Difference(s2).Equals(expectedDifference) { 417 | t.Error("Created difference doesn't match expected difference.") 418 | } 419 | }() 420 | } 421 | 422 | wg.Wait() 423 | } 424 | 425 | func TestSyncSet_SymmetricDifference(t *testing.T) { 426 | s1 := SyncFromSlice([]int{1, 2, 3, 4}) 427 | s2 := SyncFromSlice([]int{3, 4, 5, 6}) 428 | expectedSymDiff := SyncFromSlice([]int{1, 2, 5, 6}) 429 | 430 | var wg sync.WaitGroup 431 | 432 | for i := 0; i < 100; i++ { 433 | wg.Add(1) 434 | go func() { 435 | defer wg.Done() 436 | if !s1.SymmetricDifference(s2).Equals(expectedSymDiff) { 437 | t.Error("Created symmetric difference doesn't match expected symmetric difference.") 438 | } 439 | }() 440 | } 441 | 442 | wg.Wait() 443 | } 444 | 445 | func TestSyncSet_IsSubsetOf(t *testing.T) { 446 | s1 := SyncFromSlice([]int{1, 2}) 447 | s2 := SyncFromSlice([]int{1, 2, 3}) 448 | s3 := SyncFromSlice([]int{1, 3, 4}) 449 | sEmpty := NewSync[int]() 450 | 451 | var wg sync.WaitGroup 452 | 453 | for i := 0; i < 100; i++ { 454 | wg.Add(1) 455 | go func() { 456 | defer wg.Done() 457 | 458 | if !s1.IsSubsetOf(s2) { 459 | t.Errorf("%v.IsSubsetOf(%v) = false, want true", s1.ToSlice(), s2.ToSlice()) 460 | } 461 | 462 | if s2.IsSubsetOf(s1) { 463 | t.Errorf("%v.IsSubsetOf(%v) = true, want false", s2.ToSlice(), s1.ToSlice()) 464 | } 465 | 466 | if s1.IsSubsetOf(s3) { 467 | t.Errorf("%v.IsSubsetOf(%v) = true, want false", s1.ToSlice(), s3.ToSlice()) 468 | } 469 | 470 | // Empty set is subset of any set 471 | if !sEmpty.IsSubsetOf(s1) { 472 | t.Errorf("empty.IsSubsetOf(%v) = false, want true", s1.ToSlice()) 473 | } 474 | 475 | // Set is subset of itself 476 | if !s1.IsSubsetOf(s1) { 477 | t.Errorf("%v.IsSubsetOf(%v) = false, want true", s1.ToSlice(), s1.ToSlice()) 478 | } 479 | 480 | // Non-empty set cannot be subset of empty set 481 | if s1.IsSubsetOf(sEmpty) && s1.Size() > 0 { 482 | t.Errorf("%v.IsSubsetOf(empty) = true, want false", s1.ToSlice()) 483 | } 484 | }() 485 | } 486 | 487 | wg.Wait() 488 | } 489 | 490 | func TestSyncSet_Equals(t *testing.T) { 491 | s1 := SyncFromSlice([]int{1, 2, 3}) 492 | s2 := SyncFromSlice([]int{3, 2, 1}) 493 | s3 := SyncFromSlice([]int{1, 2, 4}) 494 | s4 := SyncFromSlice([]int{1, 2}) 495 | sEmpty1 := NewSync[int]() 496 | sEmpty2 := NewSync[int]() 497 | 498 | var wg sync.WaitGroup 499 | 500 | for i := 0; i < 100; i++ { 501 | wg.Add(1) 502 | go func() { 503 | defer wg.Done() 504 | 505 | if !s1.Equals(s2) { 506 | t.Errorf("%v.Equals(%v) = false, want true", s1.ToSlice(), s2.ToSlice()) 507 | } 508 | 509 | if s1.Equals(s3) { 510 | t.Errorf("%v.Equals(%v) = true, want false", s1.ToSlice(), s3.ToSlice()) 511 | } 512 | 513 | // Different size 514 | if s1.Equals(s4) { 515 | t.Errorf("%v.Equals(%v) = true, want false", s1.ToSlice(), s4.ToSlice()) 516 | } 517 | 518 | // Different size (other way) 519 | if s4.Equals(s1) { 520 | t.Errorf("%v.Equals(%v) = true, want false", s4.ToSlice(), s1.ToSlice()) 521 | } 522 | 523 | if !sEmpty1.Equals(sEmpty2) { 524 | t.Error("emptySet1.Equals(emptySet2) = false, want true") 525 | } 526 | 527 | if s1.Equals(sEmpty1) { 528 | t.Errorf("%v.Equals(empty) = true, want false", s1.ToSlice()) 529 | } 530 | }() 531 | } 532 | 533 | wg.Wait() 534 | } 535 | 536 | func TestSyncSet_ToSlice(t *testing.T) { 537 | s := New[string]() 538 | s.Push("hello", "world", "go") 539 | expectedElements := []string{"hello", "world", "go"} 540 | 541 | var wg sync.WaitGroup 542 | 543 | for i := 0; i < 100; i++ { 544 | wg.Add(1) 545 | go func() { 546 | defer wg.Done() 547 | 548 | sliceResult := s.ToSlice() 549 | 550 | if len(sliceResult) != len(expectedElements) { 551 | t.Errorf("ToSlice() length = %d, want %d. Got: %v", len(sliceResult), len(expectedElements), sliceResult) 552 | return 553 | } 554 | 555 | // Check if all expected elements are in the slice (order doesn't matter) 556 | tempSet := FromSlice(sliceResult) 557 | 558 | for _, item := range expectedElements { 559 | if !tempSet.Contains(item) { 560 | t.Errorf("ToSlice() result %v missing element %q from original set %v", sliceResult, item, expectedElements) 561 | } 562 | } 563 | 564 | // Test on empty set 565 | emptyS := New[int]() 566 | emptySliceResult := emptyS.ToSlice() 567 | 568 | if len(emptySliceResult) != 0 { 569 | t.Errorf("ToSlice() on empty set returned slice of length %d, want 0. Got: %v", len(emptySliceResult), emptySliceResult) 570 | } 571 | }() 572 | } 573 | 574 | wg.Wait() 575 | } 576 | -------------------------------------------------------------------------------- /datastructs/tuple/synctuple.go: -------------------------------------------------------------------------------- 1 | // Package tuple provides a generic, fixed-size tuple type with safe access and mutation. 2 | package tuple 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/PsionicAlch/byteforge/internal/datastructs/tuple" 8 | ) 9 | 10 | // SyncTuple represents a fixed-length collection of values of type T with thread-safety. 11 | // It supports safe element access, mutation, and conversion to/from slices. 12 | type SyncTuple[T any] struct { 13 | data *tuple.InternalTuple[T] 14 | mu sync.RWMutex 15 | } 16 | 17 | // New creates a new SyncTuple from the given variadic values. 18 | // The values are copied to ensure the SyncTuple does not alias external data. 19 | func NewSync[T any](vars ...T) *SyncTuple[T] { 20 | return &SyncTuple[T]{ 21 | data: tuple.New(vars...), 22 | } 23 | } 24 | 25 | // FromSlice creates a new SyncTuple by copying the contents of the provided slice. 26 | // The resulting Tuple has the same length as the input slice. 27 | func SyncFromSlice[T any](s []T) *SyncTuple[T] { 28 | return &SyncTuple[T]{ 29 | data: tuple.FromSlice(s), 30 | } 31 | } 32 | 33 | // Len returns the number of elements in the Tuple. 34 | func (t *SyncTuple[T]) Len() int { 35 | t.mu.RLock() 36 | defer t.mu.RUnlock() 37 | 38 | return t.data.Len() 39 | } 40 | 41 | // Get returns the element at the specified index and a boolean indicating success. 42 | // If the index is out of bounds, the zero value of T and false are returned. 43 | func (t *SyncTuple[T]) Get(index int) (T, bool) { 44 | t.mu.RLock() 45 | defer t.mu.RUnlock() 46 | 47 | return t.data.Get(index) 48 | } 49 | 50 | // Set updates the element at the specified index to the given value. 51 | // It returns true if the operation was successful, or false if the index was out of bounds. 52 | func (t *SyncTuple[T]) Set(index int, v T) bool { 53 | t.mu.Lock() 54 | defer t.mu.Unlock() 55 | 56 | return t.data.Set(index, v) 57 | } 58 | 59 | // ToSlice returns a copy of the SyncTuple's internal values as a slice. 60 | func (t *SyncTuple[T]) ToSlice() []T { 61 | t.mu.RLock() 62 | defer t.mu.RUnlock() 63 | 64 | return t.data.ToSlice() 65 | } 66 | 67 | // String returns a string representation of the SyncTuple's contents. 68 | func (t *SyncTuple[T]) String() string { 69 | t.mu.RLock() 70 | defer t.mu.RUnlock() 71 | 72 | return t.data.String() 73 | } 74 | -------------------------------------------------------------------------------- /datastructs/tuple/synctuple_test.go: -------------------------------------------------------------------------------- 1 | package tuple 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "sync" 7 | "testing" 8 | 9 | islices "github.com/PsionicAlch/byteforge/internal/functions/slices" 10 | ) 11 | 12 | func TestSyncTuple_New(t *testing.T) { 13 | scenarios := []struct { 14 | name string 15 | data []int 16 | }{ 17 | {"New with no elements", []int{}}, 18 | {"New with 1 elements", []int{1}}, 19 | {"New with 2 elements", []int{1, 2}}, 20 | {"New with 3 elements", []int{1, 2, 3}}, 21 | {"New with 100 elements", islices.ERange(0, 100)}, 22 | } 23 | 24 | for _, scenario := range scenarios { 25 | t.Run(scenario.name, func(t *testing.T) { 26 | tup := NewSync(scenario.data...) 27 | 28 | if tup == nil { 29 | t.Error("Expected tup to not be nil") 30 | } 31 | 32 | if tup.data == nil { 33 | t.Error("Expected tup.data to not be nil") 34 | } 35 | 36 | if tup.data.Len() != len(scenario.data) { 37 | t.Errorf("Expected tup.data.Len() to be %d. Got %d", len(scenario.data), tup.data.Len()) 38 | } 39 | 40 | if !slices.Equal(scenario.data, tup.data.ToSlice()) { 41 | t.Error("Expected tup.data.ToSlice() to be equal to scenario.data") 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestSyncTuple_FromSlice(t *testing.T) { 48 | scenarios := []struct { 49 | name string 50 | data []int 51 | }{ 52 | {"FromSlice with no elements", []int{}}, 53 | {"FromSlice with 1 elements", []int{1}}, 54 | {"FromSlice with 2 elements", []int{1, 2}}, 55 | {"FromSlice with 3 elements", []int{1, 2, 3}}, 56 | {"FromSlice with 100 elements", islices.ERange(0, 100)}, 57 | } 58 | 59 | for _, scenario := range scenarios { 60 | t.Run(scenario.name, func(t *testing.T) { 61 | tup := SyncFromSlice(scenario.data) 62 | 63 | if tup == nil { 64 | t.Error("Expected tup to not be nil") 65 | } 66 | 67 | if tup.data == nil { 68 | t.Error("Expected tup.vars to not be nil") 69 | } 70 | 71 | if tup.data.Len() != len(scenario.data) { 72 | t.Errorf("Expected tup.data.Len() to be %d. Got %d", len(scenario.data), tup.data.Len()) 73 | } 74 | 75 | if !slices.Equal(scenario.data, tup.data.ToSlice()) { 76 | t.Error("Expected tup.data.ToSlice() to be equal to scenario.data") 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestSyncTuple_Len(t *testing.T) { 83 | scenarios := []struct { 84 | name string 85 | data []int 86 | }{ 87 | {"Len with no elements", []int{}}, 88 | {"Len with 1 elements", []int{1}}, 89 | {"Len with 2 elements", []int{1, 2}}, 90 | {"Len with 3 elements", []int{1, 2, 3}}, 91 | {"Len with 100 elements", islices.ERange(0, 100)}, 92 | } 93 | 94 | for _, scenario := range scenarios { 95 | t.Run(scenario.name, func(t *testing.T) { 96 | tup := SyncFromSlice(scenario.data) 97 | 98 | var wg sync.WaitGroup 99 | 100 | for i := 0; i < 1000; i++ { 101 | wg.Add(1) 102 | go func() { 103 | defer wg.Done() 104 | 105 | if tup.Len() != len(scenario.data) { 106 | t.Errorf("Expected tup.Len() to be %d. Got %d", len(scenario.data), tup.data.Len()) 107 | } 108 | }() 109 | } 110 | 111 | wg.Wait() 112 | }) 113 | } 114 | } 115 | 116 | func TestSyncTuple_Get(t *testing.T) { 117 | scenarios := []struct { 118 | name string 119 | data []int 120 | }{ 121 | {"Get with no elements", []int{}}, 122 | {"Get with 1 elements", []int{1}}, 123 | {"Get with 2 elements", []int{1, 2}}, 124 | {"Get with 3 elements", []int{1, 2, 3}}, 125 | {"Get with 100 elements", islices.ERange(0, 100)}, 126 | } 127 | 128 | for _, scenario := range scenarios { 129 | t.Run(scenario.name, func(t *testing.T) { 130 | tup := SyncFromSlice(scenario.data) 131 | 132 | var wg sync.WaitGroup 133 | 134 | for i := 0; i < 1000; i++ { 135 | wg.Add(1) 136 | go func() { 137 | defer wg.Done() 138 | 139 | for index := range scenario.data { 140 | element, found := tup.Get(index) 141 | 142 | if !found { 143 | t.Errorf("Expected to find element at index %d", index) 144 | } else { 145 | if element != scenario.data[index] { 146 | t.Errorf("Expected element to be %d. Got %d", scenario.data[index], element) 147 | } 148 | } 149 | 150 | } 151 | 152 | _, found := tup.Get(len(scenario.data)) 153 | if found { 154 | t.Error("Found element at index outside the valid range.") 155 | } 156 | }() 157 | } 158 | 159 | wg.Wait() 160 | }) 161 | } 162 | } 163 | 164 | func TestSyncTuple_Set(t *testing.T) { 165 | tup := NewSync(1) 166 | 167 | var wg sync.WaitGroup 168 | 169 | for i := 0; i < 1000; i++ { 170 | wg.Add(1) 171 | go func() { 172 | defer wg.Done() 173 | 174 | if !tup.Set(0, i) { 175 | t.Error("Failed to set first element.") 176 | } 177 | 178 | if tup.Set(1, i) { 179 | t.Error("Set element outside of valid range.") 180 | } 181 | }() 182 | } 183 | 184 | wg.Wait() 185 | } 186 | 187 | func TestSyncTuple_ToSlice(t *testing.T) { 188 | scenarios := []struct { 189 | name string 190 | data []int 191 | }{ 192 | {"ToSlice with no elements", []int{}}, 193 | {"ToSlice with 1 elements", []int{1}}, 194 | {"ToSlice with 2 elements", []int{1, 2}}, 195 | {"ToSlice with 3 elements", []int{1, 2, 3}}, 196 | {"ToSlice with 100 elements", islices.ERange(0, 100)}, 197 | } 198 | 199 | for _, scenario := range scenarios { 200 | t.Run(scenario.name, func(t *testing.T) { 201 | tup := SyncFromSlice(scenario.data) 202 | 203 | var wg sync.WaitGroup 204 | 205 | for i := 0; i < 1000; i++ { 206 | wg.Add(1) 207 | go func() { 208 | defer wg.Done() 209 | 210 | if !slices.Equal(tup.ToSlice(), scenario.data) { 211 | t.Error("Expected tup.ToSlice() to be equal to scenario.data") 212 | } 213 | }() 214 | } 215 | 216 | wg.Wait() 217 | }) 218 | } 219 | } 220 | 221 | func TestSyncTuple_String(t *testing.T) { 222 | scenarios := []struct { 223 | name string 224 | data []int 225 | }{ 226 | {"String with no elements", []int{}}, 227 | {"String with 1 elements", []int{1}}, 228 | {"String with 2 elements", []int{1, 2}}, 229 | {"String with 3 elements", []int{1, 2, 3}}, 230 | {"String with 100 elements", islices.ERange(0, 100)}, 231 | } 232 | 233 | for _, scenario := range scenarios { 234 | t.Run(scenario.name, func(t *testing.T) { 235 | tup := FromSlice(scenario.data) 236 | s := fmt.Sprintf("%v", scenario.data) 237 | 238 | var wg sync.WaitGroup 239 | 240 | for i := 0; i < 1000; i++ { 241 | wg.Add(1) 242 | go func() { 243 | defer wg.Done() 244 | 245 | if tup.String() != s { 246 | t.Errorf("Expected tup.String() to be equal to \"%s\"", s) 247 | } 248 | }() 249 | } 250 | 251 | wg.Wait() 252 | }) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /datastructs/tuple/tuple.go: -------------------------------------------------------------------------------- 1 | // Package tuple provides a generic, fixed-size tuple type with safe access and mutation. 2 | package tuple 3 | 4 | import ( 5 | "github.com/PsionicAlch/byteforge/internal/datastructs/tuple" 6 | ) 7 | 8 | // Tuple represents a fixed-length collection of values of type T. 9 | // It supports safe element access, mutation, and conversion to/from slices. 10 | type Tuple[T any] struct { 11 | data *tuple.InternalTuple[T] 12 | } 13 | 14 | // New creates a new Tuple from the given variadic values. 15 | // The values are copied to ensure the Tuple does not alias external data. 16 | func New[T any](vars ...T) *Tuple[T] { 17 | return &Tuple[T]{ 18 | data: tuple.New(vars...), 19 | } 20 | } 21 | 22 | // FromSlice creates a new Tuple by copying the contents of the provided slice. 23 | // The resulting Tuple has the same length as the input slice. 24 | func FromSlice[T any](s []T) *Tuple[T] { 25 | return &Tuple[T]{ 26 | data: tuple.FromSlice(s), 27 | } 28 | } 29 | 30 | // Len returns the number of elements in the Tuple. 31 | func (t *Tuple[T]) Len() int { 32 | return t.data.Len() 33 | } 34 | 35 | // Get returns the element at the specified index and a boolean indicating success. 36 | // If the index is out of bounds, the zero value of T and false are returned. 37 | func (t *Tuple[T]) Get(index int) (T, bool) { 38 | return t.data.Get(index) 39 | } 40 | 41 | // Set updates the element at the specified index to the given value. 42 | // It returns true if the operation was successful, or false if the index was out of bounds. 43 | func (t *Tuple[T]) Set(index int, v T) bool { 44 | return t.data.Set(index, v) 45 | } 46 | 47 | // ToSlice returns a copy of the Tuple's internal values as a slice. 48 | func (t *Tuple[T]) ToSlice() []T { 49 | return t.data.ToSlice() 50 | } 51 | 52 | // String returns a string representation of the Tuple's contents. 53 | func (t *Tuple[T]) String() string { 54 | return t.data.String() 55 | } 56 | -------------------------------------------------------------------------------- /datastructs/tuple/tuple_test.go: -------------------------------------------------------------------------------- 1 | package tuple 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "testing" 7 | 8 | islices "github.com/PsionicAlch/byteforge/internal/functions/slices" 9 | ) 10 | 11 | func TestTuple_New(t *testing.T) { 12 | scenarios := []struct { 13 | name string 14 | data []int 15 | }{ 16 | {"New with no elements", []int{}}, 17 | {"New with 1 elements", []int{1}}, 18 | {"New with 2 elements", []int{1, 2}}, 19 | {"New with 3 elements", []int{1, 2, 3}}, 20 | {"New with 100 elements", islices.ERange(0, 100)}, 21 | } 22 | 23 | for _, scenario := range scenarios { 24 | t.Run(scenario.name, func(t *testing.T) { 25 | tup := New(scenario.data...) 26 | 27 | if tup == nil { 28 | t.Error("Expected tup to not be nil") 29 | } 30 | 31 | if tup.data == nil { 32 | t.Error("Expected tup.data to not be nil") 33 | } 34 | 35 | if tup.data.Len() != len(scenario.data) { 36 | t.Errorf("Expected tup.data.Len() to be %d. Got %d", len(scenario.data), tup.data.Len()) 37 | } 38 | 39 | if !slices.Equal(scenario.data, tup.data.ToSlice()) { 40 | t.Error("Expected tup.data.ToSlice() to be equal to scenario.data") 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestTuple_FromSlice(t *testing.T) { 47 | scenarios := []struct { 48 | name string 49 | data []int 50 | }{ 51 | {"FromSlice with no elements", []int{}}, 52 | {"FromSlice with 1 elements", []int{1}}, 53 | {"FromSlice with 2 elements", []int{1, 2}}, 54 | {"FromSlice with 3 elements", []int{1, 2, 3}}, 55 | {"FromSlice with 100 elements", islices.ERange(0, 100)}, 56 | } 57 | 58 | for _, scenario := range scenarios { 59 | t.Run(scenario.name, func(t *testing.T) { 60 | tup := FromSlice(scenario.data) 61 | 62 | if tup == nil { 63 | t.Error("Expected tup to not be nil") 64 | } 65 | 66 | if tup.data == nil { 67 | t.Error("Expected tup.vars to not be nil") 68 | } 69 | 70 | if tup.data.Len() != len(scenario.data) { 71 | t.Errorf("Expected tup.data.Len() to be %d. Got %d", len(scenario.data), tup.data.Len()) 72 | } 73 | 74 | if !slices.Equal(scenario.data, tup.data.ToSlice()) { 75 | t.Error("Expected tup.data.ToSlice() to be equal to scenario.data") 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func TestTuple_Len(t *testing.T) { 82 | scenarios := []struct { 83 | name string 84 | data []int 85 | }{ 86 | {"Len with no elements", []int{}}, 87 | {"Len with 1 elements", []int{1}}, 88 | {"Len with 2 elements", []int{1, 2}}, 89 | {"Len with 3 elements", []int{1, 2, 3}}, 90 | {"Len with 100 elements", islices.ERange(0, 100)}, 91 | } 92 | 93 | for _, scenario := range scenarios { 94 | t.Run(scenario.name, func(t *testing.T) { 95 | tup := FromSlice(scenario.data) 96 | 97 | if tup.Len() != len(scenario.data) { 98 | t.Errorf("Expected tup.Len() to be %d. Got %d", len(scenario.data), tup.data.Len()) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestTuple_Get(t *testing.T) { 105 | scenarios := []struct { 106 | name string 107 | data []int 108 | }{ 109 | {"Get with no elements", []int{}}, 110 | {"Get with 1 elements", []int{1}}, 111 | {"Get with 2 elements", []int{1, 2}}, 112 | {"Get with 3 elements", []int{1, 2, 3}}, 113 | {"Get with 100 elements", islices.ERange(0, 100)}, 114 | } 115 | 116 | for _, scenario := range scenarios { 117 | t.Run(scenario.name, func(t *testing.T) { 118 | tup := FromSlice(scenario.data) 119 | 120 | for index := range scenario.data { 121 | element, found := tup.Get(index) 122 | 123 | if !found { 124 | t.Errorf("Expected to find element at index %d", index) 125 | } else { 126 | if element != scenario.data[index] { 127 | t.Errorf("Expected element to be %d. Got %d", scenario.data[index], element) 128 | } 129 | } 130 | 131 | } 132 | 133 | _, found := tup.Get(len(scenario.data)) 134 | if found { 135 | t.Error("Found element at index outside the valid range.") 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestTuple_Set(t *testing.T) { 142 | tup := New(1) 143 | 144 | if !tup.Set(0, 2) { 145 | t.Error("Failed to set first element.") 146 | } 147 | 148 | if element, found := tup.Get(0); found && element != 2 { 149 | t.Errorf("Expected element to be 2. Got %d", element) 150 | } 151 | 152 | if tup.Set(1, 2) { 153 | t.Error("Set element outside of valid range.") 154 | } 155 | } 156 | 157 | func TestTuple_ToSlice(t *testing.T) { 158 | scenarios := []struct { 159 | name string 160 | data []int 161 | }{ 162 | {"ToSlice with no elements", []int{}}, 163 | {"ToSlice with 1 elements", []int{1}}, 164 | {"ToSlice with 2 elements", []int{1, 2}}, 165 | {"ToSlice with 3 elements", []int{1, 2, 3}}, 166 | {"ToSlice with 100 elements", islices.ERange(0, 100)}, 167 | } 168 | 169 | for _, scenario := range scenarios { 170 | t.Run(scenario.name, func(t *testing.T) { 171 | tup := FromSlice(scenario.data) 172 | 173 | if !slices.Equal(tup.ToSlice(), scenario.data) { 174 | t.Error("Expected tup.ToSlice() to be equal to scenario.data") 175 | } 176 | }) 177 | } 178 | } 179 | 180 | func TestTuple_String(t *testing.T) { 181 | scenarios := []struct { 182 | name string 183 | data []int 184 | }{ 185 | {"String with no elements", []int{}}, 186 | {"String with 1 elements", []int{1}}, 187 | {"String with 2 elements", []int{1, 2}}, 188 | {"String with 3 elements", []int{1, 2, 3}}, 189 | {"String with 100 elements", islices.ERange(0, 100)}, 190 | } 191 | 192 | for _, scenario := range scenarios { 193 | t.Run(scenario.name, func(t *testing.T) { 194 | tup := FromSlice(scenario.data) 195 | s := fmt.Sprintf("%v", scenario.data) 196 | 197 | if tup.String() != s { 198 | t.Errorf("Expected tup.String() to be equal to \"%s\"", s) 199 | } 200 | }) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /functions/slices/equals.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "slices" 5 | ) 6 | 7 | // ShallowEquals checks to make sure that both slices contain the 8 | // same elements. The ordering of the elements doesn't matter. 9 | func ShallowEquals[T comparable, A ~[]T](s1, s2 A) bool { 10 | if len(s1) != len(s2) { 11 | return false 12 | } 13 | 14 | elementCount := make(map[T]int) 15 | 16 | for _, item := range s1 { 17 | elementCount[item]++ 18 | } 19 | 20 | for _, item := range s2 { 21 | elementCount[item]-- 22 | 23 | if elementCount[item] < 0 { 24 | return false 25 | } 26 | } 27 | 28 | for _, count := range elementCount { 29 | if count != 0 { 30 | return false 31 | } 32 | } 33 | 34 | return true 35 | } 36 | 37 | // DeepEquals checks to make sure that both slices contain the 38 | // same elements. The ordering of the elements matter. 39 | func DeepEquals[T comparable, A ~[]T](s1, s2 A) bool { 40 | return slices.Equal(s1, s2) 41 | } 42 | -------------------------------------------------------------------------------- /functions/slices/equals_test.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestShallowEquals(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | s1 any 11 | s2 any 12 | expected bool 13 | }{ 14 | { 15 | name: "Empty slices", 16 | s1: []int{}, 17 | s2: []int{}, 18 | expected: true, 19 | }, 20 | { 21 | name: "Same elements same order", 22 | s1: []int{1, 2, 3, 4, 5}, 23 | s2: []int{1, 2, 3, 4, 5}, 24 | expected: true, 25 | }, 26 | { 27 | name: "Same elements different order", 28 | s1: []int{1, 2, 3, 4, 5}, 29 | s2: []int{5, 4, 3, 2, 1}, 30 | expected: true, 31 | }, 32 | { 33 | name: "Different lengths", 34 | s1: []int{1, 2, 3}, 35 | s2: []int{1, 2, 3, 4}, 36 | expected: false, 37 | }, 38 | { 39 | name: "Different elements", 40 | s1: []int{1, 2, 3, 4, 5}, 41 | s2: []int{1, 2, 3, 4, 6}, 42 | expected: false, 43 | }, 44 | { 45 | name: "Different frequency of elements", 46 | s1: []int{1, 2, 3, 4, 5}, 47 | s2: []int{1, 2, 3, 5, 5}, 48 | expected: false, 49 | }, 50 | { 51 | name: "With duplicate elements", 52 | s1: []int{1, 2, 2, 3, 3}, 53 | s2: []int{3, 3, 2, 2, 1}, 54 | expected: true, 55 | }, 56 | { 57 | name: "String slices equal", 58 | s1: []string{"apple", "banana", "cherry"}, 59 | s2: []string{"cherry", "banana", "apple"}, 60 | expected: true, 61 | }, 62 | { 63 | name: "String slices not equal", 64 | s1: []string{"apple", "banana", "cherry"}, 65 | s2: []string{"apple", "banana", "orange"}, 66 | expected: false, 67 | }, 68 | { 69 | name: "With negative numbers", 70 | s1: []int{-1, -2, -3, 0, 1}, 71 | s2: []int{1, 0, -3, -2, -1}, 72 | expected: true, 73 | }, 74 | { 75 | name: "All same element", 76 | s1: []int{5, 5, 5, 5}, 77 | s2: []int{5, 5, 5, 5}, 78 | expected: true, 79 | }, 80 | } 81 | 82 | for _, tt := range tests { 83 | t.Run(tt.name, func(t *testing.T) { 84 | switch s1 := tt.s1.(type) { 85 | case []int: 86 | s2 := tt.s2.([]int) 87 | if got := ShallowEquals(s1, s2); got != tt.expected { 88 | t.Errorf("ShallowEquals() = %v, want %v for %v and %v", got, tt.expected, s1, s2) 89 | } 90 | case []string: 91 | s2 := tt.s2.([]string) 92 | if got := ShallowEquals(s1, s2); got != tt.expected { 93 | t.Errorf("ShallowEquals() = %v, want %v for %v and %v", got, tt.expected, s1, s2) 94 | } 95 | } 96 | }) 97 | } 98 | } 99 | 100 | func TestDeepEquals(t *testing.T) { 101 | tests := []struct { 102 | name string 103 | s1 any 104 | s2 any 105 | expected bool 106 | }{ 107 | { 108 | name: "Empty slices", 109 | s1: []int{}, 110 | s2: []int{}, 111 | expected: true, 112 | }, 113 | { 114 | name: "Same elements same order", 115 | s1: []int{1, 2, 3, 4, 5}, 116 | s2: []int{1, 2, 3, 4, 5}, 117 | expected: true, 118 | }, 119 | { 120 | name: "Same elements different order", 121 | s1: []int{1, 2, 3, 4, 5}, 122 | s2: []int{5, 4, 3, 2, 1}, 123 | expected: false, // Order matters for DeepEquals 124 | }, 125 | { 126 | name: "Different lengths", 127 | s1: []int{1, 2, 3}, 128 | s2: []int{1, 2, 3, 4}, 129 | expected: false, 130 | }, 131 | { 132 | name: "Different elements", 133 | s1: []int{1, 2, 3, 4, 5}, 134 | s2: []int{1, 2, 3, 4, 6}, 135 | expected: false, 136 | }, 137 | { 138 | name: "String slices equal", 139 | s1: []string{"apple", "banana", "cherry"}, 140 | s2: []string{"apple", "banana", "cherry"}, 141 | expected: true, 142 | }, 143 | { 144 | name: "String slices equal order different", 145 | s1: []string{"apple", "banana", "cherry"}, 146 | s2: []string{"cherry", "banana", "apple"}, 147 | expected: false, // Order matters for DeepEquals 148 | }, 149 | { 150 | name: "With negative numbers same order", 151 | s1: []int{-1, -2, -3, 0, 1}, 152 | s2: []int{-1, -2, -3, 0, 1}, 153 | expected: true, 154 | }, 155 | { 156 | name: "Empty vs non-empty", 157 | s1: []int{}, 158 | s2: []int{1}, 159 | expected: false, 160 | }, 161 | { 162 | name: "Single element", 163 | s1: []int{42}, 164 | s2: []int{42}, 165 | expected: true, 166 | }, 167 | { 168 | name: "Duplicate elements in same positions", 169 | s1: []int{1, 2, 2, 3, 3}, 170 | s2: []int{1, 2, 2, 3, 3}, 171 | expected: true, 172 | }, 173 | } 174 | 175 | for _, tt := range tests { 176 | t.Run(tt.name, func(t *testing.T) { 177 | switch s1 := tt.s1.(type) { 178 | case []int: 179 | s2 := tt.s2.([]int) 180 | if got := DeepEquals(s1, s2); got != tt.expected { 181 | t.Errorf("DeepEquals() = %v, want %v for %v and %v", got, tt.expected, s1, s2) 182 | } 183 | case []string: 184 | s2 := tt.s2.([]string) 185 | if got := DeepEquals(s1, s2); got != tt.expected { 186 | t.Errorf("DeepEquals() = %v, want %v for %v and %v", got, tt.expected, s1, s2) 187 | } 188 | } 189 | }) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /functions/slices/filter.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | ) 7 | 8 | // Filter returns a new slice containing only the elements of the input slice `s` 9 | // for which the predicate function `f` returns true. 10 | // 11 | // The original order of elements is preserved. The output slice is a newly allocated 12 | // slice of the same type as the input. 13 | // 14 | // Example: 15 | // 16 | // evens := Filter([]int{1, 2, 3, 4}, func(n int) bool { 17 | // return n%2 == 0 18 | // }) 19 | // // evens == []int{2, 4} 20 | func Filter[T any, S ~[]T](s S, f func(T) bool) S { 21 | var result S 22 | for _, i := range s { 23 | if f(i) { 24 | result = append(result, i) 25 | } 26 | } 27 | 28 | return result 29 | } 30 | 31 | // ParallelFilter evaluates the predicate function `f` in parallel on each element 32 | // of the input slice `s` and returns a new slice containing only those elements 33 | // for which `f` returns true. 34 | // 35 | // The number of concurrent workers can be optionally specified via the `workers` 36 | // variadic argument. If omitted, it defaults to `runtime.GOMAXPROCS(0)`. 37 | // 38 | // The original order of elements is preserved. This function is particularly useful 39 | // when the predicate function is expensive and you want to utilize multiple CPU cores. 40 | // 41 | // Note: Although filtering is performed in parallel, the result is assembled 42 | // sequentially, making this function most beneficial when `f` is significantly 43 | // more expensive than a simple condition. 44 | // 45 | // Example: 46 | // 47 | // evens := ParallelFilter([]int{1, 2, 3, 4}, func(n int) bool { 48 | // return n%2 == 0 49 | // }) 50 | // // evens == []int{2, 4} 51 | func ParallelFilter[T any, S ~[]T](s S, f func(T) bool, workers ...int) S { 52 | type result struct { 53 | index int 54 | value bool 55 | } 56 | 57 | if len(s) == 0 { 58 | var temp S 59 | return temp 60 | } 61 | 62 | workerCount := runtime.GOMAXPROCS(0) 63 | if len(workers) > 0 && workers[0] > 0 { 64 | workerCount = workers[0] 65 | } 66 | 67 | jobs := make(chan int, len(s)) 68 | go func() { 69 | for i := 0; i < len(s); i++ { 70 | jobs <- i 71 | } 72 | close(jobs) 73 | }() 74 | 75 | results := make(chan result, len(s)) 76 | 77 | var wg sync.WaitGroup 78 | 79 | for i := 0; i < workerCount; i++ { 80 | wg.Add(1) 81 | go func() { 82 | defer wg.Done() 83 | for index := range jobs { 84 | results <- result{index, f(s[index])} 85 | } 86 | }() 87 | } 88 | 89 | go func() { 90 | wg.Wait() 91 | close(results) 92 | }() 93 | 94 | temp := make([]bool, len(s)) 95 | for result := range results { 96 | temp[result.index] = result.value 97 | } 98 | 99 | var items S 100 | for index, shouldAdd := range temp { 101 | if shouldAdd { 102 | items = append(items, s[index]) 103 | } 104 | } 105 | 106 | return items 107 | } 108 | -------------------------------------------------------------------------------- /functions/slices/filter_test.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | 7 | islices "github.com/PsionicAlch/byteforge/internal/functions/slices" 8 | ) 9 | 10 | func TestFilter(t *testing.T) { 11 | const max = 1000000 12 | largeArr := islices.IRange(1, max) 13 | 14 | largeExpected := make([]int, 0, max) 15 | for _, num := range largeArr { 16 | if num%2 == 0 { 17 | largeExpected = append(largeExpected, num) 18 | } 19 | } 20 | 21 | t.Run("Filter small slice of int", func(t *testing.T) { 22 | result := Filter(islices.IRange(1, 10), func(num int) bool { 23 | return num%2 == 0 24 | }) 25 | expected := []int{2, 4, 6, 8, 10} 26 | 27 | if !slices.Equal(result, expected) { 28 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 29 | } 30 | }) 31 | 32 | t.Run("Filter large slice of int", func(t *testing.T) { 33 | result := Filter(largeArr, func(num int) bool { 34 | return num%2 == 0 35 | }) 36 | 37 | if !slices.Equal(result, largeExpected) { 38 | t.Errorf("Expected result to be %#v. Got %#v", largeExpected, result) 39 | } 40 | }) 41 | } 42 | 43 | func TestParallelFilter(t *testing.T) { 44 | const max = 1000000 45 | largeArr := islices.IRange(1, max) 46 | 47 | largeExpected := make([]int, 0, max) 48 | for _, num := range largeArr { 49 | if num%2 == 0 { 50 | largeExpected = append(largeExpected, num) 51 | } 52 | } 53 | 54 | t.Run("Parallel filter small slice of int", func(t *testing.T) { 55 | result := ParallelFilter(islices.IRange(1, 10), func(num int) bool { 56 | return num%2 == 0 57 | }) 58 | expected := []int{2, 4, 6, 8, 10} 59 | 60 | if !slices.Equal(result, expected) { 61 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 62 | } 63 | }) 64 | 65 | t.Run("Parallel filter large slice of int", func(t *testing.T) { 66 | result := ParallelFilter(largeArr, func(num int) bool { 67 | return num%2 == 0 68 | }) 69 | 70 | if !slices.Equal(result, largeExpected) { 71 | t.Errorf("Expected result to be %#v. Got %#v", largeExpected, result) 72 | } 73 | }) 74 | 75 | t.Run("Filter with empty slice", func(t *testing.T) { 76 | result := ParallelFilter([]int{}, func(_ int) bool { 77 | return true 78 | }) 79 | expected := []int{} 80 | 81 | if !slices.Equal(result, expected) { 82 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 83 | } 84 | }) 85 | 86 | t.Run("Parallel filter with worker pool", func(t *testing.T) { 87 | result := ParallelFilter(largeArr, func(num int) bool { 88 | return num%2 == 0 89 | }, 100) 90 | 91 | if !slices.Equal(result, largeExpected) { 92 | t.Errorf("Expected result to be %#v. Got %#v", largeExpected, result) 93 | } 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /functions/slices/foreach.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | ) 7 | 8 | // ForEach iterates over the elements of the provided slice `s`, 9 | // calling the function `f` for each element with its index and value. 10 | // 11 | // Example usage: 12 | // 13 | // slices.ForEach([]string{"a", "b", "c"}, func(i int, v string) { 14 | // fmt.Printf("Index %d: %s\n", i, v) 15 | // }) 16 | func ForEach[T any, E ~[]T](s E, f func(int, T)) { 17 | for i, e := range s { 18 | f(i, e) 19 | } 20 | } 21 | 22 | // ParallelForEach iterates over the elements of the provided slice `s` in parallel, 23 | // using multiple worker goroutines. It calls the function `f` for each element 24 | // with its index and value. 25 | // 26 | // The optional `workers` argument allows you to specify the number of worker goroutines. 27 | // If omitted or zero, it defaults to runtime.GOMAXPROCS(0). 28 | // 29 | // Example usage: 30 | // 31 | // slices.ParallelForEach([]int{1, 2, 3, 4}, func(i int, v int) { 32 | // fmt.Printf("Index %d: %d\n", i, v) 33 | // }) 34 | // 35 | // slices.ParallelForEach([]int{1, 2, 3, 4}, func(i int, v int) { 36 | // fmt.Printf("Index %d: %d\n", i, v) 37 | // }, 4) // use 4 workers 38 | func ParallelForEach[T any, E ~[]T](s E, f func(int, T), workers ...int) { 39 | if len(s) == 0 { 40 | return 41 | } 42 | 43 | workerCount := runtime.GOMAXPROCS(0) 44 | if len(workers) > 0 && workers[0] > 0 { 45 | workerCount = workers[0] 46 | } 47 | 48 | jobs := make(chan int, len(s)) 49 | go func() { 50 | for i := 0; i < len(s); i++ { 51 | jobs <- i 52 | } 53 | close(jobs) 54 | }() 55 | 56 | var wg sync.WaitGroup 57 | 58 | for i := 0; i < workerCount; i++ { 59 | wg.Add(1) 60 | go func() { 61 | defer wg.Done() 62 | for index := range jobs { 63 | f(index, s[index]) 64 | } 65 | }() 66 | } 67 | 68 | wg.Wait() 69 | } 70 | -------------------------------------------------------------------------------- /functions/slices/foreach_test.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestForEach(t *testing.T) { 9 | input := []int{10, 20, 30, 40} 10 | expected := []int{10, 20, 30, 40} 11 | var results []int 12 | 13 | ForEach(input, func(i int, v int) { 14 | results = append(results, v) 15 | }) 16 | 17 | if len(results) != len(expected) { 18 | t.Fatalf("Expected length %d, got %d", len(expected), len(results)) 19 | } 20 | 21 | for i := range expected { 22 | if results[i] != expected[i] { 23 | t.Errorf("At index %d: expected %d, got %d", i, expected[i], results[i]) 24 | } 25 | } 26 | } 27 | 28 | func TestParallelForEach(t *testing.T) { 29 | t.Run("Basic", func(t *testing.T) { 30 | input := []string{"a", "b", "c", "d"} 31 | expected := map[string]bool{"a": true, "b": true, "c": true, "d": true} 32 | results := make(map[string]bool) 33 | var mu sync.Mutex 34 | 35 | ParallelForEach(input, func(i int, v string) { 36 | mu.Lock() 37 | results[v] = true 38 | mu.Unlock() 39 | }) 40 | 41 | if len(results) != len(expected) { 42 | t.Fatalf("Expected %d unique results, got %d", len(expected), len(results)) 43 | } 44 | 45 | for k := range expected { 46 | if !results[k] { 47 | t.Errorf("Missing expected value: %s", k) 48 | } 49 | } 50 | }) 51 | 52 | t.Run("With Workers", func(t *testing.T) { 53 | input := []int{1, 2, 3, 4, 5} 54 | expected := map[int]bool{1: true, 2: true, 3: true, 4: true, 5: true} 55 | results := make(map[int]bool) 56 | var mu sync.Mutex 57 | 58 | ParallelForEach(input, func(i int, v int) { 59 | mu.Lock() 60 | results[v] = true 61 | mu.Unlock() 62 | }, 2) // specify 2 workers 63 | 64 | if len(results) != len(expected) { 65 | t.Fatalf("Expected %d unique results, got %d", len(expected), len(results)) 66 | } 67 | 68 | for k := range expected { 69 | if !results[k] { 70 | t.Errorf("Missing expected value: %d", k) 71 | } 72 | } 73 | }) 74 | 75 | t.Run("With Empty Slice", func(t *testing.T) { 76 | called := false 77 | 78 | ParallelForEach([]int{}, func(i int, v int) { 79 | called = true 80 | }) 81 | 82 | if called { 83 | t.Errorf("Expected function not to be called for empty slice") 84 | } 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /functions/slices/map.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "runtime" 5 | "sync" 6 | ) 7 | 8 | // Map applies the given function f to each element of the input slice s, 9 | // returning a new slice containing the results. 10 | // 11 | // It preserves the order of the original slice and runs sequentially. 12 | // 13 | // Example: 14 | // 15 | // doubled := Map([]int{1, 2, 3}, func(n int) int { 16 | // return n * 2 17 | // }) 18 | // // doubled = []int{2, 4, 6} 19 | func Map[T any, R any, S ~[]T](s S, f func(T) R) []R { 20 | result := make([]R, len(s)) 21 | for i, v := range s { 22 | result[i] = f(v) 23 | } 24 | 25 | return result 26 | } 27 | 28 | // ParallelMap applies the function f to each element of the input slice s 29 | // concurrently using a worker pool, and returns a new slice containing 30 | // the results in the original order. 31 | // 32 | // The number of concurrent workers can be controlled via the optional 33 | // workers parameter. If omitted or set to a non-positive number, 34 | // the number of logical CPUs (runtime.GOMAXPROCS(0)) is used by default. 35 | // 36 | // Example: 37 | // 38 | // squared := ParallelMap([]int{1, 2, 3, 4}, func(n int) int { 39 | // return n * n 40 | // }, 8) 41 | // // squared = []int{1, 4, 9, 16} 42 | // 43 | // Notes: 44 | // - This function is safe for functions f that are side-effect free or thread-safe. 45 | // - Use ParallelMap for CPU-bound or latency-sensitive transforms over large slices. 46 | // 47 | // Panics if f panics; it does not recover from errors within goroutines. 48 | func ParallelMap[T any, R any, S ~[]T](s S, f func(T) R, workers ...int) []R { 49 | type result struct { 50 | index int 51 | value R 52 | } 53 | 54 | if len(s) == 0 { 55 | return []R{} 56 | } 57 | 58 | workerCount := runtime.GOMAXPROCS(0) 59 | if len(workers) > 0 && workers[0] > 0 { 60 | workerCount = workers[0] 61 | } 62 | 63 | jobs := make(chan int, len(s)) 64 | go func() { 65 | for i := 0; i < len(s); i++ { 66 | jobs <- i 67 | } 68 | close(jobs) 69 | }() 70 | 71 | results := make(chan result, len(s)) 72 | 73 | var wg sync.WaitGroup 74 | 75 | for i := 0; i < workerCount; i++ { 76 | wg.Add(1) 77 | go func() { 78 | defer wg.Done() 79 | for index := range jobs { 80 | results <- result{index, f(s[index])} 81 | } 82 | }() 83 | } 84 | 85 | go func() { 86 | wg.Wait() 87 | close(results) 88 | }() 89 | 90 | items := make([]R, len(s)) 91 | for result := range results { 92 | items[result.index] = result.value 93 | } 94 | 95 | return items 96 | } 97 | -------------------------------------------------------------------------------- /functions/slices/map_test.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "slices" 5 | "strconv" 6 | "testing" 7 | 8 | islices "github.com/PsionicAlch/byteforge/internal/functions/slices" 9 | ) 10 | 11 | func TestMap(t *testing.T) { 12 | const max = 1000000 13 | largeArr := islices.ERange(0, max) 14 | 15 | largeExpected := make([]int, max) 16 | for i := 0; i < max; i++ { 17 | largeExpected[i] = i * 2 18 | } 19 | 20 | t.Run("Map from int to int", func(t *testing.T) { 21 | result := Map([]int{0, 1, 2, 3, 4, 5}, func(num int) int { 22 | return num * 2 23 | }) 24 | expected := []int{0, 2, 4, 6, 8, 10} 25 | 26 | if !slices.Equal(result, expected) { 27 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 28 | } 29 | }) 30 | 31 | t.Run("Map from int to string", func(t *testing.T) { 32 | result := Map([]int{0, 1, 2, 3, 4, 5}, func(num int) string { 33 | return strconv.Itoa(num) 34 | }) 35 | expected := []string{"0", "1", "2", "3", "4", "5"} 36 | 37 | if !slices.Equal(result, expected) { 38 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 39 | } 40 | }) 41 | 42 | t.Run("Map with empty slice", func(t *testing.T) { 43 | result := Map([]int{}, func(num int) int { 44 | return num 45 | }) 46 | expected := []int{} 47 | 48 | if !slices.Equal(result, expected) { 49 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 50 | } 51 | }) 52 | 53 | t.Run("Map with huge slice", func(t *testing.T) { 54 | result := Map(largeArr, func(num int) int { 55 | return num * 2 56 | }) 57 | 58 | if !slices.Equal(result, largeExpected) { 59 | t.Errorf("Expected result to be %#v. Got %#v", largeExpected, result) 60 | } 61 | }) 62 | } 63 | 64 | func TestParallelMap(t *testing.T) { 65 | const max = 1000000 66 | largeArr := islices.ERange(0, max) 67 | 68 | largeExpected := make([]int, max) 69 | for i := 0; i < max; i++ { 70 | largeExpected[i] = i * 2 71 | } 72 | 73 | t.Run("Parallel map from int to int", func(t *testing.T) { 74 | result := ParallelMap([]int{0, 1, 2, 3, 4, 5}, func(num int) int { 75 | return num * 2 76 | }) 77 | expected := []int{0, 2, 4, 6, 8, 10} 78 | 79 | if !slices.Equal(result, expected) { 80 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 81 | } 82 | }) 83 | 84 | t.Run("Parallel map from int to string", func(t *testing.T) { 85 | result := ParallelMap([]int{0, 1, 2, 3, 4, 5}, func(num int) string { 86 | return strconv.Itoa(num) 87 | }) 88 | expected := []string{"0", "1", "2", "3", "4", "5"} 89 | 90 | if !slices.Equal(result, expected) { 91 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 92 | } 93 | }) 94 | 95 | t.Run("Parallel map with empty slice", func(t *testing.T) { 96 | result := ParallelMap([]int{}, func(num int) int { 97 | return num 98 | }) 99 | expected := []int{} 100 | 101 | if !slices.Equal(result, expected) { 102 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 103 | } 104 | }) 105 | 106 | t.Run("Parallel map with huge slice", func(t *testing.T) { 107 | result := ParallelMap(largeArr, func(num int) int { 108 | return num * 2 109 | }) 110 | 111 | if !slices.Equal(result, largeExpected) { 112 | t.Errorf("Expected result to be %#v. Got %#v", largeExpected, result) 113 | } 114 | }) 115 | 116 | t.Run("Parallel map with negative worker pool", func(t *testing.T) { 117 | result := ParallelMap([]int{0, 1, 2, 3, 4, 5}, func(num int) int { 118 | return num * 2 119 | }, -10) 120 | expected := []int{0, 2, 4, 6, 8, 10} 121 | 122 | if !slices.Equal(result, expected) { 123 | t.Errorf("Expected result to be %#v. Got %#v", expected, result) 124 | } 125 | }) 126 | 127 | t.Run("Parallel map with positive worker pool", func(t *testing.T) { 128 | result := ParallelMap(largeArr, func(num int) int { 129 | return num * 2 130 | }, 50) 131 | 132 | if !slices.Equal(result, largeExpected) { 133 | t.Errorf("Expected result to be %#v. Got %#v", largeExpected, result) 134 | } 135 | }) 136 | } 137 | -------------------------------------------------------------------------------- /functions/slices/range.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "github.com/PsionicAlch/byteforge/constraints" 5 | "github.com/PsionicAlch/byteforge/internal/functions/slices" 6 | ) 7 | 8 | // IRange generates a slice of numbers from min to max, inclusive. 9 | func IRange[T constraints.Number](min, max T, step ...T) []T { 10 | return slices.IRange(min, max, step...) 11 | } 12 | 13 | // ERange generates a slice of numbers from min up to, but not including, max. 14 | func ERange[T constraints.Number](min, max T, step ...T) []T { 15 | return slices.ERange(min, max, step...) 16 | } 17 | -------------------------------------------------------------------------------- /functions/slices/range_test.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | ) 7 | 8 | func TestIRange(t *testing.T) { 9 | t.Run("Basic use cases", func(t *testing.T) { 10 | tests := []struct { 11 | min, max int 12 | step []int 13 | expected []int 14 | }{ 15 | {1, 5, nil, []int{1, 2, 3, 4, 5}}, 16 | {5, 1, nil, []int{5, 4, 3, 2, 1}}, 17 | {0, 10, []int{2}, []int{0, 2, 4, 6, 8, 10}}, 18 | {10, 0, []int{-2}, []int{10, 8, 6, 4, 2, 0}}, 19 | {3, 3, nil, []int{3}}, 20 | } 21 | 22 | for _, tt := range tests { 23 | out := IRange(tt.min, tt.max, tt.step...) 24 | if !slices.Equal(out, tt.expected) { 25 | t.Errorf("IRange(%v, %v, %v) = %v; want %v", tt.min, tt.max, tt.step, out, tt.expected) 26 | } 27 | } 28 | }) 29 | 30 | t.Run("Invalid range", func(t *testing.T) { 31 | out := IRange(1, 10, -1) 32 | if !slices.Equal(out, []int{}) { 33 | t.Errorf("Expected to get empty slice. Got %#v", out) 34 | } 35 | 36 | out = IRange(10, 1, 1) 37 | if !slices.Equal(out, []int{}) { 38 | t.Errorf("Expected to get empty slice for step in wrong direction. Got %#v", out) 39 | } 40 | }) 41 | } 42 | 43 | func TestERange(t *testing.T) { 44 | t.Run("Basic use cases", func(t *testing.T) { 45 | tests := []struct { 46 | min, max int 47 | step []int 48 | expected []int 49 | }{ 50 | {1, 5, nil, []int{1, 2, 3, 4}}, 51 | {5, 1, nil, []int{5, 4, 3, 2}}, 52 | {0, 10, []int{3}, []int{0, 3, 6, 9}}, 53 | {10, 0, []int{-3}, []int{10, 7, 4, 1}}, 54 | {3, 3, nil, []int{}}, 55 | } 56 | 57 | for _, tt := range tests { 58 | out := ERange(tt.min, tt.max, tt.step...) 59 | if !slices.Equal(out, tt.expected) { 60 | t.Errorf("ERange(%v, %v, %v) = %v; want %v", tt.min, tt.max, tt.step, out, tt.expected) 61 | } 62 | } 63 | }) 64 | 65 | t.Run("Invalid range", func(t *testing.T) { 66 | out := ERange(1, 10, -1) 67 | if !slices.Equal(out, []int{}) { 68 | t.Errorf("Expected to get empty slice for step in wrong direction. Got %#v", out) 69 | } 70 | 71 | out = ERange(10, 1, 1) 72 | if !slices.Equal(out, []int{}) { 73 | t.Errorf("Expected to get empty slice step in wrong direction. Got %#v", out) 74 | } 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/PsionicAlch/byteforge 2 | 3 | go 1.24.2 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PsionicAlch/byteforge/5a444d980722cd15953f5821b5d27a98f80cc48a/go.sum -------------------------------------------------------------------------------- /images/byteforge-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PsionicAlch/byteforge/5a444d980722cd15953f5821b5d27a98f80cc48a/images/byteforge-banner.png -------------------------------------------------------------------------------- /internal/datastructs/buffers/ring/ring.go: -------------------------------------------------------------------------------- 1 | // Package ring provides a generic ring buffer (circular buffer) implementation. 2 | // This internal version serves as the core data structure for FIFO queues and other public-facing abstractions. 3 | // It supports dynamic resizing and is optimized for enqueue/dequeue performance without relying on third-party libraries. 4 | package ring 5 | 6 | import "slices" 7 | 8 | // InternalRingBuffer is a generic dynamically resizable circular buffer. 9 | // It supports enqueue and dequeue operations in constant amortized time, 10 | // and grows or shrinks based on usage to optimize memory consumption. 11 | // 12 | // T represents the type of elements stored in the buffer. 13 | type InternalRingBuffer[T any] struct { 14 | data []T 15 | head, tail int 16 | size int 17 | capacity int 18 | } 19 | 20 | // New returns a new InternalRingBuffer with an optional initial capacity. 21 | // If no capacity is provided or the provided value is <= 0, a default of 8 is used. 22 | func New[T any](capacity ...int) *InternalRingBuffer[T] { 23 | cap := 8 24 | if len(capacity) > 0 && capacity[0] > 0 { 25 | cap = capacity[0] 26 | } 27 | 28 | return &InternalRingBuffer[T]{ 29 | data: make([]T, cap), 30 | capacity: cap, 31 | } 32 | } 33 | 34 | // FromSlice creates a new InternalRingBuffer from a given slice. 35 | // An optional capacity may be provided. If the capacity is less than the slice length, 36 | // the slice length is used as the minimum capacity. 37 | func FromSlice[T any, A ~[]T](s A, capacity ...int) *InternalRingBuffer[T] { 38 | desiredCapacity := 8 39 | 40 | if len(capacity) > 0 && capacity[0] > desiredCapacity { 41 | desiredCapacity = capacity[0] 42 | } else if len(s) > 0 { 43 | desiredCapacity = len(s) 44 | } 45 | 46 | var data []T 47 | 48 | if desiredCapacity > len(s) { 49 | data = make([]T, desiredCapacity) 50 | for i := 0; i < len(s); i++ { 51 | data[i] = s[i] 52 | } 53 | } else { 54 | data = slices.Clone(s) 55 | } 56 | 57 | return &InternalRingBuffer[T]{ 58 | data: data, 59 | capacity: desiredCapacity, 60 | tail: len(s), 61 | size: len(s), 62 | } 63 | } 64 | 65 | // Len returns the number of elements currently stored in the buffer. 66 | func (rb *InternalRingBuffer[T]) Len() int { 67 | return rb.size 68 | } 69 | 70 | // Cap returns the total capacity of the buffer. 71 | func (rb *InternalRingBuffer[T]) Cap() int { 72 | return rb.capacity 73 | } 74 | 75 | // IsEmpty returns true if the buffer contains no elements. 76 | func (rb *InternalRingBuffer[T]) IsEmpty() bool { 77 | return rb.size == 0 78 | } 79 | 80 | // Enqueue appends one or more values to the end of the buffer. 81 | // If necessary, the buffer is resized to accommodate the new values. 82 | func (rb *InternalRingBuffer[T]) Enqueue(values ...T) { 83 | required := rb.size + len(values) 84 | if required > rb.capacity { 85 | newCap := rb.capacity * 2 86 | for newCap < required { 87 | newCap *= 2 88 | } 89 | 90 | rb.resize(newCap) 91 | } 92 | 93 | for _, value := range values { 94 | rb.data[rb.tail] = value 95 | rb.tail = (rb.tail + 1) % rb.capacity 96 | rb.size++ 97 | } 98 | } 99 | 100 | // Dequeue removes and returns the element at the front of the buffer. 101 | // If the buffer is empty, it returns the zero value of T and false. 102 | // The buffer may shrink if usage falls below 25% of capacity. 103 | func (rb *InternalRingBuffer[T]) Dequeue() (T, bool) { 104 | var zero T 105 | if rb.size == 0 { 106 | return zero, false 107 | } 108 | 109 | val := rb.data[rb.head] 110 | rb.head = (rb.head + 1) % rb.capacity 111 | rb.size-- 112 | 113 | if rb.capacity > 1 && rb.size <= rb.capacity/4 { 114 | rb.resize(rb.capacity / 2) 115 | } 116 | 117 | return val, true 118 | } 119 | 120 | // Peek returns the element at the front of the buffer without removing it. 121 | // If the buffer is empty, it returns the zero value of T and false. 122 | func (rb *InternalRingBuffer[T]) Peek() (T, bool) { 123 | var zero T 124 | if rb.size == 0 { 125 | return zero, false 126 | } 127 | 128 | return rb.data[rb.head], true 129 | } 130 | 131 | // ToSlice returns a new slice containing all elements in the buffer in their logical order. 132 | // The returned slice is independent of the internal buffer state. 133 | func (rb *InternalRingBuffer[T]) ToSlice() []T { 134 | if rb.size == 0 { 135 | return make([]T, 0) 136 | } 137 | 138 | result := make([]T, rb.size) 139 | for i := 0; i < rb.size; i++ { 140 | result[i] = rb.data[(rb.head+i)%rb.capacity] 141 | } 142 | 143 | return result 144 | } 145 | 146 | // Clone creates a deep copy of the source InternalRingBuffer. 147 | func (rb *InternalRingBuffer[T]) Clone() *InternalRingBuffer[T] { 148 | newData := make([]T, rb.capacity) 149 | for i := 0; i < rb.size; i++ { 150 | newData[i] = rb.data[(rb.head+i)%rb.capacity] 151 | } 152 | 153 | return &InternalRingBuffer[T]{ 154 | data: newData, 155 | head: 0, 156 | tail: rb.size, 157 | size: rb.size, 158 | capacity: rb.capacity, 159 | } 160 | } 161 | 162 | // resize adjusts the capacity of the buffer to the specified value, 163 | // reordering the contents so that head = 0 and tail = size. 164 | func (rb *InternalRingBuffer[T]) resize(newCap int) { 165 | newData := make([]T, newCap) 166 | for i := 0; i < rb.size; i++ { 167 | newData[i] = rb.data[(rb.head+i)%rb.capacity] 168 | } 169 | 170 | rb.data = newData 171 | rb.head = 0 172 | rb.tail = rb.size 173 | rb.capacity = newCap 174 | } 175 | -------------------------------------------------------------------------------- /internal/datastructs/buffers/ring/ring_test.go: -------------------------------------------------------------------------------- 1 | package ring 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | ) 7 | 8 | func TestInternalRingBuffer_New(t *testing.T) { 9 | scenarios := []struct { 10 | name string 11 | capacity []int 12 | expectedSize int 13 | expectedCap int 14 | }{ 15 | {"Empty capacity", []int{}, 0, 8}, 16 | {"Non-empty capacity", []int{5}, 0, 5}, 17 | {"Negative capacity", []int{-10}, 0, 8}, 18 | } 19 | 20 | for _, scenario := range scenarios { 21 | t.Run(scenario.name, func(t *testing.T) { 22 | buf := New[int](scenario.capacity...) 23 | 24 | if buf.size != scenario.expectedSize { 25 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.size) 26 | } 27 | 28 | if buf.capacity != scenario.expectedCap { 29 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.capacity) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestInternalRingBuffer_FromSlice(t *testing.T) { 36 | scenarios := []struct { 37 | name string 38 | slice []int 39 | capacity []int 40 | expectedSize int 41 | expectedCap int 42 | }{ 43 | {"Empty slice and empty capacity", []int{}, []int{}, 0, 8}, 44 | {"Non-empty slice and empty capacity", []int{1, 2, 3}, []int{}, 3, 3}, 45 | {"Non-empty slice and non-empty capacity", []int{1, 2, 3}, []int{10}, 3, 10}, 46 | } 47 | 48 | for _, scenario := range scenarios { 49 | t.Run(scenario.name, func(t *testing.T) { 50 | buf := FromSlice(scenario.slice, scenario.capacity...) 51 | 52 | if buf.size != scenario.expectedSize { 53 | t.Errorf("Expected buffer's size to be %d. Got %d.", scenario.expectedSize, buf.size) 54 | } 55 | 56 | if buf.capacity != scenario.expectedCap { 57 | t.Errorf("Expected buffer's capacity to be %d. Got %d.", scenario.expectedCap, buf.capacity) 58 | } 59 | 60 | for _, item := range scenario.slice { 61 | if !slices.Contains(buf.data, item) { 62 | t.Errorf("Expected buffer to contain %d", item) 63 | } 64 | } 65 | }) 66 | } 67 | } 68 | 69 | func TestInternalRingBuffer_Len(t *testing.T) { 70 | scenarios := []struct { 71 | name string 72 | data []int 73 | expectedLen int 74 | }{ 75 | {"Lenght with 0 items", nil, 0}, 76 | {"Lenght with 3 items", []int{1, 2, 3}, 3}, 77 | } 78 | 79 | for _, scenario := range scenarios { 80 | t.Run(scenario.name, func(t *testing.T) { 81 | buf := New[int]() 82 | 83 | if len(scenario.data) > 0 { 84 | buf.Enqueue(scenario.data...) 85 | } 86 | 87 | if buf.Len() != scenario.expectedLen { 88 | t.Errorf("Expected buf.Len() to be %d. Got %d", scenario.expectedLen, buf.Len()) 89 | } 90 | }) 91 | } 92 | } 93 | 94 | func TestInternalRingBuffer_Cap(t *testing.T) { 95 | scenarios := []struct { 96 | name string 97 | data []int 98 | desiredCapacity []int 99 | expectedCapacity int 100 | }{ 101 | {"Capacity with 0 items and no desired capacity", nil, nil, 8}, 102 | {"Capacity with 0 items and desired capacity of 1", nil, []int{1}, 1}, 103 | {"Capacity with 0 items and desired capacity of -1", nil, []int{-1}, 8}, 104 | {"Capacity with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, 16}, 105 | {"Capacity with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, 6}, 106 | {"Capacity with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, 16}, 107 | } 108 | 109 | for _, scenario := range scenarios { 110 | t.Run(scenario.name, func(t *testing.T) { 111 | buf := New[int](scenario.desiredCapacity...) 112 | buf.Enqueue(scenario.data...) 113 | 114 | if buf.Cap() != scenario.expectedCapacity { 115 | t.Errorf("Expected buf.Cap() to be %d. Got %d", scenario.expectedCapacity, buf.Cap()) 116 | } 117 | }) 118 | } 119 | } 120 | 121 | func TestInternalRingBuffer_IsEmpty(t *testing.T) { 122 | scenarios := []struct { 123 | name string 124 | data []int 125 | desiredCapacity []int 126 | isEmpty bool 127 | }{ 128 | {"IsEmpty with 0 items and no desired capacity", nil, nil, true}, 129 | {"IsEmpty with 0 items and desired capacity of 1", nil, []int{1}, true}, 130 | {"IsEmpty with 0 items and desired capacity of -1", nil, []int{-1}, true}, 131 | {"IsEmpty with 9 items and no desired capacity", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, nil, false}, 132 | {"IsEmpty with 5 items and desired capacity of 3", []int{1, 2, 3, 4, 5}, []int{3}, false}, 133 | {"IsEmpty with 9 items and desired capacity of -9", []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, []int{-9}, false}, 134 | } 135 | 136 | for _, scenario := range scenarios { 137 | t.Run(scenario.name, func(t *testing.T) { 138 | buf := New[int](scenario.desiredCapacity...) 139 | buf.Enqueue(scenario.data...) 140 | 141 | if buf.IsEmpty() != scenario.isEmpty { 142 | t.Errorf("Expected buf.IsEmpty() to be %t. Got %t", scenario.isEmpty, buf.IsEmpty()) 143 | } 144 | }) 145 | } 146 | } 147 | 148 | func TestInternalRingBuffer_Enqueue(t *testing.T) { 149 | scenarios := []struct { 150 | name string 151 | initial []int 152 | enqueue []int 153 | expectedOrder []int 154 | expectedSize int 155 | expectedCapAtLeast int 156 | }{ 157 | { 158 | name: "Append within capacity", 159 | initial: []int{1, 2}, 160 | enqueue: []int{3, 4}, 161 | expectedOrder: []int{1, 2, 3, 4}, 162 | expectedSize: 4, 163 | expectedCapAtLeast: 4, 164 | }, 165 | { 166 | name: "Trigger resize on append", 167 | initial: []int{1, 2, 3, 4}, 168 | enqueue: []int{5, 6, 7}, 169 | expectedOrder: []int{1, 2, 3, 4, 5, 6, 7}, 170 | expectedSize: 7, 171 | expectedCapAtLeast: 7, 172 | }, 173 | { 174 | name: "Append nothing (no-op)", 175 | initial: []int{9, 8, 7}, 176 | enqueue: []int{}, 177 | expectedOrder: []int{9, 8, 7}, 178 | expectedSize: 3, 179 | expectedCapAtLeast: 3, 180 | }, 181 | { 182 | name: "Append to empty buffer", 183 | initial: []int{}, 184 | enqueue: []int{10, 20, 30}, 185 | expectedOrder: []int{10, 20, 30}, 186 | expectedSize: 3, 187 | expectedCapAtLeast: 3, 188 | }, 189 | { 190 | name: "Large append exceeds multiple resizes", 191 | initial: []int{}, 192 | enqueue: makeRange(1, 50), 193 | expectedOrder: makeRange(1, 50), 194 | expectedSize: 50, 195 | expectedCapAtLeast: 50, 196 | }, 197 | } 198 | 199 | for _, scenario := range scenarios { 200 | t.Run(scenario.name, func(t *testing.T) { 201 | buf := New[int](len(scenario.initial)) 202 | buf.Enqueue(scenario.initial...) 203 | buf.Enqueue(scenario.enqueue...) 204 | 205 | if buf.Len() != scenario.expectedSize { 206 | t.Errorf("Expected size %d, got %d", scenario.expectedSize, buf.Len()) 207 | } 208 | 209 | if buf.Cap() < scenario.expectedCapAtLeast { 210 | t.Errorf("Expected capacity >= %d, got %d", scenario.expectedCapAtLeast, buf.Cap()) 211 | } 212 | 213 | for _, want := range scenario.expectedOrder { 214 | got, ok := buf.Dequeue() 215 | 216 | if !ok { 217 | t.Fatalf("Expected to dequeue %d but got nothing", want) 218 | } 219 | 220 | if got != want { 221 | t.Errorf("Expected dequeue value %d, got %d", want, got) 222 | } 223 | } 224 | 225 | if !buf.IsEmpty() { 226 | t.Errorf("Expected buffer to be empty after all dequeues") 227 | } 228 | }) 229 | } 230 | } 231 | 232 | func TestInternalRingBuffer_Dequeue(t *testing.T) { 233 | scenarios := []struct { 234 | name string 235 | initial []int 236 | expectedDequeues []int 237 | expectedEmpty bool 238 | expectedCapAtMost int 239 | }{ 240 | { 241 | name: "Dequeue from empty buffer", 242 | initial: []int{}, 243 | expectedDequeues: []int{}, 244 | expectedEmpty: true, 245 | expectedCapAtMost: 8, 246 | }, 247 | { 248 | name: "Dequeue single element", 249 | initial: []int{42}, 250 | expectedDequeues: []int{42}, 251 | expectedEmpty: true, 252 | expectedCapAtMost: 8, 253 | }, 254 | { 255 | name: "Dequeue multiple elements", 256 | initial: []int{1, 2, 3, 4}, 257 | expectedDequeues: []int{1, 2, 3, 4}, 258 | expectedEmpty: true, 259 | expectedCapAtMost: 8, 260 | }, 261 | { 262 | name: "Dequeue triggers downsize", 263 | initial: makeRange(1, 16), 264 | expectedDequeues: makeRange(1, 14), 265 | expectedEmpty: false, 266 | expectedCapAtMost: 16, 267 | }, 268 | { 269 | name: "Dequeue wraparound case", 270 | initial: []int{10, 20, 30}, 271 | expectedDequeues: []int{10, 20, 30}, 272 | expectedEmpty: true, 273 | expectedCapAtMost: 8, 274 | }, 275 | } 276 | 277 | for _, scenario := range scenarios { 278 | t.Run(scenario.name, func(t *testing.T) { 279 | buf := New[int]() 280 | buf.Enqueue(scenario.initial...) 281 | 282 | for i, expected := range scenario.expectedDequeues { 283 | val, ok := buf.Dequeue() 284 | 285 | if !ok { 286 | t.Fatalf("Expected Dequeue #%d to return value %d, got nothing", i, expected) 287 | } 288 | 289 | if val != expected { 290 | t.Errorf("Dequeue #%d: expected %d, got %d", i, expected, val) 291 | } 292 | } 293 | 294 | if buf.IsEmpty() != scenario.expectedEmpty { 295 | t.Errorf("Expected IsEmpty to be %v, got %v", scenario.expectedEmpty, buf.IsEmpty()) 296 | } 297 | 298 | if buf.Cap() > scenario.expectedCapAtMost { 299 | t.Errorf("Expected capacity <= %d, got %d", scenario.expectedCapAtMost, buf.Cap()) 300 | } 301 | }) 302 | } 303 | } 304 | 305 | func TestInternalRingBuffer_Peek(t *testing.T) { 306 | scenarios := []struct { 307 | name string 308 | initial []int 309 | expectedPeek int 310 | expectOK bool 311 | expectSize int 312 | }{ 313 | { 314 | name: "Peek empty buffer", 315 | initial: []int{}, 316 | expectedPeek: 0, 317 | expectOK: false, 318 | expectSize: 0, 319 | }, 320 | { 321 | name: "Peek one item", 322 | initial: []int{7}, 323 | expectedPeek: 7, 324 | expectOK: true, 325 | expectSize: 1, 326 | }, 327 | { 328 | name: "Peek multiple items", 329 | initial: []int{3, 4, 5}, 330 | expectedPeek: 3, 331 | expectOK: true, 332 | expectSize: 3, 333 | }, 334 | { 335 | name: "Peek after internal wraparound", 336 | initial: makeRange(1, 10), 337 | expectedPeek: 1, 338 | expectOK: true, 339 | expectSize: 10, 340 | }, 341 | } 342 | 343 | for _, scenario := range scenarios { 344 | t.Run(scenario.name, func(t *testing.T) { 345 | buf := New[int]() 346 | buf.Enqueue(scenario.initial...) 347 | 348 | val, ok := buf.Peek() 349 | 350 | if ok != scenario.expectOK { 351 | t.Fatalf("Expected Peek ok=%v, got %v", scenario.expectOK, ok) 352 | } 353 | 354 | if ok && val != scenario.expectedPeek { 355 | t.Errorf("Expected Peek value %d, got %d", scenario.expectedPeek, val) 356 | } 357 | 358 | if buf.Len() != scenario.expectSize { 359 | t.Errorf("Expected buffer size %d after Peek, got %d", scenario.expectSize, buf.Len()) 360 | } 361 | }) 362 | } 363 | } 364 | 365 | func TestInternalRingBuffer_ToSlice(t *testing.T) { 366 | scenarios := []struct { 367 | name string 368 | setup func() *InternalRingBuffer[int] 369 | expectedSlice []int 370 | expectedSize int 371 | expectedBuffer []int 372 | }{ 373 | { 374 | name: "Empty buffer", 375 | setup: func() *InternalRingBuffer[int] { 376 | return New[int]() 377 | }, 378 | expectedSlice: []int{}, 379 | expectedSize: 0, 380 | expectedBuffer: []int{}, 381 | }, 382 | { 383 | name: "Buffer with single element", 384 | setup: func() *InternalRingBuffer[int] { 385 | buf := New[int]() 386 | buf.Enqueue(42) 387 | return buf 388 | }, 389 | expectedSlice: []int{42}, 390 | expectedSize: 1, 391 | expectedBuffer: []int{42}, 392 | }, 393 | { 394 | name: "Buffer with multiple elements", 395 | setup: func() *InternalRingBuffer[int] { 396 | buf := New[int]() 397 | buf.Enqueue(1, 2, 3, 4, 5) 398 | return buf 399 | }, 400 | expectedSlice: []int{1, 2, 3, 4, 5}, 401 | expectedSize: 5, 402 | expectedBuffer: []int{1, 2, 3, 4, 5}, 403 | }, 404 | { 405 | name: "Buffer with wrapped elements (head > 0)", 406 | setup: func() *InternalRingBuffer[int] { 407 | buf := New[int](5) 408 | buf.Enqueue(1, 2, 3, 4, 5) 409 | buf.Dequeue() 410 | buf.Dequeue() 411 | buf.Enqueue(6, 7) 412 | return buf 413 | }, 414 | expectedSlice: []int{3, 4, 5, 6, 7}, 415 | expectedSize: 5, 416 | expectedBuffer: []int{3, 4, 5, 6, 7}, 417 | }, 418 | { 419 | name: "Buffer after resize", 420 | setup: func() *InternalRingBuffer[int] { 421 | buf := New[int](4) 422 | buf.Enqueue(1, 2, 3, 4) 423 | buf.Enqueue(5) 424 | return buf 425 | }, 426 | expectedSlice: []int{1, 2, 3, 4, 5}, 427 | expectedSize: 5, 428 | expectedBuffer: []int{1, 2, 3, 4, 5}, 429 | }, 430 | { 431 | name: "Buffer after many enqueue/dequeue operations", 432 | setup: func() *InternalRingBuffer[int] { 433 | buf := New[int](3) 434 | buf.Enqueue(1, 2, 3) 435 | buf.Dequeue() 436 | buf.Dequeue() 437 | buf.Enqueue(4, 5) 438 | buf.Dequeue() 439 | buf.Enqueue(6) 440 | return buf 441 | }, 442 | expectedSlice: []int{4, 5, 6}, 443 | expectedSize: 3, 444 | expectedBuffer: []int{4, 5, 6}, 445 | }, 446 | { 447 | name: "FromSlice initialization", 448 | setup: func() *InternalRingBuffer[int] { 449 | return FromSlice([]int{10, 20, 30, 40}) 450 | }, 451 | expectedSlice: []int{10, 20, 30, 40}, 452 | expectedSize: 4, 453 | expectedBuffer: []int{10, 20, 30, 40}, 454 | }, 455 | } 456 | 457 | for _, scenario := range scenarios { 458 | t.Run(scenario.name, func(t *testing.T) { 459 | buf := scenario.setup() 460 | 461 | if buf.Len() != scenario.expectedSize { 462 | t.Errorf("Before ToSlice: Expected size %d, got %d", scenario.expectedSize, buf.Len()) 463 | } 464 | 465 | result := buf.ToSlice() 466 | 467 | if len(result) != len(scenario.expectedSlice) { 468 | t.Errorf("ToSlice result length: Expected %d, got %d", 469 | len(scenario.expectedSlice), len(result)) 470 | } 471 | 472 | for i, expected := range scenario.expectedSlice { 473 | if i >= len(result) { 474 | t.Errorf("Missing expected element at index %d: %v", i, expected) 475 | continue 476 | } 477 | if result[i] != expected { 478 | t.Errorf("Element mismatch at index %d: Expected %v, got %v", 479 | i, expected, result[i]) 480 | } 481 | } 482 | 483 | bufferSlice := []int{} 484 | originalSize := buf.Len() 485 | for i := 0; i < originalSize; i++ { 486 | val, ok := buf.Dequeue() 487 | if !ok { 488 | t.Fatalf("Failed to dequeue element %d", i) 489 | } 490 | bufferSlice = append(bufferSlice, val) 491 | } 492 | 493 | if len(bufferSlice) != len(scenario.expectedBuffer) { 494 | t.Errorf("After ToSlice - Buffer size changed: Expected %d, got %d", 495 | len(scenario.expectedBuffer), len(bufferSlice)) 496 | } 497 | 498 | for i, expected := range scenario.expectedBuffer { 499 | if i >= len(bufferSlice) { 500 | t.Errorf("After ToSlice - Missing expected element at index %d: %v", i, expected) 501 | continue 502 | } 503 | if bufferSlice[i] != expected { 504 | t.Errorf("After ToSlice - Element mismatch at index %d: Expected %v, got %v", 505 | i, expected, bufferSlice[i]) 506 | } 507 | } 508 | }) 509 | } 510 | } 511 | 512 | func TestInternalRingBuffer_Clone(t *testing.T) { 513 | src := FromSlice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) 514 | dst := src.Clone() 515 | 516 | if !slices.Equal(src.ToSlice(), dst.ToSlice()) { 517 | t.Error("Expected src and destination to have the same underlying data.") 518 | } 519 | 520 | src.Enqueue(11, 12, 13, 14, 15, 16, 17, 18, 19, 20) 521 | 522 | if slices.Equal(src.ToSlice(), dst.ToSlice()) { 523 | t.Error("Expected src and destination to have the varying underlying data.") 524 | } 525 | 526 | dstSlice := dst.ToSlice() 527 | for _, num := range []int{11, 12, 13, 14, 15, 16, 17, 18, 19, 20} { 528 | if slices.Contains(dstSlice, num) { 529 | t.Errorf("dst slice was not supposed to contain %d", num) 530 | } 531 | } 532 | } 533 | 534 | func TestInternalRingBuffer_resize(t *testing.T) { 535 | scenarios := []struct { 536 | name string 537 | setup func() *InternalRingBuffer[int] 538 | resizeTo int 539 | expectedData []int 540 | }{ 541 | { 542 | name: "Resize larger with contiguous data", 543 | setup: func() *InternalRingBuffer[int] { 544 | buf := New[int]() 545 | buf.Enqueue(1, 2, 3) 546 | return buf 547 | }, 548 | resizeTo: 8, 549 | expectedData: []int{1, 2, 3}, 550 | }, 551 | { 552 | name: "Resize with wrapped data", 553 | setup: func() *InternalRingBuffer[int] { 554 | buf := New[int]() 555 | buf.Enqueue(1, 2, 3, 4) 556 | _, _ = buf.Dequeue() // Remove 1 557 | _, _ = buf.Dequeue() // Remove 2 558 | buf.Enqueue(5, 6) // Wrap around 559 | return buf 560 | }, 561 | resizeTo: 10, 562 | expectedData: []int{3, 4, 5, 6}, 563 | }, 564 | } 565 | 566 | for _, scenario := range scenarios { 567 | t.Run(scenario.name, func(t *testing.T) { 568 | buf := scenario.setup() 569 | buf.resize(scenario.resizeTo) 570 | 571 | if buf.capacity != scenario.resizeTo { 572 | t.Errorf("Expected capacity %d, got %d", scenario.resizeTo, buf.capacity) 573 | } 574 | if buf.head != 0 { 575 | t.Errorf("Expected head to be reset to 0, got %d", buf.head) 576 | } 577 | if buf.tail != buf.size { 578 | t.Errorf("Expected tail == size (%d), got %d", buf.size, buf.tail) 579 | } 580 | if buf.size != len(scenario.expectedData) { 581 | t.Fatalf("Expected size %d, got %d", len(scenario.expectedData), buf.size) 582 | } 583 | 584 | for i := 0; i < buf.size; i++ { 585 | actual := buf.data[i] 586 | expected := scenario.expectedData[i] 587 | if actual != expected { 588 | t.Errorf("At index %d: expected %d, got %d", i, expected, actual) 589 | } 590 | } 591 | }) 592 | } 593 | } 594 | 595 | func makeRange(start, end int) []int { 596 | out := make([]int, end-start+1) 597 | for i := range out { 598 | out[i] = start + i 599 | } 600 | 601 | return out 602 | } 603 | -------------------------------------------------------------------------------- /internal/datastructs/tuple/tuple.go: -------------------------------------------------------------------------------- 1 | // Package tuple provides a generic, fixed-size tuple type with safe access and mutation. 2 | package tuple 3 | 4 | import ( 5 | "fmt" 6 | "slices" 7 | ) 8 | 9 | // InternalTuple represents a fixed-length collection of values of type T. 10 | // It supports safe element access, mutation, and conversion to/from slices. 11 | type InternalTuple[T any] struct { 12 | vars []T 13 | } 14 | 15 | // New creates a new InternalTuple from the given variadic values. 16 | // The values are copied to ensure the Tuple does not alias external data. 17 | func New[T any](vars ...T) *InternalTuple[T] { 18 | data := make([]T, len(vars)) 19 | for index := range data { 20 | data[index] = vars[index] 21 | } 22 | 23 | return &InternalTuple[T]{ 24 | vars: data, 25 | } 26 | } 27 | 28 | // FromSlice creates a new InternalTuple by copying the contents of the provided slice. 29 | // The resulting Tuple has the same length as the input slice. 30 | func FromSlice[T any](s []T) *InternalTuple[T] { 31 | return &InternalTuple[T]{ 32 | vars: slices.Clone(s), 33 | } 34 | } 35 | 36 | // Len returns the number of elements in the InternalTuple. 37 | func (t *InternalTuple[T]) Len() int { 38 | return len(t.vars) 39 | } 40 | 41 | // Get returns the element at the specified index and a boolean indicating success. 42 | // If the index is out of bounds, the zero value of T and false are returned. 43 | func (t *InternalTuple[T]) Get(index int) (T, bool) { 44 | if index >= 0 && index < len(t.vars) { 45 | return t.vars[index], true 46 | } 47 | 48 | var data T 49 | return data, false 50 | } 51 | 52 | // Set updates the element at the specified index to the given value. 53 | // It returns true if the operation was successful, or false if the index was out of bounds. 54 | func (t *InternalTuple[T]) Set(index int, v T) bool { 55 | if index >= 0 && index < len(t.vars) { 56 | t.vars[index] = v 57 | return true 58 | } 59 | 60 | return false 61 | } 62 | 63 | // ToSlice returns a copy of the InternalTuple's internal values as a slice. 64 | func (t *InternalTuple[T]) ToSlice() []T { 65 | return slices.Clone(t.vars) 66 | } 67 | 68 | // String returns a string representation of the InternalTuple's contents. 69 | func (t *InternalTuple[T]) String() string { 70 | return fmt.Sprintf("%v", t.vars) 71 | } 72 | -------------------------------------------------------------------------------- /internal/datastructs/tuple/tuple_test.go: -------------------------------------------------------------------------------- 1 | package tuple 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "testing" 7 | 8 | islices "github.com/PsionicAlch/byteforge/internal/functions/slices" 9 | ) 10 | 11 | func TestInternalTuple_New(t *testing.T) { 12 | scenarios := []struct { 13 | name string 14 | data []int 15 | }{ 16 | {"New with no elements", []int{}}, 17 | {"New with 1 elements", []int{1}}, 18 | {"New with 2 elements", []int{1, 2}}, 19 | {"New with 3 elements", []int{1, 2, 3}}, 20 | {"New with 100 elements", islices.ERange(0, 100)}, 21 | } 22 | 23 | for _, scenario := range scenarios { 24 | t.Run(scenario.name, func(t *testing.T) { 25 | tup := New(scenario.data...) 26 | 27 | if tup == nil { 28 | t.Error("Expected tup to not be nil") 29 | } 30 | 31 | if tup.vars == nil { 32 | t.Error("Expected tup.vars to not be nil") 33 | } 34 | 35 | if len(tup.vars) != len(scenario.data) { 36 | t.Errorf("Expected len(tup.vars) to be %d. Got %d", len(scenario.data), len(tup.vars)) 37 | } 38 | 39 | if !slices.Equal(scenario.data, tup.vars) { 40 | t.Error("Expected tup.vars to be equal to scenario.data") 41 | } 42 | }) 43 | } 44 | } 45 | 46 | func TestInternalTuple_FromSlice(t *testing.T) { 47 | scenarios := []struct { 48 | name string 49 | data []int 50 | }{ 51 | {"FromSlice with no elements", []int{}}, 52 | {"FromSlice with 1 elements", []int{1}}, 53 | {"FromSlice with 2 elements", []int{1, 2}}, 54 | {"FromSlice with 3 elements", []int{1, 2, 3}}, 55 | {"FromSlice with 100 elements", islices.ERange(0, 100)}, 56 | } 57 | 58 | for _, scenario := range scenarios { 59 | t.Run(scenario.name, func(t *testing.T) { 60 | tup := FromSlice(scenario.data) 61 | 62 | if tup == nil { 63 | t.Error("Expected tup to not be nil") 64 | } 65 | 66 | if tup.vars == nil { 67 | t.Error("Expected tup.vars to not be nil") 68 | } 69 | 70 | if len(tup.vars) != len(scenario.data) { 71 | t.Errorf("Expected len(tup.vars) to be %d. Got %d", len(scenario.data), len(tup.vars)) 72 | } 73 | 74 | if !slices.Equal(scenario.data, tup.vars) { 75 | t.Error("Expected tup.vars to be equal to scenario.data") 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func TestInternalTuple_Len(t *testing.T) { 82 | scenarios := []struct { 83 | name string 84 | data []int 85 | }{ 86 | {"Len with no elements", []int{}}, 87 | {"Len with 1 elements", []int{1}}, 88 | {"Len with 2 elements", []int{1, 2}}, 89 | {"Len with 3 elements", []int{1, 2, 3}}, 90 | {"Len with 100 elements", islices.ERange(0, 100)}, 91 | } 92 | 93 | for _, scenario := range scenarios { 94 | t.Run(scenario.name, func(t *testing.T) { 95 | tup := FromSlice(scenario.data) 96 | 97 | if tup.Len() != len(scenario.data) { 98 | t.Errorf("Expected tup.Len() to be %d. Got %d", len(scenario.data), len(tup.vars)) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestInternalTuple_Get(t *testing.T) { 105 | scenarios := []struct { 106 | name string 107 | data []int 108 | }{ 109 | {"Get with no elements", []int{}}, 110 | {"Get with 1 elements", []int{1}}, 111 | {"Get with 2 elements", []int{1, 2}}, 112 | {"Get with 3 elements", []int{1, 2, 3}}, 113 | {"Get with 100 elements", islices.ERange(0, 100)}, 114 | } 115 | 116 | for _, scenario := range scenarios { 117 | t.Run(scenario.name, func(t *testing.T) { 118 | tup := FromSlice(scenario.data) 119 | 120 | for index := range scenario.data { 121 | element, found := tup.Get(index) 122 | 123 | if !found { 124 | t.Errorf("Expected to find element at index %d", index) 125 | } else { 126 | if element != scenario.data[index] { 127 | t.Errorf("Expected element to be %d. Got %d", scenario.data[index], element) 128 | } 129 | } 130 | 131 | } 132 | 133 | _, found := tup.Get(len(scenario.data)) 134 | if found { 135 | t.Error("Found element at index outside the valid range.") 136 | } 137 | }) 138 | } 139 | } 140 | 141 | func TestInternalTuple_Set(t *testing.T) { 142 | tup := New(1) 143 | 144 | if !tup.Set(0, 2) { 145 | t.Error("Failed to set first element.") 146 | } 147 | 148 | if element, found := tup.Get(0); found && element != 2 { 149 | t.Errorf("Expected element to be 2. Got %d", element) 150 | } 151 | 152 | if tup.Set(1, 2) { 153 | t.Error("Set element outside of valid range.") 154 | } 155 | } 156 | 157 | func TestInternalTuple_ToSlice(t *testing.T) { 158 | scenarios := []struct { 159 | name string 160 | data []int 161 | }{ 162 | {"ToSlice with no elements", []int{}}, 163 | {"ToSlice with 1 elements", []int{1}}, 164 | {"ToSlice with 2 elements", []int{1, 2}}, 165 | {"ToSlice with 3 elements", []int{1, 2, 3}}, 166 | {"ToSlice with 100 elements", islices.ERange(0, 100)}, 167 | } 168 | 169 | for _, scenario := range scenarios { 170 | t.Run(scenario.name, func(t *testing.T) { 171 | tup := FromSlice(scenario.data) 172 | 173 | if !slices.Equal(tup.ToSlice(), scenario.data) { 174 | t.Error("Expected tup.ToSlice() to be equal to scenario.data") 175 | } 176 | }) 177 | } 178 | } 179 | 180 | func TestInternalTuple_String(t *testing.T) { 181 | scenarios := []struct { 182 | name string 183 | data []int 184 | }{ 185 | {"String with no elements", []int{}}, 186 | {"String with 1 elements", []int{1}}, 187 | {"String with 2 elements", []int{1, 2}}, 188 | {"String with 3 elements", []int{1, 2, 3}}, 189 | {"String with 100 elements", islices.ERange(0, 100)}, 190 | } 191 | 192 | for _, scenario := range scenarios { 193 | t.Run(scenario.name, func(t *testing.T) { 194 | tup := FromSlice(scenario.data) 195 | s := fmt.Sprintf("%v", scenario.data) 196 | 197 | if tup.String() != s { 198 | t.Errorf("Expected tup.String() to be equal to \"%s\"", s) 199 | } 200 | }) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /internal/functions/slices/range.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "github.com/PsionicAlch/byteforge/constraints" 5 | ) 6 | 7 | // IRange generates a slice of numbers from min to max, inclusive. 8 | func IRange[T constraints.Number](min, max T, step ...T) []T { 9 | var stepSize T 10 | if len(step) > 0 { 11 | stepSize = step[0] 12 | } 13 | 14 | stepSize, correct := validateRangeParams(min, max, stepSize) 15 | if !correct { 16 | return []T{} 17 | } 18 | 19 | var nums []T 20 | 21 | for i := min; (stepSize > 0 && i <= max) || (stepSize < 0 && i >= max); i += stepSize { 22 | nums = append(nums, i) 23 | } 24 | 25 | return nums 26 | } 27 | 28 | // ERange generates a slice of numbers from min up to, but not including, max. 29 | func ERange[T constraints.Number](min, max T, step ...T) []T { 30 | var stepSize T 31 | if len(step) > 0 { 32 | stepSize = step[0] 33 | } 34 | 35 | stepSize, correct := validateRangeParams(min, max, stepSize) 36 | if !correct { 37 | return []T{} 38 | } 39 | 40 | var nums []T 41 | 42 | for i := min; (stepSize > 0 && i < max) || (stepSize < 0 && i > max); i += stepSize { 43 | nums = append(nums, i) 44 | } 45 | 46 | return nums 47 | } 48 | 49 | // validateRangeParams checks that the step value is appropriate for the given min and max. 50 | func validateRangeParams[T constraints.Number](min, max, step T) (T, bool) { 51 | // Check for zero step 52 | var zero T = max - max 53 | if step == zero { 54 | // Determine default step based on direction 55 | if min > max { 56 | step = max - max - 1 57 | } else { 58 | step = max - max + 1 59 | } 60 | } 61 | 62 | // Detect direction mismatch 63 | if min < max && step <= zero { 64 | return step, false 65 | } 66 | 67 | if min > max && step >= zero { 68 | return step, false 69 | } 70 | 71 | return step, true 72 | } 73 | -------------------------------------------------------------------------------- /internal/functions/slices/range_test.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "slices" 5 | "testing" 6 | ) 7 | 8 | func TestIRange(t *testing.T) { 9 | t.Run("Basic use cases", func(t *testing.T) { 10 | tests := []struct { 11 | min, max int 12 | step []int 13 | expected []int 14 | }{ 15 | {1, 5, nil, []int{1, 2, 3, 4, 5}}, 16 | {5, 1, nil, []int{5, 4, 3, 2, 1}}, 17 | {0, 10, []int{2}, []int{0, 2, 4, 6, 8, 10}}, 18 | {10, 0, []int{-2}, []int{10, 8, 6, 4, 2, 0}}, 19 | {3, 3, nil, []int{3}}, 20 | } 21 | 22 | for _, tt := range tests { 23 | out := IRange(tt.min, tt.max, tt.step...) 24 | if !slices.Equal(out, tt.expected) { 25 | t.Errorf("IRange(%v, %v, %v) = %v; want %v", tt.min, tt.max, tt.step, out, tt.expected) 26 | } 27 | } 28 | }) 29 | 30 | t.Run("Invalid range", func(t *testing.T) { 31 | out := IRange(1, 10, -1) 32 | if !slices.Equal(out, []int{}) { 33 | t.Errorf("Expected to get empty slice. Got %#v", out) 34 | } 35 | 36 | out = IRange(10, 1, 1) 37 | if !slices.Equal(out, []int{}) { 38 | t.Errorf("Expected to get empty slice for step in wrong direction. Got %#v", out) 39 | } 40 | }) 41 | } 42 | 43 | func TestERange(t *testing.T) { 44 | t.Run("Basic use cases", func(t *testing.T) { 45 | tests := []struct { 46 | min, max int 47 | step []int 48 | expected []int 49 | }{ 50 | {1, 5, nil, []int{1, 2, 3, 4}}, 51 | {5, 1, nil, []int{5, 4, 3, 2}}, 52 | {0, 10, []int{3}, []int{0, 3, 6, 9}}, 53 | {10, 0, []int{-3}, []int{10, 7, 4, 1}}, 54 | {3, 3, nil, []int{}}, 55 | } 56 | 57 | for _, tt := range tests { 58 | out := ERange(tt.min, tt.max, tt.step...) 59 | if !slices.Equal(out, tt.expected) { 60 | t.Errorf("ERange(%v, %v, %v) = %v; want %v", tt.min, tt.max, tt.step, out, tt.expected) 61 | } 62 | } 63 | }) 64 | 65 | t.Run("Invalid range", func(t *testing.T) { 66 | out := ERange(1, 10, -1) 67 | if !slices.Equal(out, []int{}) { 68 | t.Errorf("Expected to get empty slice for step in wrong direction. Got %#v", out) 69 | } 70 | 71 | out = ERange(10, 1, 1) 72 | if !slices.Equal(out, []int{}) { 73 | t.Errorf("Expected to get empty slice step in wrong direction. Got %#v", out) 74 | } 75 | }) 76 | } 77 | 78 | func TestValidateRangeParams(t *testing.T) { 79 | type testCase[T any] struct { 80 | min, max, step T 81 | expectedStep T 82 | expectValid bool 83 | } 84 | 85 | tests := []testCase[int]{ 86 | {1, 5, 0, 1, true}, 87 | {5, 1, 0, -1, true}, 88 | {5, 1, -1, -1, true}, 89 | {1, 5, -1, -1, false}, 90 | {5, 1, 1, 1, false}, 91 | } 92 | 93 | for _, tt := range tests { 94 | step, valid := validateRangeParams(tt.min, tt.max, tt.step) 95 | 96 | if tt.expectValid && !valid { 97 | t.Errorf("Expected to be valid for min=%v, max=%v, step=%v", tt.min, tt.max, tt.step) 98 | 99 | continue 100 | } 101 | 102 | if step != tt.expectedStep { 103 | t.Errorf("got step %v, want %v", step, tt.expectedStep) 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /internal/functions/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "unsafe" 4 | 5 | func SortByAddress[Q any, T *Q](a, b T) (T, T) { 6 | if uintptr(unsafe.Pointer(a)) < uintptr(unsafe.Pointer(b)) { 7 | return a, b 8 | } 9 | 10 | return b, a 11 | } 12 | -------------------------------------------------------------------------------- /internal/functions/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | "unsafe" 6 | ) 7 | 8 | func TestSortByAddress(t *testing.T) { 9 | type sample struct { 10 | value int 11 | } 12 | 13 | x := &sample{value: 1} 14 | y := &sample{value: 2} 15 | 16 | xAddr := uintptr(unsafe.Pointer(x)) 17 | yAddr := uintptr(unsafe.Pointer(y)) 18 | 19 | if xAddr == yAddr { 20 | t.Fatalf("unexpected: x and y have the same address (%p)", x) 21 | } 22 | 23 | a1, b1 := SortByAddress(x, y) 24 | if uintptr(unsafe.Pointer(a1)) > uintptr(unsafe.Pointer(b1)) { 25 | t.Errorf("SortByAddress(x, y) returned addresses out of order: %p > %p", a1, b1) 26 | } 27 | 28 | a2, b2 := SortByAddress(y, x) 29 | if uintptr(unsafe.Pointer(a2)) > uintptr(unsafe.Pointer(b2)) { 30 | t.Errorf("SortByAddress(y, x) returned addresses out of order: %p > %p", a2, b2) 31 | } 32 | 33 | if a1 != a2 || b1 != b2 { 34 | t.Errorf("SortByAddress(x, y) and SortByAddress(y, x) gave inconsistent results:\n (a1, b1) = (%p, %p)\n (a2, b2) = (%p, %p)", a1, b1, a2, b2) 35 | } 36 | } 37 | --------------------------------------------------------------------------------