diff --git a/api/label/iterator.go b/api/label/iterator.go index 6b345d092..ca852d500 100644 --- a/api/label/iterator.go +++ b/api/label/iterator.go @@ -25,6 +25,21 @@ type Iterator struct { idx int } +// MergeIterator supports iterating over two sets of labels while +// eliminating duplicate values from the combined set. The first +// iterator value takes precedence. +type MergeItererator struct { + one oneIterator + two oneIterator + current kv.KeyValue +} + +type oneIterator struct { + iter Iterator + done bool + label kv.KeyValue +} + // Next moves the iterator to the next position. Returns false if there // are no more labels. func (i *Iterator) Next() bool { @@ -75,3 +90,64 @@ func (i *Iterator) ToSlice() []kv.KeyValue { } return slice } + +// NewMergeIterator returns a MergeIterator for merging two label set +// iterators. Duplicates are resolved by taking the value from the +// first iterator. +func NewMergeIterator(iter1, iter2 Iterator) MergeItererator { + mi := MergeItererator{ + one: makeOne(iter1), + two: makeOne(iter2), + } + return mi +} + +func makeOne(iter Iterator) oneIterator { + oi := oneIterator{ + iter: iter, + } + oi.advance() + return oi +} + +func (oi *oneIterator) advance() { + if oi.done = !oi.iter.Next(); !oi.done { + oi.label = oi.iter.Label() + } +} + +// Next returns true if there is another label available. +func (m *MergeItererator) Next() bool { + if m.one.done && m.two.done { + return false + } + if m.one.done { + m.current = m.two.label + m.two.advance() + return true + } + if m.two.done { + m.current = m.one.label + m.one.advance() + return true + } + if m.one.label.Key == m.two.label.Key { + m.current = m.one.label // first iterator label value wins + m.one.advance() + m.two.advance() + return true + } + if m.one.label.Key < m.two.label.Key { + m.current = m.one.label + m.one.advance() + return true + } + m.current = m.two.label + m.two.advance() + return true +} + +// Label returns the current value after Next() returns true. +func (m *MergeItererator) Label() kv.KeyValue { + return m.current +} diff --git a/api/label/iterator_test.go b/api/label/iterator_test.go index 0548240f1..d0f7feece 100644 --- a/api/label/iterator_test.go +++ b/api/label/iterator_test.go @@ -15,6 +15,7 @@ package label_test import ( + "fmt" "testing" "go.opentelemetry.io/otel/api/kv" @@ -55,3 +56,96 @@ func TestEmptyIterator(t *testing.T) { require.Equal(t, 0, iter.Len()) require.False(t, iter.Next()) } + +func TestMergedIterator(t *testing.T) { + + type inputs struct { + name string + keys1 []string + keys2 []string + expect []string + } + + makeLabels := func(keys []string, num int) (result []kv.KeyValue) { + for _, k := range keys { + result = append(result, kv.Int(k, num)) + } + return + } + + for _, input := range []inputs{ + { + name: "one overlap", + keys1: []string{"A", "B"}, + keys2: []string{"B", "C"}, + expect: []string{"A/1", "B/1", "C/2"}, + }, + { + name: "reversed one overlap", + keys1: []string{"B", "A"}, + keys2: []string{"C", "B"}, + expect: []string{"A/1", "B/1", "C/2"}, + }, + { + name: "one empty", + keys1: nil, + keys2: []string{"C", "B"}, + expect: []string{"B/2", "C/2"}, + }, + { + name: "two empty", + keys1: []string{"C", "B"}, + keys2: nil, + expect: []string{"B/1", "C/1"}, + }, + { + name: "no overlap both", + keys1: []string{"C"}, + keys2: []string{"B"}, + expect: []string{"B/2", "C/1"}, + }, + { + name: "one empty single two", + keys1: nil, + keys2: []string{"B"}, + expect: []string{"B/2"}, + }, + { + name: "two empty single one", + keys1: []string{"A"}, + keys2: nil, + expect: []string{"A/1"}, + }, + { + name: "all empty", + keys1: nil, + keys2: nil, + expect: nil, + }, + { + name: "full overlap", + keys1: []string{"A", "B", "C", "D"}, + keys2: []string{"A", "B", "C", "D"}, + expect: []string{"A/1", "B/1", "C/1", "D/1"}, + }, + } { + t.Run(input.name, func(t *testing.T) { + labels1 := makeLabels(input.keys1, 1) + labels2 := makeLabels(input.keys2, 2) + + set1 := label.NewSet(labels1...) + set2 := label.NewSet(labels2...) + + merge := label.NewMergeIterator(set1.Iter(), set2.Iter()) + + var result []string + + for merge.Next() { + label := merge.Label() + result = append(result, fmt.Sprint(label.Key, "/", label.Value.Emit())) + } + + require.Equal(t, input.expect, result) + }) + } +}