Add CI workflow and update documentation for KDTree version

This commit is contained in:
Snider 2025-11-03 18:10:49 +00:00
parent 43c37f900f
commit ec61d6afde
7 changed files with 119 additions and 8 deletions

42
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: CI
on:
push:
branches: [ main, master ]
pull_request:
jobs:
build-test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.22.x', '1.23.x' ]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Verify go.mod is tidy
run: |
go mod tidy
git diff --exit-code -- go.mod go.sum
- name: Build
run: go build ./...
- name: Test (race)
run: go test -race ./...
- name: Vet
run: go vet ./...
- name: Govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-input: ${{ matrix.go-version }}
# Run against packages in the module
args: ./...

4
doc.go Normal file
View file

@ -0,0 +1,4 @@
// Package poindexter provides sorting utilities and a KDTree with simple
// nearest-neighbour queries. It also includes helper functions to build
// normalised, weighted KD points for 2D/3D/4D use-cases.
package poindexter

View file

@ -13,13 +13,13 @@ func Version() string
Returns the current version of the library.
**Returns:**
- `string`: The version string (e.g., "0.1.0")
- `string`: The version string (e.g., "0.2.0")
**Example:**
```go
version := poindexter.Version()
fmt.Println(version) // Output: 0.1.0
fmt.Println(version) // Output: 0.2.0
```
---

36
examples_test.go Normal file
View file

@ -0,0 +1,36 @@
package poindexter_test
import (
"fmt"
poindexter "github.com/Snider/Poindexter"
)
func ExampleNewKDTree() {
pts := []poindexter.KDPoint[string]{
{ID: "A", Coords: []float64{0, 0}, Value: "alpha"},
{ID: "B", Coords: []float64{1, 0}, Value: "bravo"},
}
tr, _ := poindexter.NewKDTree(pts)
p, _, _ := tr.Nearest([]float64{0.2, 0})
fmt.Println(p.ID)
// Output: A
}
func ExampleBuild2D() {
type rec struct{ ping, hops float64 }
items := []rec{{ping: 20, hops: 3}, {ping: 30, hops: 2}, {ping: 15, hops: 4}}
weights := [2]float64{1.0, 1.0}
invert := [2]bool{false, false}
pts, _ := poindexter.Build2D(items,
func(r rec) string { return "" },
func(r rec) float64 { return r.ping },
func(r rec) float64 { return r.hops },
weights, invert,
)
tr, _ := poindexter.NewKDTree(pts, poindexter.WithMetric(poindexter.ManhattanDistance{}))
_, _, _ = tr.Nearest([]float64{0, 0})
// Querying the origin (0,0) in normalized space tends to favor minima on each axis.
fmt.Printf("dim=%d len=%d", tr.Dim(), tr.Len())
// Output: dim=2 len=3
}

2
go.mod
View file

@ -1,3 +1,3 @@
module github.com/Snider/Poindexter
go 1.24.9
go 1.23

View file

@ -6,6 +6,17 @@ import (
"sort"
)
var (
// ErrEmptyPoints indicates that no points were provided to build a KDTree.
ErrEmptyPoints = errors.New("kdtree: no points provided")
// ErrZeroDim indicates that points or tree dimension must be at least 1.
ErrZeroDim = errors.New("kdtree: points must have at least one dimension")
// ErrDimMismatch indicates inconsistent dimensionality among points.
ErrDimMismatch = errors.New("kdtree: inconsistent dimensionality in points")
// ErrDuplicateID indicates a duplicate point ID was encountered.
ErrDuplicateID = errors.New("kdtree: duplicate point ID")
)
// KDPoint represents a point with coordinates and an attached payload/value.
// ID should be unique within a tree to enable O(1) deletes by ID.
// Coords must all have the same dimensionality within a given KDTree.
@ -89,20 +100,20 @@ type KDTree[T any] struct {
// All points must have the same dimensionality (>0).
func NewKDTree[T any](pts []KDPoint[T], opts ...KDOption) (*KDTree[T], error) {
if len(pts) == 0 {
return nil, errors.New("no points provided")
return nil, ErrEmptyPoints
}
dim := len(pts[0].Coords)
if dim == 0 {
return nil, errors.New("points must have at least one dimension")
return nil, ErrZeroDim
}
idIndex := make(map[string]int, len(pts))
for i, p := range pts {
if len(p.Coords) != dim {
return nil, errors.New("inconsistent dimensionality in points")
return nil, ErrDimMismatch
}
if p.ID != "" {
if _, exists := idIndex[p.ID]; exists {
return nil, errors.New("duplicate point ID: " + p.ID)
return nil, ErrDuplicateID
}
idIndex[p.ID] = i
}
@ -120,6 +131,24 @@ func NewKDTree[T any](pts []KDPoint[T], opts ...KDOption) (*KDTree[T], error) {
return t, nil
}
// NewKDTreeFromDim constructs an empty KDTree with the specified dimension.
// Call Insert to add points after construction.
func NewKDTreeFromDim[T any](dim int, opts ...KDOption) (*KDTree[T], error) {
if dim <= 0 {
return nil, ErrZeroDim
}
cfg := kdOptions{metric: EuclideanDistance{}}
for _, o := range opts {
o(&cfg)
}
return &KDTree[T]{
points: nil,
dim: dim,
metric: cfg.metric,
idIndex: make(map[string]int),
}, nil
}
// Dim returns the number of dimensions.
func (t *KDTree[T]) Dim() int { return t.dim }

View file

@ -39,7 +39,7 @@ func SortBy[T any](data []T, less func(i, j int) bool) {
}
// SortByKey sorts a slice by extracting a comparable key from each element.
// The key function should return a value that implements constraints.Ordered.
// K is restricted to int, float64, or string.
func SortByKey[T any, K int | float64 | string](data []T, key func(T) K) {
sort.Slice(data, func(i, j int) bool {
return key(data[i]) < key(data[j])