diff --git a/internal/slice/count_unique.go b/internal/slice/count_unique.go index 8d91ad2..0b30279 100644 --- a/internal/slice/count_unique.go +++ b/internal/slice/count_unique.go @@ -1,5 +1,7 @@ package slice +// CountUniqueInSorted counts the number of unique elements in a sorted slice. +// It assumes the input slice is already sorted. func CountUniqueInSorted[T comparable](s []T) int { out := 0 var previous T @@ -50,9 +52,11 @@ func GroupSorted[E any, K comparable](s []E, sKeys []K) (map[K]IndexRange, []K) previousIdx = i } } - groups[previous] = IndexRange{ - Offset: previousIdx, - Length: len(s) - previousIdx, + if len(s) > 0 { + groups[previous] = IndexRange{ + Offset: previousIdx, + Length: len(s) - previousIdx, + } } } return groups, keys diff --git a/internal/slice/or_alloc.go b/internal/slice/or_alloc.go index 783fab6..270fe0b 100644 --- a/internal/slice/or_alloc.go +++ b/internal/slice/or_alloc.go @@ -1,5 +1,8 @@ package slice +// OrAlloc ensures that a slice has the specified length. +// If the input slice is shorter, it's extended if possible, and reallocated otherwise. +// If it's longer, it's truncated. func OrAlloc[T any](s []T, n int) []T { if len(s) == n { return s diff --git a/internal/slice/reorder.go b/internal/slice/reorder.go index ea37936..53dbaf5 100644 --- a/internal/slice/reorder.go +++ b/internal/slice/reorder.go @@ -1,5 +1,7 @@ package slice +// ReorderInPlace reorders elements in a slice based on the provided indices. +// The swap function is used to perform the actual swapping of elements. func ReorderInPlace(swap func(i, j int), indices []int) { for i, targetIdx := range indices { for targetIdx < i { diff --git a/internal/slice/slice_test.go b/internal/slice/slice_test.go new file mode 100644 index 0000000..5865bcb --- /dev/null +++ b/internal/slice/slice_test.go @@ -0,0 +1,131 @@ +package slice_test + +import ( + "reflect" + "testing" + + "github.com/keilerkonzept/bitknn/internal/slice" +) + +func TestCountUniqueInSorted(t *testing.T) { + tests := []struct { + name string + input []int + expected int + }{ + {"Empty slice", []int{}, 0}, + {"Single element", []int{1}, 1}, + {"All unique", []int{1, 2, 3, 4, 5}, 5}, + {"Some duplicates", []int{1, 1, 2, 3, 3, 4, 5, 5}, 5}, + {"All duplicates", []int{1, 1, 1, 1, 1}, 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := slice.CountUniqueInSorted(tt.input) + if result != tt.expected { + t.Errorf("CountUniqueInSorted(%v) = %d, want %d", tt.input, result, tt.expected) + } + }) + } +} + +func TestGroupSorted(t *testing.T) { + tests := []struct { + name string + input []int + keys []string + expectedGroups map[string]slice.IndexRange + expectedKeys []string + }{ + { + name: "Empty slices", + input: []int{}, + keys: []string{}, + expectedGroups: map[string]slice.IndexRange{}, + expectedKeys: []string{}, + }, + { + name: "Single group", + input: []int{1, 2, 3}, + keys: []string{"a", "a", "a"}, + expectedGroups: map[string]slice.IndexRange{"a": {Offset: 0, Length: 3}}, + expectedKeys: []string{"a"}, + }, + { + name: "Multiple groups", + input: []int{1, 2, 3, 4, 5, 6}, + keys: []string{"a", "a", "b", "b", "c", "c"}, + expectedGroups: map[string]slice.IndexRange{ + "a": {Offset: 0, Length: 2}, + "b": {Offset: 2, Length: 2}, + "c": {Offset: 4, Length: 2}, + }, + expectedKeys: []string{"a", "b", "c"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + groups, keys := slice.GroupSorted(tt.input, tt.keys) + if !reflect.DeepEqual(groups, tt.expectedGroups) { + t.Errorf("GroupSorted() groups = %v, want %v", groups, tt.expectedGroups) + } + if !reflect.DeepEqual(keys, tt.expectedKeys) { + t.Errorf("GroupSorted() keys = %v, want %v", keys, tt.expectedKeys) + } + }) + } +} + +func TestOrAlloc(t *testing.T) { + tests := []struct { + name string + input []int + n int + expected []int + }{ + {"Empty slice, n=0", []int{}, 0, []int{}}, + {"Empty slice, n>0", []int{}, 3, []int{0, 0, 0}}, + {"Slice shorter than n, reuse", []int{1, 2, 3, 4}[:2], 4, []int{1, 2, 3, 4}}, + {"Slice shorter than n, realloc", []int{1, 2}, 4, []int{0, 0, 0, 0}}, + {"Slice longer than n", []int{1, 2, 3, 4, 5}, 3, []int{1, 2, 3}}, + {"Slice equal to n", []int{1, 2, 3}, 3, []int{1, 2, 3}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := slice.OrAlloc(tt.input, tt.n) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("OrAlloc(%v, %d) = %v, want %v", tt.input, tt.n, result, tt.expected) + } + }) + } +} + +func TestReorderInPlace(t *testing.T) { + tests := []struct { + name string + input []int + indices []int + expected []int + }{ + {"Empty slice", []int{}, []int{}, []int{}}, + {"No reordering", []int{1, 2, 3}, []int{0, 1, 2}, []int{1, 2, 3}}, + {"Simple reordering", []int{1, 2, 3}, []int{2, 0, 1}, []int{3, 1, 2}}, + {"Complex reordering", []int{1, 2, 3, 4, 5}, []int{4, 3, 2, 1, 0}, []int{5, 4, 3, 2, 1}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := make([]int, len(tt.input)) + copy(input, tt.input) + slice.ReorderInPlace(func(i, j int) { + input[i], input[j] = input[j], input[i] + }, tt.indices) + if !reflect.DeepEqual(input, tt.expected) { + t.Errorf("ReorderInPlace() result = %v, want %v", input, tt.expected) + } + }) + } +}