1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-28 13:48:51 +08:00

Generalizing a tracker of attribute ranges.

This commit is contained in:
Jakub Sobon 2019-02-04 00:39:51 -05:00
parent f16bda1131
commit 4e705950f2
No known key found for this signature in database
GPG Key ID: F2451A77FB05D3B7
2 changed files with 212 additions and 0 deletions

94
attrrange/attrrange.go Normal file
View File

@ -0,0 +1,94 @@
// Package attrrange simplifies tracking of attributes that apply to a range of
// items.
// Refer to the examples if the test file for details on usage.
package attrrange
import (
"errors"
"fmt"
"sort"
)
// AttrRange is a range of items that share the same attributes.
type AttrRange struct {
// Low is the first position where these attributes apply.
Low int
// High is the end of the range. The attributes apply to all items in range
// Low <= b < high.
High int
// AttrIdx is the index of the attributes that apply to this range.
AttrIdx int
}
// newAttrRange returns a new AttrRange instance.
func newAttrRange(low, high, attrIdx int) *AttrRange {
return &AttrRange{
Low: low,
High: high,
AttrIdx: attrIdx,
}
}
// Tracker tracks attributes that apply to a range of items.
// This object is not thread safe.
type Tracker struct {
// ranges maps low indices of ranges to the attribute ranges.
ranges map[int]*AttrRange
}
// NewTracker returns a new tracker of ranges that share the same attributes.
func NewTracker() *Tracker {
return &Tracker{
ranges: map[int]*AttrRange{},
}
}
// Add adds a new range of items that share attributes with the specified
// index.
// The low position of the range must not overlap with low position of any
// existing range.
func (t *Tracker) Add(low, high, attrIdx int) error {
ar := newAttrRange(low, high, attrIdx)
if ar, ok := t.ranges[low]; ok {
return fmt.Errorf("already have range starting on low:%d, existing:%+v", low, ar)
}
t.ranges[low] = ar
return nil
}
// ErrNotFound indicates that the requested position wasn't found in any of the
// known ranges.
var ErrNotFound = errors.New("range not found")
// ForPosition returns attribute index that apply to the specified position.
// Returns ErrNotFound when the requested position wasn't found in any of the
// known ranges.
func (t *Tracker) ForPosition(pos int) (*AttrRange, error) {
if ar, ok := t.ranges[pos]; ok {
return ar, nil
}
var keys []int
for k := range t.ranges {
keys = append(keys, k)
}
sort.Ints(keys)
var res *AttrRange
for _, k := range keys {
ar := t.ranges[k]
if ar.Low > pos {
break
}
if ar.High > pos {
res = ar
}
}
if res == nil {
return nil, ErrNotFound
}
return res, nil
}

118
attrrange/attrrange_test.go Normal file
View File

@ -0,0 +1,118 @@
package attrrange
import (
"testing"
"github.com/kylelemons/godebug/pretty"
)
func TestForPosition(t *testing.T) {
tests := []struct {
desc string
// if not nil, called before calling ForPosition.
// Can add ranges.
update func(*Tracker) error
pos int
want *AttrRange
wantErr error
wantUpdateErr bool
}{
{
desc: "fails when no ranges given",
pos: 0,
wantErr: ErrNotFound,
},
{
desc: "fails to add a duplicate",
update: func(tr *Tracker) error {
if err := tr.Add(2, 5, 40); err != nil {
return err
}
return tr.Add(2, 3, 41)
},
wantUpdateErr: true,
},
{
desc: "fails when multiple given ranges, position falls before them",
update: func(tr *Tracker) error {
if err := tr.Add(2, 5, 40); err != nil {
return err
}
return tr.Add(5, 10, 41)
},
pos: 1,
wantErr: ErrNotFound,
},
{
desc: "multiple given options, position falls on the lower",
update: func(tr *Tracker) error {
if err := tr.Add(2, 5, 40); err != nil {
return err
}
return tr.Add(5, 10, 41)
},
pos: 2,
want: newAttrRange(2, 5, 40),
},
{
desc: "multiple given options, position falls between them",
update: func(tr *Tracker) error {
if err := tr.Add(2, 5, 40); err != nil {
return err
}
return tr.Add(5, 10, 41)
},
pos: 4,
want: newAttrRange(2, 5, 40),
},
{
desc: "multiple given options, position falls on the higher",
update: func(tr *Tracker) error {
if err := tr.Add(2, 5, 40); err != nil {
return err
}
return tr.Add(5, 10, 41)
},
pos: 5,
want: newAttrRange(5, 10, 41),
},
{
desc: "multiple given options, position falls after them",
update: func(tr *Tracker) error {
if err := tr.Add(2, 5, 40); err != nil {
return err
}
return tr.Add(5, 10, 41)
},
pos: 10,
wantErr: ErrNotFound,
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
tr := NewTracker()
if tc.update != nil {
err := tc.update(tr)
if (err != nil) != tc.wantUpdateErr {
t.Errorf("tc.update => unexpected error:%v, wantUpdateErr:%v", err, tc.wantUpdateErr)
}
if err != nil {
return
}
}
got, err := tr.ForPosition(tc.pos)
if err != tc.wantErr {
t.Errorf("ForPosition => unexpected error:%v, wantErr:%v", err, tc.wantErr)
}
if err != nil {
return
}
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("ForPosition => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}