diff --git a/gnetdag/doc.go b/gnetdag/doc.go new file mode 100644 index 0000000..f88ed37 --- /dev/null +++ b/gnetdag/doc.go @@ -0,0 +1,14 @@ +// Package gnetdag (Gordian NETwork Directed Acyclic Graph) +// contains types for determining directional flow of network traffic. +// +// Types in this package are focused on int values, +// so that they remain decoupled from any concrete implementations +// of validators, network addresses, and so on. +// Callers may simply use the int values as indices into slices +// of the actual type needing the directed graph. +// +// This package currently contains the [FixedTree] type, +// which effectively maps indices in a slice such that +// every non-root node contains a fixed number of children. +// This package will be expanded with more types as deemed necessary. +package gnetdag diff --git a/gnetdag/fixedtree.go b/gnetdag/fixedtree.go new file mode 100644 index 0000000..533e21a --- /dev/null +++ b/gnetdag/fixedtree.go @@ -0,0 +1,100 @@ +package gnetdag + +// FixedTree represents a tree where each non-leaf node has a fixed number of children. +// With BranchFactor=3, the entries are arranged in layers like: +// +// 0 (L0) +// 1 2 3 (L1) +// 4 5 6 7 8 9 10 11 12 (L2) +// +// The entryIdx parameter used in methods on FixedTree +// are intended to be used as indices into an existing ordered slice +// that is to be treated as a tree. +// +// Methods on FixedTree use unchecked math, +// so invalid values, such as negative entry indices or branch factors, +// or branch factor so large that bf^2 overflows an int, +// result in undefined behavior. +type FixedTree struct { + // The width of the layer at index 1. + BranchFactor int +} + +// Parent returns the "parent" index of the given entry index. +// It returns -1 for entryIdx = 0. +func (t FixedTree) Parent(entryIdx int) int { + if entryIdx == 0 { + return -1 + } + + // Special case for first layer, to avoid some off by one math. + if entryIdx <= t.BranchFactor { + return 0 + } + + curLayer := t.Layer(entryIdx) + + // Calculate how many entries are present before the parent layer. + parentLayer := curLayer - 1 + ancestorEntries := 1 + ancestorWidth := 1 + for range parentLayer - 1 { + ancestorWidth *= t.BranchFactor + ancestorEntries += ancestorWidth + } + + // Our current row has t.BranchFactor times more entries than the parent row, + // so map our offset in the current row, into the offset of the parent row. + parentLayerWidth := ancestorWidth * t.BranchFactor + parentOffset := (entryIdx - parentLayerWidth - ancestorEntries) / t.BranchFactor + return ancestorEntries + parentOffset +} + +// FirstChild returns the entry index of the first child of the given entry index. +// Every parent contains t.BranchFactor children, +// but the FixedTree type does not track number of entries, +// so it is the caller's responsibility to confirm that there are +// at least t.BranchFactor children available. +func (t FixedTree) FirstChild(entryIdx int) int { + if entryIdx == 0 { + return 1 + } + + curLayerWidth := t.BranchFactor + entriesBeforeCurLayer := 1 + + for { + if entryIdx <= entriesBeforeCurLayer+curLayerWidth { + // Offset of the given entry index, within its respective layer. + entryLayerOffset := entryIdx - entriesBeforeCurLayer + + // Then find the start of the next layer, + // and move forward t.BranchFactor times according to the current layer offset. + return entriesBeforeCurLayer + curLayerWidth + (entryLayerOffset * t.BranchFactor) + } + + entriesBeforeCurLayer += curLayerWidth + curLayerWidth *= t.BranchFactor + } +} + +// Layer returns the layer that would contain the given entry index. +func (t FixedTree) Layer(entryIdx int) int { + if entryIdx == 0 { + return 0 + } + + layer := 1 + layerWidth := t.BranchFactor + entriesSoFar := 1 + t.BranchFactor + + for { + if entryIdx < entriesSoFar { + return layer + } + + layer++ + layerWidth *= t.BranchFactor + entriesSoFar += layerWidth + } +} diff --git a/gnetdag/fixedtree_test.go b/gnetdag/fixedtree_test.go new file mode 100644 index 0000000..0df9f0d --- /dev/null +++ b/gnetdag/fixedtree_test.go @@ -0,0 +1,65 @@ +package gnetdag_test + +import ( + "testing" + + "github.com/gordian-engine/gordian/gnetdag" + "github.com/stretchr/testify/require" +) + +// Most of these tests use a branch factor of 3, +// resulting in layers like: +// 0 (L0) +// 1 2 3 (L1) +// 4 5 6 7 8 9 10 11 12 (L2) +// 13 14 15 16... (L3) + +func TestFixedTree_Layer(t *testing.T) { + t.Parallel() + + tree := gnetdag.FixedTree{BranchFactor: 3} + require.Equal(t, 0, tree.Layer(0)) + require.Equal(t, 1, tree.Layer(1)) + require.Equal(t, 2, tree.Layer(4)) + + tree.BranchFactor = 5 + require.Equal(t, 0, tree.Layer(0)) + require.Equal(t, 1, tree.Layer(4)) +} + +func TestFixedTree_Parent(t *testing.T) { + t.Parallel() + + tree := gnetdag.FixedTree{BranchFactor: 3} + require.Equal(t, -1, tree.Parent(0)) + + require.Equal(t, 0, tree.Parent(1)) + require.Equal(t, 0, tree.Parent(2)) + require.Equal(t, 0, tree.Parent(3)) + + require.Equal(t, 1, tree.Parent(4)) + require.Equal(t, 1, tree.Parent(5)) + require.Equal(t, 1, tree.Parent(6)) + require.Equal(t, 2, tree.Parent(7)) + require.Equal(t, 2, tree.Parent(8)) + require.Equal(t, 2, tree.Parent(9)) + require.Equal(t, 3, tree.Parent(10)) + require.Equal(t, 3, tree.Parent(11)) + require.Equal(t, 3, tree.Parent(12)) + + require.Equal(t, 4, tree.Parent(13)) +} + +func TestFixedTree_FirstChild(t *testing.T) { + t.Parallel() + + tree := gnetdag.FixedTree{BranchFactor: 3} + + require.Equal(t, 1, tree.FirstChild(0)) + + require.Equal(t, 4, tree.FirstChild(1)) + require.Equal(t, 7, tree.FirstChild(2)) + require.Equal(t, 10, tree.FirstChild(3)) + + require.Equal(t, 13, tree.FirstChild(4)) +}