tools: introduce tools.OrderedSet

This commit is contained in:
Taylor Blau 2017-06-21 16:49:31 -06:00
parent 42661cdeed
commit cc351fcba5
2 changed files with 434 additions and 0 deletions

217
tools/ordered_set.go Normal file

@ -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
}

217
tools/ordered_set_test.go Normal file

@ -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])
}