diff --git a/README.md b/README.md index 482397c..bbbcaf2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # collections-go -A dependency-free package of generic Go collection data structures. Compatible with Go 1.18 and above. +A dependency-free package of generic Go collection data structures. Compatible with Go 1.18 and above. Strives for the most optimal solutions available with lowest number of allocations. + +Contributions are not currently open as this project is in active early development. diff --git a/predicate.go b/predicate.go new file mode 100644 index 0000000..0fdf162 --- /dev/null +++ b/predicate.go @@ -0,0 +1,9 @@ +package collections + +// A UnaryPredicate is a function that takes a single element of type +// T and returns a boolean. +type UnaryPredicate[T any] func(T) bool + +// A UnaryOperator is a function that takes a single element of type +// T and returns an element of type T. +type UnaryOperator[T any] func(T) T diff --git a/set.go b/set.go index 72bdc1f..2fa21ff 100644 --- a/set.go +++ b/set.go @@ -6,16 +6,16 @@ package collections // empty struct initialization any time you add to the set. type Set[T comparable] map[T]struct{} -// Initialize a new Set by providing a slice. The Set will have the -// same type as the slice. +// Initialize a new Set with a set of arguments. Good for if you want to initialize +// a new set in place with values. // -// Time Complexity: O(n) -// Space Complexity: O(n) -// Allocations: 1 set, n elements -func NewSet[T comparable](slice []T) Set[T] { +// Time Complexity: O(n) +// Space Complexity: O(n) +// Allocations: 1 set, n elements. +func NewSet[T comparable](elements ...T) Set[T] { set := make(Set[T]) - for _, el := range slice { - set[el] = struct{}{} + for i := 0; i < len(elements); i++ { + set.Add(elements[i]) } return set } @@ -23,11 +23,11 @@ func NewSet[T comparable](slice []T) Set[T] { // Initialize a new set while preserving the elements that were found to be duplicate. // The duplicate slice will be nil if there are no duplicates to save on allocations. // -// Time Complexity: O(n) -// Space Complexity: O(n) -// Allocations (best case): 1 set, n elements -// Allocations (worst case): 1 set, n/2 elements, 2 slices, one with n-1 space, the -// other is the first one resized based on number of duplicates. +// Time Complexity: O(n) +// Space Complexity: O(n) +// Allocations (best case): 1 set, n elements +// Allocations (worst case): 1 set, n/2 elements, 2 slices, one with n-1 space, the +// second is the first one resized if necessary. func NewSetSaveDuplicates[T comparable](slice []T) (Set[T], []T) { set := make(Set[T]) var dupes []T = nil @@ -46,14 +46,17 @@ func NewSetSaveDuplicates[T comparable](slice []T) (Set[T], []T) { set[el] = struct{}{} } } - return set, dupes[:dupesIdx] + if dupes != nil && dupesIdx < len(dupes)-1 { + dupes = dupes[:dupesIdx] + } + return set, dupes } // ToSlice will take all of the elements of the set and create a new slice out of them. // -// Time Complexity: O(n) -// Space Complexity: O(n) -// Allocations: 1 slice, n elements +// Time Complexity: O(n) +// Space Complexity: O(n) +// Allocations: 1 slice, n elements func (set Set[T]) ToSlice() []T { slice := make([]T, len(set)) i := 0 @@ -66,27 +69,27 @@ func (set Set[T]) ToSlice() []T { // Add an element to the set. // -// Time Complexity: O(1) -// Space Complexity: O(1) -// Allocations: 1 elements +// Time Complexity: O(1) +// Space Complexity: O(1) +// Allocations: 1 elements func (s Set[T]) Add(el T) { s[el] = struct{}{} } // Remove an element from the set. // -// Time Complexity: O(1) -// Space Complexity: O(1) -// Allocations: None +// Time Complexity: O(1) +// Space Complexity: O(1) +// Allocations: None func (s Set[T]) Remove(el T) { delete(s, el) } // Check if the set contains a particular value. // -// Time Complexity: O(1) -// Space Complexity: O(1) -// Allocations: None +// Time Complexity: O(1) +// Space Complexity: O(1) +// Allocations: None func (s Set[T]) Contains(el T) bool { _, containsEl := s[el] return containsEl @@ -94,9 +97,9 @@ func (s Set[T]) Contains(el T) bool { // Creates a Clone of the set which contains all the same values. // -// Time Complexity: O(n) -// Space Complexity: O(n) -// Allocations: 1 set, n elements +// Time Complexity: O(n) +// Space Complexity: O(n) +// Allocations: 1 set, n elements func (s Set[T]) Clone() Set[T] { clone := make(Set[T]) for el := range s { @@ -107,9 +110,9 @@ func (s Set[T]) Clone() Set[T] { // Checks if the set is equal to another. // -// Time Complexity: O(n) -// Space Complexity: O(1) -// Allocations: None +// Time Complexity: O(n) +// Space Complexity: O(1) +// Allocations: None func (s Set[T]) Equals(s2 Set[T]) bool { if len(s) != len(s2) { return false diff --git a/set_test.go b/set_test.go index 08e0acd..aa6cff6 100644 --- a/set_test.go +++ b/set_test.go @@ -7,14 +7,14 @@ import ( "github.com/RageCage64/go-assert" ) -func TestSetToSlice(t *testing.T) { - set := collections.Set[int]{ - 1: {}, - 2: {}, - 3: {}, - } - slice := set.ToSlice() - assert.SliceEqual(t, []int{1, 2, 3}, slice) +func runNewSetTestCase[T comparable](t *testing.T, elements ...T) { + t.Helper() + + compareSetAndSlice(t, collections.NewSet(elements...), elements) +} + +func TestNewSet(t *testing.T) { + runNewSetTestCase(t, []int{1, 2, 3}...) } func runNewSetSaveDuplicatesTestCase[T comparable]( @@ -24,6 +24,7 @@ func runNewSetSaveDuplicatesTestCase[T comparable]( expectedDupes []T, ) { t.Helper() + newSet, dupes := collections.NewSetSaveDuplicates(slice) assert.Assert( t, @@ -76,3 +77,41 @@ func TestNewSetSaveDuplicates(t *testing.T) { ) }) } + +func runSetToSliceTestCase[T comparable](t *testing.T, set collections.Set[T], expected []T) { + assert.SliceEqual(t, expected, set.ToSlice()) +} + +func TestSetToSlice(t *testing.T) { + runSetToSliceTestCase( + t, + collections.Set[int]{ + 1: {}, + 2: {}, + 3: {}, + }, + []int{1, 2, 3}, + ) +} + +func compareSetAndSlice[T comparable](t *testing.T, set collections.Set[T], slice []T) { + t.Helper() + + for _, el := range slice { + if !set.Contains(el) { + t.Fatalf("expected set to contain %v", el) + } + } + for el := range set { + found := false + for _, sliceEl := range slice { + if el == sliceEl { + found = true + break + } + } + if !found { + t.Fatalf("set contained unexpected element %v", el) + } + } +} diff --git a/slice.go b/slice.go new file mode 100644 index 0000000..181cf8f --- /dev/null +++ b/slice.go @@ -0,0 +1,87 @@ +package collections + +/***************** +This section contains the alias API, which allows for receiver-style calling +convention. +*****************/ + +// Slice is an alias over a Go slice that enables a direct receiver-style API. +// For Slice, T must satisfy comparable. +type Slice[T comparable] []T + +// Check if a slice contains an element. Calls SliceContains. +func (sl Slice[T]) Contains(needle T) bool { + return SliceContains(sl, needle) +} + +// Run an operator on every element in the slice, and return a slice that +// contains the result of every operation. Calls SliceMap. +func (sl Slice[T]) Map(op UnaryOperator[T]) Slice[T] { + return SliceMap(sl, op) +} + +// AnySlice is an alias over a Go slice that does not enforce T satisfying +// comparable. Any method that relies on comparison of elements is not +// available for AnySlice. +type AnySlice[T any] []T + +// Run an operator on every element in the slice, and return a slice that +// contains the result of every operation. Calls SliceMap. +func (sl AnySlice[T]) Map(op UnaryOperator[T]) AnySlice[T] { + return SliceMap(sl, op) +} + +/***************** +This section contains the direct API, which is a more traditional Go style +API. +*****************/ + +// Check if a slice contains an element. +// +// Time Complexity: O(n) +// Space Complexity: O(1) +// Allocations: None. +func SliceContains[T comparable](haystack []T, needle T) bool { + for i := 0; i < len(haystack); i++ { + if haystack[i] == needle { + return true + } + } + return false +} + +// Run an operator on every element in the slice, and return a slice that +// contains the result of every operation. +// +// Time Complexity: O(n * m) (where m = complexity of operator) +// Space Complexity: O(n) +// Allocations: 1 slice, n elements. +func SliceMap[T any](sl []T, op UnaryOperator[T]) []T { + result := make([]T, len(sl)) + for i := 0; i < len(sl); i++ { + result[i] = op(sl[i]) + } + return result +} + +// Run a predicate on every element in the slice, and return a slice +// that contains every element for which the predicate was true. +// +// Time Complexity: O(n * m) (where m = complexity of predicate) +// Space Complexity: O(n) +// Allocations: 2 slice, first with n elements, second is the first slice +// resized if necessary. +func SliceFilter[T any](sl []T, pred UnaryPredicate[T]) []T { + result := make([]T, len(sl)) + resultIdx := 0 + for i := 0; i < len(sl); i++ { + if pred(sl[i]) { + result[resultIdx] = sl[i] + resultIdx++ + } + } + if resultIdx < len(result)-1 { + result = result[:resultIdx] + } + return result +} diff --git a/slice_test.go b/slice_test.go new file mode 100644 index 0000000..7a18bf5 --- /dev/null +++ b/slice_test.go @@ -0,0 +1,13 @@ +package collections_test + +import ( + "testing" + + "github.com/RageCage64/collections-go" + "github.com/RageCage64/go-assert" +) + +func TestSliceContainsDirect(t *testing.T) { + x := []int{1, 2, 3} + assert.Assert(t, collections.SliceContains(x, 1), "it didn't") +}