diff --git a/spatial-index/codepoint.go b/spatial-index/codepoint.go index a763868..d66ee50 100644 --- a/spatial-index/codepoint.go +++ b/spatial-index/codepoint.go @@ -20,6 +20,19 @@ type SpatialIndex struct { tree *rtree.RTreeGN[uint32, string] } +func NewCodePointSpatialIndex(zipFile string) (*SpatialIndex, error) { + idx := SpatialIndex{ + tree: &rtree.RTreeGN[uint32, string]{}, + } + + err := idx.importCodePoint(zipFile) + if err != nil { + return nil, fmt.Errorf("failed to load codepoint index from zip file: %w", err) + } + + return &idx, nil +} + func (idx *SpatialIndex) Search(bounds []uint32) (*[]CodePoint, error) { results := make([]CodePoint, 0, 100) @@ -51,19 +64,6 @@ func (idx *SpatialIndex) SearchIter(bounds []uint32, iter func(min, max [2]uint3 return nil } -func NewCodePointSpatialIndex(zipFile string) (*SpatialIndex, error) { - idx := SpatialIndex{ - tree: &rtree.RTreeGN[uint32, string]{}, - } - - err := idx.importCodePoint(zipFile) - if err != nil { - return nil, fmt.Errorf("failed to load codepoint index from zip file: %w", err) - } - - return &idx, nil -} - func (idx *SpatialIndex) Len() int { return idx.tree.Len() } diff --git a/spatial-index/codepoint_test.go b/spatial-index/codepoint_test.go new file mode 100644 index 0000000..3806651 --- /dev/null +++ b/spatial-index/codepoint_test.go @@ -0,0 +1,103 @@ +package spatialindex + +import ( + "archive/zip" + "os" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/rtree" +) + +func createTestZip(t *testing.T, files map[string]string) string { + t.Helper() + tmpfile, err := os.CreateTemp("", "test-*.zip") + require.NoError(t, err, "failed to create temp file") + defer func() { _ = tmpfile.Close() }() + + w := zip.NewWriter(tmpfile) + for name, content := range files { + f, err := w.Create(name) + require.NoError(t, err, "failed to create file in zip") + _, err = f.Write([]byte(content)) + require.NoError(t, err, "failed to write file in zip") + } + _ = w.Close() + return tmpfile.Name() +} + +func TestNewCodePointSpatialIndex_Success(t *testing.T) { + csv := "PC1,PC2,123,456\nPC3,PC4,789,1011\n" + zipPath := createTestZip(t, map[string]string{"Data/CSV/test.csv": csv}) + defer func() { _ = os.Remove(zipPath) }() + + idx, err := NewCodePointSpatialIndex(zipPath) + require.NoError(t, err) + require.Equal(t, 2, idx.Len()) +} + +func TestNewCodePointSpatialIndex_BadZip(t *testing.T) { + _, err := NewCodePointSpatialIndex("/no/such/file.zip") + require.Error(t, err, "failed to open zip file: file does not exist") +} + +func TestSearch(t *testing.T) { + csv := "PC1,PC2,100,200\nPC2,PC3,300,400\n" + zipPath := createTestZip(t, map[string]string{"Data/CSV/test.csv": csv}) + defer func() { _ = os.Remove(zipPath) }() + idx, err := NewCodePointSpatialIndex(zipPath) + require.NoError(t, err) + + // Search for both + res, err := idx.Search([]uint32{0, 0, 500, 500}) + require.NoError(t, err) + require.Equal(t, 2, len(*res)) + + // Search for one + res, err = idx.Search([]uint32{90, 190, 110, 210}) + require.NoError(t, err) + require.Equal(t, 1, len(*res)) + require.Equal(t, "PC1", (*res)[0].PostCode) + + // Search for none + res, err = idx.Search([]uint32{1000, 1000, 2000, 2000}) + require.NoError(t, err) + require.Equal(t, 0, len(*res)) +} + +func TestSearchIter_InvalidBounds(t *testing.T) { + idx := &SpatialIndex{tree: &rtree.RTreeGN[uint32, string]{}} + err := idx.SearchIter([]uint32{1, 2, 3}, func([2]uint32, [2]uint32, string) bool { return true }) + require.Error(t, err) + require.Contains(t, err.Error(), "bounds must contain exactly 4 values") +} + +func TestLen(t *testing.T) { + csv := "PC1,PC2,1,2\nPC2,PC3,3,4\nPC3,PC4,5,6\n" + zipPath := createTestZip(t, map[string]string{"Data/CSV/test.csv": csv}) + defer func() { _ = os.Remove(zipPath) }() + idx, err := NewCodePointSpatialIndex(zipPath) + require.NoError(t, err) + require.Equal(t, 3, idx.Len()) +} + +func Test_fromCodePointCSV(t *testing.T) { + rec := []string{"PC1", "PC2", "123", "456"} + cp, err := fromCodePointCSV(rec, nil) + require.NoError(t, err) + require.Equal(t, "PC1", cp.PostCode) + require.Equal(t, uint32(123), cp.Easting) + require.Equal(t, uint32(456), cp.Northing) + + // Bad easting + rec = []string{"PC1", "PC2", "bad", "456"} + _, err = fromCodePointCSV(rec, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "easting") + + // Bad northing + rec = []string{"PC1", "PC2", "123", "bad"} + _, err = fromCodePointCSV(rec, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "northing") +}