diff --git a/tools/ordered_set.go b/tools/ordered_set.go new file mode 100644 index 00000000..83d356c0 --- /dev/null +++ b/tools/ordered_set.go @@ -0,0 +1,217 @@ +package tools + +// OrderedSet is a unique set of strings that maintains insertion order. +type OrderedSet struct { + // s is the set of strings that we're keeping track of. + s []string + // m is a mapping of string value "s" into the index "i" that that + // string is present in in the given "s". + m map[string]int +} + +// NewOrderedSet creates an ordered set with no values. +func NewOrderedSet() *OrderedSet { + return NewOrderedSetWithCapacity(0) +} + +// NewOrderedSetWithCapacity creates a new ordered set with no values. The +// returned ordered set can be appended to "capacity" number of times before it +// grows internally. +func NewOrderedSetWithCapacity(capacity int) *OrderedSet { + return &OrderedSet{ + s: make([]string, 0, capacity), + m: make(map[string]int, capacity), + } +} + +// NewOrderedSetFromSlice returns a new ordered set with the elements given in +// the slice "s". +func NewOrderedSetFromSlice(s []string) *OrderedSet { + set := NewOrderedSetWithCapacity(len(s)) + for _, e := range s { + set.Add(e) + } + + return set +} + +// Add adds the given element "i" to the ordered set, unless the element is +// already present. It returns whether or not the element was added. +func (s *OrderedSet) Add(i string) bool { + if _, ok := s.m[i]; ok { + return false + } + + s.s = append(s.s, i) + s.m[i] = len(s.s) - 1 + + return true +} + +// Contains returns whether or not the given "i" is contained in this ordered +// set. It is a constant-time operation. +func (s *OrderedSet) Contains(i string) bool { + if _, ok := s.m[i]; ok { + return true + } + return false +} + +// ContainsAll returns whether or not all of the given items in "i" are present +// in the ordered set. +func (s *OrderedSet) ContainsAll(i ...string) bool { + for _, item := range i { + if !s.Contains(item) { + return false + } + } + return true +} + +// IsSubset returns whether other is a subset of this ordered set. In other +// words, it returns whether or not all of the elements in "other" are also +// present in this set. +func (s *OrderedSet) IsSubset(other *OrderedSet) bool { + for _, i := range other.s { + if !s.Contains(i) { + return false + } + } + return true +} + +// IsSuperset returns whether or not this set is a superset of "other". In other +// words, it returns whether or not all of the elements in this set are also in +// the set "other". +func (s *OrderedSet) IsSuperset(other *OrderedSet) bool { + return other.IsSubset(s) +} + +// Union returns a union of this set with the given set "other". It returns the +// items that are in either set while maintaining uniqueness constraints. It +// preserves ordered within each set, and orders the elements in this set before +// the elements in "other". +// +// It is an O(n+m) operation. +func (s *OrderedSet) Union(other *OrderedSet) *OrderedSet { + union := NewOrderedSetWithCapacity(other.Cardinality() + s.Cardinality()) + + for _, e := range s.s { + union.Add(e) + } + for _, e := range other.s { + union.Add(e) + } + + return union +} + +// Intersect returns the elements that are in both this set and then given +// "ordered" set. It is an O(min(n, m)) (in other words, O(n)) operation. +func (s *OrderedSet) Intersect(other *OrderedSet) *OrderedSet { + intersection := NewOrderedSetWithCapacity(MinInt( + s.Cardinality(), other.Cardinality())) + + if s.Cardinality() < other.Cardinality() { + for _, elem := range s.s { + if other.Contains(elem) { + intersection.Add(elem) + } + } + } else { + for _, elem := range other.s { + if s.Contains(elem) { + intersection.Add(elem) + } + } + } + + return intersection +} + +// Difference returns the elements that are in this set, but not included in +// other. +func (s *OrderedSet) Difference(other *OrderedSet) *OrderedSet { + diff := NewOrderedSetWithCapacity(s.Cardinality()) + for _, e := range s.s { + if !other.Contains(e) { + diff.Add(e) + } + } + + return diff +} + +// SymmetricDifference returns the elements that are not present in both sets. +func (s *OrderedSet) SymmetricDifference(other *OrderedSet) *OrderedSet { + left := s.Difference(other) + right := other.Difference(s) + + return left.Union(right) +} + +// Clear removes all elements from this set. +func (s *OrderedSet) Clear() { + s.s = make([]string, 0) + s.m = make(map[string]int, 0) +} + +// Remove removes the given element "i" from this set. +func (s *OrderedSet) Remove(i string) { + idx, ok := s.m[i] + if !ok { + return + } + + rest := MinInt(idx+1, len(s.s)-1) + + s.s = append(s.s[:idx], s.s[rest:]...) + for _, e := range s.s[rest:] { + s.m[e] = s.m[e] - 1 + } + delete(s.m, i) +} + +// Cardinality returns the cardinality of this set. +func (s *OrderedSet) Cardinality() int { + return len(s.s) +} + +// Iter returns a channel which yields the elements in this set in insertion +// order. +func (s *OrderedSet) Iter() <-chan string { + c := make(chan string) + go func() { + for _, i := range s.s { + c <- i + } + close(c) + }() + + return c +} + +// Equal returns whether this element has the same number, identity and ordering +// elements as given in "other". +func (s *OrderedSet) Equal(other *OrderedSet) bool { + if s.Cardinality() != other.Cardinality() { + return false + } + + for e, i := range s.m { + if ci, ok := other.m[e]; !ok || ci != i { + return false + } + } + + return true +} + +// Clone returns a deep copy of this set. +func (s *OrderedSet) Clone() *OrderedSet { + clone := NewOrderedSetWithCapacity(s.Cardinality()) + for _, i := range s.s { + clone.Add(i) + } + return clone +} diff --git a/tools/ordered_set_test.go b/tools/ordered_set_test.go new file mode 100644 index 00000000..97c69d10 --- /dev/null +++ b/tools/ordered_set_test.go @@ -0,0 +1,217 @@ +package tools + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOrderedSetAddAddsElements(t *testing.T) { + s := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + assert.False(t, s.Contains("d"), + "tools: did not expected s to contain \"d\"") + + assert.True(t, s.Add("d")) + + assert.True(t, s.Contains("d"), + "tools: expected s to contain \"d\"") +} + +func TestOrderedSetContainsReturnsTrueForItemsItContains(t *testing.T) { + s := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + assert.True(t, s.Contains("b"), + "tools: expected s to contain element \"b\"") +} + +func TestOrderedSetContainsReturnsFalseForItemsItDoesNotContains(t *testing.T) { + s := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + assert.False(t, s.Contains("d"), + "tools: did not expect s to contain element \"d\"") +} + +func TestOrderedSetContainsAllReturnsTrueWhenAllElementsAreContained(t *testing.T) { + s := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + assert.True(t, s.ContainsAll("b", "c"), + "tools: expected s to contain element \"b\" and \"c\"") +} + +func TestOrderedSetContainsAllReturnsFalseWhenAllElementsAreNotContained(t *testing.T) { + s := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + assert.False(t, s.ContainsAll("b", "c", "d"), + "tools: did not expect s to contain element \"b\", \"c\" and \"d\"") +} + +func TestOrderedSetIsSubsetReturnsTrueWhenOtherContainsAllElements(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + s2 := NewOrderedSetFromSlice([]string{"a", "b"}) + + assert.True(t, s1.IsSubset(s2), + "tools: expected [a, b] to be a subset of [a, b, c]") +} + +func TestOrderedSetIsSubsetReturnsFalseWhenOtherDoesNotContainAllElements(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b"}) + s2 := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + assert.False(t, s1.IsSubset(s2), + "tools: did not expect [a, b, c] to be a subset of [a, b]") +} + +func TestOrderedSetIsSupersetReturnsTrueWhenContainsAllElementsOfOther(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b"}) + s2 := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + assert.True(t, s1.IsSuperset(s2), + "tools: expected [a, b, c] to be a superset of [a, b]") +} + +func TestOrderedSetIsSupersetReturnsFalseWhenDoesNotContainAllElementsOfOther(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + s2 := NewOrderedSetFromSlice([]string{"a", "b"}) + + assert.False(t, s1.IsSuperset(s2), + "tools: did not expect [a, b] to be a superset of [a, b, c]") +} + +func TestOrderedSetUnion(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a"}) + s2 := NewOrderedSetFromSlice([]string{"b", "a"}) + + elems := make([]string, 0) + for e := range s1.Union(s2).Iter() { + elems = append(elems, e) + } + + require.Len(t, elems, 2) + assert.Equal(t, "a", elems[0]) + assert.Equal(t, "b", elems[1]) +} + +func TestOrderedSetIntersect(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a"}) + s2 := NewOrderedSetFromSlice([]string{"b", "a"}) + + elems := make([]string, 0) + for e := range s1.Intersect(s2).Iter() { + elems = append(elems, e) + } + + require.Len(t, elems, 1) + assert.Equal(t, "a", elems[0]) +} + +func TestOrderedSetDifference(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b"}) + s2 := NewOrderedSetFromSlice([]string{"a"}) + + elems := make([]string, 0) + for e := range s1.Difference(s2).Iter() { + elems = append(elems, e) + } + + require.Len(t, elems, 1) + assert.Equal(t, "b", elems[0]) +} + +func TestOrderedSetSymmetricDifference(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b"}) + s2 := NewOrderedSetFromSlice([]string{"b", "c"}) + + elems := make([]string, 0) + for e := range s1.SymmetricDifference(s2).Iter() { + elems = append(elems, e) + } + + require.Len(t, elems, 2) + assert.Equal(t, "a", elems[0]) + assert.Equal(t, "c", elems[1]) +} + +func TestOrderedSetClear(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b"}) + + assert.Equal(t, 2, s1.Cardinality()) + + s1.Clear() + + assert.Equal(t, 0, s1.Cardinality()) +} + +func TestOrderedSetRemove(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b"}) + + assert.True(t, s1.Contains("a"), "tools: expected [a, b] to contain 'a'") + assert.True(t, s1.Contains("b"), "tools: expected [a, b] to contain 'b'") + + s1.Remove("a") + + assert.False(t, s1.Contains("a"), "tools: did not expect to find 'a' in [b]") + assert.True(t, s1.Contains("b"), "tools: expected [b] to contain 'b'") +} + +func TestOrderedSetCardinality(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b"}) + + assert.Equal(t, 2, s1.Cardinality(), + "tools: expected cardinality of [a, b] to equal 2") +} + +func TestOrderedSetIter(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + elems := make([]string, 0) + for e := range s1.Iter() { + elems = append(elems, e) + } + + require.Len(t, elems, 3) + assert.Equal(t, "a", elems[0]) + assert.Equal(t, "b", elems[1]) + assert.Equal(t, "c", elems[2]) +} + +func TestOrderedSetEqualReturnsTrueWhenSameElementsInSameOrder(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + s2 := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + assert.True(t, s1.Equal(s2), + "tools: expected [a, b, c] to equal [a, b, c]") +} + +func TestOrderedSetEqualReturnsFalseWhenSameElementsInDifferentOrder(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + s2 := NewOrderedSetFromSlice([]string{"a", "c", "b"}) + + assert.False(t, s1.Equal(s2), + "tools: did not expect [a, b, c] to equal [a, c, b]") +} + +func TestOrderedSetEqualReturnsFalseWithDifferentCardinalities(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a"}) + s2 := NewOrderedSetFromSlice([]string{"a", "b"}) + + assert.False(t, s1.Equal(s2), + "tools: did not expect [a] to equal [a, b]") +} + +func TestOrderedSetClone(t *testing.T) { + s1 := NewOrderedSetFromSlice([]string{"a", "b", "c"}) + + s2 := s1.Clone() + + elems := make([]string, 0) + for e := range s2.Iter() { + elems = append(elems, e) + } + + require.Len(t, elems, 3) + assert.Equal(t, "a", elems[0]) + assert.Equal(t, "b", elems[1]) + assert.Equal(t, "c", elems[2]) +}