Skip to content

Commit

Permalink
chore(bst): 100% test coverage.
Browse files Browse the repository at this point in the history
  • Loading branch information
fgm committed May 14, 2022
1 parent f49a1d9 commit 966639c
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 89 deletions.
63 changes: 0 additions & 63 deletions binarysearchtree/bst_opaque_test.go

This file was deleted.

59 changes: 37 additions & 22 deletions binarysearchtree/bst.go → binarysearchtree/intrinsic.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package binarysearchtree

import (
"golang.org/x/exp/constraints"
)

type walkCB[E constraints.Ordered] func(*E)
"github.com/fgm/container"
)

type node[E constraints.Ordered] struct {
data *E
Expand Down Expand Up @@ -68,7 +68,7 @@ func (n *node[E]) upsert(m *node[E]) *E {
}
}

func (n *node[E]) walkInOrder(cb walkCB[E]) {
func (n *node[E]) walkInOrder(cb container.BinarySearchTreeWalkCB[E]) {
if n == nil {
return
}
Expand All @@ -81,7 +81,7 @@ func (n *node[E]) walkInOrder(cb walkCB[E]) {
}
}

func (n *node[E]) walkPostOrder(cb walkCB[E]) {
func (n *node[E]) walkPostOrder(cb container.BinarySearchTreeWalkCB[E]) {
if n == nil {
return
}
Expand All @@ -94,7 +94,7 @@ func (n *node[E]) walkPostOrder(cb walkCB[E]) {
cb(n.data)
}

func (n *node[E]) walkPreOrder(cb walkCB[E]) {
func (n *node[E]) walkPreOrder(cb container.BinarySearchTreeWalkCB[E]) {
if n == nil {
return
}
Expand All @@ -107,43 +107,58 @@ func (n *node[E]) walkPreOrder(cb walkCB[E]) {
}
}

type Tree[E constraints.Ordered] struct {
// Intrinsic holds nodes which are their own ordering key.
type Intrinsic[E constraints.Ordered] struct {
root *node[E]
}

// Len returns the number of nodes in the tree, for the container.Countable interface.
// Complexity is O(n).
func (t *Intrinsic[E]) Len() int {
l := 0
t.WalkPostOrder(func(_ *E) { l++ })
return l
}

func (t *Intrinsic[E]) Elements() []E {
var sl []E
t.WalkPreOrder(func(e *E) { sl = append(sl, *e) })
return sl
}

// Upsert adds a value to the tree, replacing and returning the previous one if any.
// If none existed, it returns nil.
func (t *Tree[E]) Upsert(e ...*E) []*E {
res := make([]*E, 0, len(e))
func (t *Intrinsic[E]) Upsert(e ...*E) []*E {
results := make([]*E, 0, len(e))
var result *E
for _, oneE := range e {
n := &node[E]{data: oneE}

switch {
case t == nil, e == nil:
res = append(res, nil)
result = nil
case t.root == nil:
t.root = n
res = append(res, nil)
result = nil
default:
res = append(res, t.root.upsert(n))
result = t.root.upsert(n)
}
results = append(results, result)
}
return res
return results
}

func (t *Tree[E]) Delete(e *E) {
func (t *Intrinsic[E]) Delete(e *E) {
if t == nil || e == nil {
return
}
t.root.delete(e)
// two children: promote the leftmost child of the right tree as the root.
// If it had a right child (can't have a left child since it is rightmost), promote it.
}

// IndexOf returns the position of the value among those in the tree.
// If the value cannot be found, it will return 0, false, otherwise the position
// starting at 0, and true.
func (t *Tree[E]) IndexOf(e *E) (int, bool) {
func (t *Intrinsic[E]) IndexOf(e *E) (int, bool) {
index, found := 0, false
t.WalkInOrder(func(x *E) {
if *x == *e {
Expand All @@ -160,31 +175,31 @@ func (t *Tree[E]) IndexOf(e *E) (int, bool) {
}

// WalkInOrder in useful for search and listing nodes in order.
func (t *Tree[E]) WalkInOrder(cb walkCB[E]) {
func (t *Intrinsic[E]) WalkInOrder(cb container.BinarySearchTreeWalkCB[E]) {
if t == nil {
return
}
t.root.walkInOrder(cb)
}

// WalkPostOrder in useful for deleting subtrees.
func (t *Tree[E]) WalkPostOrder(fn func(e *E)) {
func (t *Intrinsic[E]) WalkPostOrder(cb container.BinarySearchTreeWalkCB[E]) {
if t == nil {
return
}
t.root.walkPostOrder(fn)
t.root.walkPostOrder(cb)
}

// WalkPreOrder is useful to clone the tree.
func (t *Tree[E]) WalkPreOrder(cb walkCB[E]) {
func (t *Intrinsic[E]) WalkPreOrder(cb container.BinarySearchTreeWalkCB[E]) {
if t == nil {
return
}
t.root.walkPreOrder(cb)
}

func (t *Tree[E]) Clone() *Tree[E] {
clone := &Tree[E]{}
func (t *Intrinsic[E]) Clone() container.BinarySearchTree[E] {
clone := &Intrinsic[E]{}
t.WalkPreOrder(func(e *E) {
clone.Upsert(e)
})
Expand Down
109 changes: 109 additions & 0 deletions binarysearchtree/intrinsic_opaque_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package binarysearchtree_test

import (
"strconv"
"testing"

bst "github.com/fgm/container/binarysearchtree"
)

func TestIntrinsic_nil(t *testing.T) {
var tree *bst.Intrinsic[int]
tree.WalkInOrder(bst.P)
tree.WalkPostOrder(bst.P)
tree.WalkPreOrder(bst.P)
tree.Upsert(nil)
tree.Delete(nil)

tree = &bst.Intrinsic[int]{}
tree.WalkInOrder(bst.P)
tree.WalkPostOrder(bst.P)
tree.WalkPreOrder(bst.P)
// Output:
}

func TestIntrinsic_Upsert(t *testing.T) {
tree := bst.Simple()
actual := tree.Upsert(&bst.One)
if len(actual) != 1 {
t.Fatalf("expected overwriting upsert to return one value, got %v", actual)
}
if *actual[0] != bst.One {
t.Fatalf("expected overwriting upsert to return %d, got %d", bst.One, *actual[0])
}

actual = tree.Upsert(&bst.Six)
if len(actual) != 1 {
t.Fatalf("expected non-overwriting upsert to return one value, got %v", actual)
}
if actual[0] != nil {
t.Fatalf("expected non-overwriting upsert to return one nil, got %v", actual[0])
}
}

func TestIntrinsic_IndexOf(t *testing.T) {
tree := bst.Simple()
checks := [...]struct {
input int
expectedOK bool
expectedIndex int
}{
{bst.One, true, 0},
{bst.Two, true, 1},
{bst.Three, true, 2},
{bst.Four, true, 3},
{bst.Five, true, 4},
{bst.Six, false, 0},
}
for _, check := range checks {
t.Run(strconv.Itoa(check.input), func(t *testing.T) {
actualIndex, actualOK := tree.IndexOf(&check.input)
if actualOK != check.expectedOK {
t.Fatalf("%d found: %t but expected %t", check.input, actualOK, check.expectedOK)
}
if actualIndex != check.expectedIndex {
t.Fatalf("%d at index %d but expected %d", check.input, actualIndex, check.expectedIndex)
}
})
}
}

func TestIntrinsic_Len(t *testing.T) {
si := bst.Simple().Elements()
hf := bst.HalfFull().Elements()

checks := [...]struct {
name string
input []int
deletions []int
expected int
}{
{"empty", nil, nil, 0},
{"simple", si, nil, 5},
{"half-full", hf, nil, 6},
{"overwrite element", append(si, bst.Three), nil, 5},
{"delete nonexistent", si, []int{bst.Six}, 5},
{"delete existing childless", si, []int{bst.One}, 4},
{"delete existing with 1 left child", si, []int{bst.Two}, 4},
{"delete existing with 1 right child", si, []int{bst.Four}, 4},
{"delete existing with 2 children", hf, []int{bst.Three}, 5},
}
for _, check := range checks {
t.Run(check.name, func(t *testing.T) {
tree := bst.Intrinsic[int]{}
// In these loops, e is always the same variable: without cloning,
// each iteration reuses the same pointer, overwriting the tree.
for _, e := range check.input {
clone := e
tree.Upsert(&clone)
}
for _, e := range check.deletions {
clone := e
tree.Delete(&clone)
}
if tree.Len() != check.expected {
t.Fatalf("Found len %d, but expected %d", tree.Len(), check.expected)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ var (
// 2 4
// / \
// 1 5
func Simple() *Tree[int] {
simple := Tree[int]{}
func Simple() *Intrinsic[int] {
simple := Intrinsic[int]{}
simple.Upsert(&Three, &Two, &Four, &One, &Five)
return &simple
}
Expand All @@ -27,8 +27,8 @@ func Simple() *Tree[int] {
// 2 5
// / / \
// 1 4 6
func HalfFull() *Tree[int] {
hf := Tree[int]{}
func HalfFull() *Intrinsic[int] {
hf := Intrinsic[int]{}
hf.Upsert(&Three, &Two, &Five, &One, &Four, &Six)
return &hf
}
Expand Down

0 comments on commit 966639c

Please sign in to comment.