diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36335b2..c852120 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ["1.20"] + go-version: ["1.21"] steps: - uses: actions/setup-go@v5 @@ -29,9 +29,9 @@ jobs: strategy: matrix: go-version: - - "1.20" - "1.21" - "1.22" + - "1.23" steps: - uses: actions/setup-go@v5 diff --git a/.golangci.yml b/.golangci.yml index 1a59fb5..11b8b96 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,8 @@ linters-settings: errcheck: check-type-assertions: true + exhaustive: + default-signifies-exhaustive: true goconst: min-len: 2 min-occurrences: 3 diff --git a/bench_test.go b/bench_test.go index faccfa7..91de0bf 100644 --- a/bench_test.go +++ b/bench_test.go @@ -10,7 +10,7 @@ var ( Lat: 37, Lng: -122, } - cell = LatLngToCell(geo, 15) + cell, _ = LatLngToCell(geo, 15) addr = cell.String() geoBndry CellBoundary cells []Cell @@ -31,30 +31,30 @@ func BenchmarkFromString(b *testing.B) { func BenchmarkToGeoRes15(b *testing.B) { for n := 0; n < b.N; n++ { - geo = CellToLatLng(cell) + geo, _ = CellToLatLng(cell) } } func BenchmarkFromGeoRes15(b *testing.B) { for n := 0; n < b.N; n++ { - cell = LatLngToCell(geo, 15) + cell, _ = LatLngToCell(geo, 15) } } func BenchmarkToGeoBndryRes15(b *testing.B) { for n := 0; n < b.N; n++ { - geoBndry = CellToBoundary(cell) + geoBndry, _ = CellToBoundary(cell) } } func BenchmarkHexRange(b *testing.B) { for n := 0; n < b.N; n++ { - cells = cell.GridDisk(10) + cells, _ = cell.GridDisk(10) } } func BenchmarkPolyfill(b *testing.B) { for n := 0; n < b.N; n++ { - cells = PolygonToCells(validGeoPolygonHoles, 15) + cells, _ = PolygonToCells(validGeoPolygonHoles, 15) } } diff --git a/example_test.go b/example_test.go index 5450d3c..0a73fbf 100644 --- a/example_test.go +++ b/example_test.go @@ -9,7 +9,12 @@ import ( func ExampleLatLngToCell() { latLng := h3.NewLatLng(37.775938728915946, -122.41795063018799) resolution := 9 - c := h3.LatLngToCell(latLng, resolution) + + c, err := h3.LatLngToCell(latLng, resolution) + if err != nil { + panic(err) + } + fmt.Printf("%s", c) // Output: // 8928308280fffff diff --git a/go.mod b/go.mod index 0fd5b13..58320d4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/uber/h3-go/v4 -go 1.20 +go 1.21 diff --git a/h3.go b/h3.go index abaafbd..b04e4cd 100644 --- a/h3.go +++ b/h3.go @@ -68,6 +68,46 @@ const ( RadsToDegs = 180.0 / math.Pi ) +// Error codes. +var ( + ErrFailed = errors.New("the operation failed") + ErrDomain = errors.New("argument was outside of acceptable range") + ErrLatLngDomain = errors.New("latitude or longitude arguments were outside of acceptable range") + ErrResolutionDomain = errors.New("resolution argument was outside of acceptable range") + ErrCellInvalid = errors.New("H3Index cell argument was not valid") + ErrDirectedEdgeInvalid = errors.New("H3Index directed edge argument was not valid") + ErrUndirectedEdgeInvalid = errors.New("H3Index undirected edge argument was not valid") + ErrVertexInvalid = errors.New("H3Index vertex argument was not valid") + ErrPentagon = errors.New("pentagon distortion was encountered") + ErrDuplicateInput = errors.New("duplicate input was encountered in the arguments") + ErrNotNeighbors = errors.New("H3Index cell arguments were not neighbors") + ErrRsolutionMismatch = errors.New("H3Index cell arguments had incompatible resolutions") + ErrMemoryAlloc = errors.New("necessary memory allocation failed") + ErrMemoryBounds = errors.New("bounds of provided memory were not large enough") + ErrOptionInvalid = errors.New("mode or flags argument was not valid") + + ErrUnknown = errors.New("unknown error code returned by H3") + + errMap = map[C.uint32_t]error{ + 0: nil, // Success error code. + 1: ErrFailed, + 2: ErrDomain, + 3: ErrLatLngDomain, + 4: ErrResolutionDomain, + 5: ErrCellInvalid, + 6: ErrDirectedEdgeInvalid, + 7: ErrUndirectedEdgeInvalid, + 8: ErrVertexInvalid, + 9: ErrPentagon, + 10: ErrDuplicateInput, + 11: ErrNotNeighbors, + 12: ErrRsolutionMismatch, + 13: ErrMemoryAlloc, + 14: ErrMemoryBounds, + 15: ErrOptionInvalid, + } +) + type ( // Cell is an Index that identifies a single hexagon cell at a resolution. @@ -104,44 +144,44 @@ func NewLatLng(lat, lng float64) LatLng { } // LatLngToCell returns the Cell at resolution for a geographic coordinate. -func LatLngToCell(latLng LatLng, resolution int) Cell { +func LatLngToCell(latLng LatLng, resolution int) (Cell, error) { var i C.H3Index - C.latLngToCell(latLng.toCPtr(), C.int(resolution), &i) + errC := C.latLngToCell(latLng.toCPtr(), C.int(resolution), &i) - return Cell(i) + return Cell(i), toErr(errC) } // Cell returns the Cell at resolution for a geographic coordinate. -func (g LatLng) Cell(resolution int) Cell { +func (g LatLng) Cell(resolution int) (Cell, error) { return LatLngToCell(g, resolution) } // CellToLatLng returns the geographic centerpoint of a Cell. -func CellToLatLng(c Cell) LatLng { +func CellToLatLng(c Cell) (LatLng, error) { var g C.LatLng - C.cellToLatLng(C.H3Index(c), &g) + errC := C.cellToLatLng(C.H3Index(c), &g) - return latLngFromC(g) + return latLngFromC(g), toErr(errC) } // LatLng returns the Cell at resolution for a geographic coordinate. -func (c Cell) LatLng() LatLng { +func (c Cell) LatLng() (LatLng, error) { return CellToLatLng(c) } // CellToBoundary returns a CellBoundary of the Cell. -func CellToBoundary(c Cell) CellBoundary { +func CellToBoundary(c Cell) (CellBoundary, error) { var cb C.CellBoundary - C.cellToBoundary(C.H3Index(c), &cb) + errC := C.cellToBoundary(C.H3Index(c), &cb) - return cellBndryFromC(&cb) + return cellBndryFromC(&cb), toErr(errC) } // Boundary returns a CellBoundary of the Cell. -func (c Cell) Boundary() CellBoundary { +func (c Cell) Boundary() (CellBoundary, error) { return CellToBoundary(c) } @@ -152,11 +192,11 @@ func (c Cell) Boundary() CellBoundary { // // Output is placed in an array in no particular order. Elements of the output // array may be left zero, as can happen when crossing a pentagon. -func GridDisk(origin Cell, k int) []Cell { +func GridDisk(origin Cell, k int) ([]Cell, error) { out := make([]C.H3Index, maxGridDiskSize(k)) - C.gridDisk(C.H3Index(origin), C.int(k), &out[0]) + errC := C.gridDisk(C.H3Index(origin), C.int(k), &out[0]) // QUESTION: should we prune zeroes from the output? - return cellsFromC(out, true, false) + return cellsFromC(out, true, false), toErr(errC) } // GridDisk produces cells within grid distance k of the origin cell. @@ -166,7 +206,7 @@ func GridDisk(origin Cell, k int) []Cell { // // Output is placed in an array in no particular order. Elements of the output // array may be left zero, as can happen when crossing a pentagon. -func (c Cell) GridDisk(k int) []Cell { +func (c Cell) GridDisk(k int) ([]Cell, error) { return GridDisk(c, k) } @@ -178,11 +218,13 @@ func (c Cell) GridDisk(k int) []Cell { // Outer slice is ordered from origin outwards. Inner slices are in no // particular order. Elements of the output array may be left zero, as can // happen when crossing a pentagon. -func GridDiskDistances(origin Cell, k int) [][]Cell { +func GridDiskDistances(origin Cell, k int) ([][]Cell, error) { rsz := maxGridDiskSize(k) outHexes := make([]C.H3Index, rsz) outDists := make([]C.int, rsz) - C.gridDiskDistances(C.H3Index(origin), C.int(k), &outHexes[0], &outDists[0]) + if err := toErr(C.gridDiskDistances(C.H3Index(origin), C.int(k), &outHexes[0], &outDists[0])); err != nil { + return nil, err + } ret := make([][]Cell, k+1) for i := 0; i <= k; i++ { @@ -193,7 +235,7 @@ func GridDiskDistances(origin Cell, k int) [][]Cell { ret[d] = append(ret[d], Cell(outHexes[i])) } - return ret + return ret, nil } // GridDiskDistances produces cells within grid distance k of the origin cell. @@ -204,7 +246,7 @@ func GridDiskDistances(origin Cell, k int) [][]Cell { // Outer slice is ordered from origin outwards. Inner slices are in no // particular order. Elements of the output array may be left zero, as can // happen when crossing a pentagon. -func (c Cell) GridDiskDistances(k int) [][]Cell { +func (c Cell) GridDiskDistances(k int) ([][]Cell, error) { return GridDiskDistances(c, k) } @@ -215,21 +257,23 @@ func (c Cell) GridDiskDistances(k int) [][]Cell { // hexagons, tests them and their neighbors to be contained by the geoloop(s), // and then any newly found hexagons are used to test again until no new // hexagons are found. -func PolygonToCells(polygon GeoPolygon, resolution int) []Cell { +func PolygonToCells(polygon GeoPolygon, resolution int) ([]Cell, error) { if len(polygon.GeoLoop) == 0 { - return nil + return nil, nil } cpoly := allocCGeoPolygon(polygon) defer freeCGeoPolygon(&cpoly) maxLen := new(C.int64_t) - C.maxPolygonToCellsSize(&cpoly, C.int(resolution), 0, maxLen) + if err := toErr(C.maxPolygonToCellsSize(&cpoly, C.int(resolution), 0, maxLen)); err != nil { + return nil, err + } out := make([]C.H3Index, *maxLen) - C.polygonToCells(&cpoly, C.int(resolution), 0, &out[0]) + errC := C.polygonToCells(&cpoly, C.int(resolution), 0, &out[0]) - return cellsFromC(out, true, false) + return cellsFromC(out, true, false), toErr(errC) } // PolygonToCells takes a given GeoJSON-like data structure fills it with the @@ -239,7 +283,7 @@ func PolygonToCells(polygon GeoPolygon, resolution int) []Cell { // hexagons, tests them and their neighbors to be contained by the geoloop(s), // and then any newly found hexagons are used to test again until no new // hexagons are found. -func (p GeoPolygon) Cells(resolution int) []Cell { +func (p GeoPolygon) Cells(resolution int) ([]Cell, error) { return PolygonToCells(p, resolution) } @@ -251,13 +295,16 @@ func (p GeoPolygon) Cells(resolution int) []Cell { // It is expected that all hexagons in the set have the same resolution and that the set // contains no duplicates. Behavior is undefined if duplicates or multiple resolutions are // present, and the algorithm may produce unexpected or invalid output. -func CellsToMultiPolygon(cells []Cell) []GeoPolygon { +func CellsToMultiPolygon(cells []Cell) ([]GeoPolygon, error) { if len(cells) == 0 { - return nil + return nil, nil } h3Indexes := cellsToC(cells) cLinkedGeoPolygon := new(C.LinkedGeoPolygon) - C.cellsToLinkedMultiPolygon(&h3Indexes[0], C.int(len(h3Indexes)), cLinkedGeoPolygon) + if err := toErr(C.cellsToLinkedMultiPolygon(&h3Indexes[0], C.int(len(h3Indexes)), cLinkedGeoPolygon)); err != nil { + return nil, err + } + ret := []GeoPolygon{} // traverse polygons for linked list of polygons @@ -285,7 +332,7 @@ func CellsToMultiPolygon(cells []Cell) []GeoPolygon { currPoly = currPoly.next } - return ret + return ret, nil } // PointDistRads returns the "great circle" or "haversine" distance between @@ -308,99 +355,99 @@ func GreatCircleDistanceM(a, b LatLng) float64 { // HexAreaKm2 returns the average hexagon area in square kilometers at the given // resolution. -func HexagonAreaAvgKm2(resolution int) float64 { +func HexagonAreaAvgKm2(resolution int) (float64, error) { var out C.double - C.getHexagonAreaAvgKm2(C.int(resolution), &out) + errC := C.getHexagonAreaAvgKm2(C.int(resolution), &out) - return float64(out) + return float64(out), toErr(errC) } // HexAreaM2 returns the average hexagon area in square meters at the given // resolution. -func HexagonAreaAvgM2(resolution int) float64 { +func HexagonAreaAvgM2(resolution int) (float64, error) { var out C.double - C.getHexagonAreaAvgM2(C.int(resolution), &out) + errC := C.getHexagonAreaAvgM2(C.int(resolution), &out) - return float64(out) + return float64(out), toErr(errC) } // CellAreaRads2 returns the exact area of specific cell in square radians. -func CellAreaRads2(c Cell) float64 { +func CellAreaRads2(c Cell) (float64, error) { var out C.double - C.cellAreaRads2(C.H3Index(c), &out) + errC := C.cellAreaRads2(C.H3Index(c), &out) - return float64(out) + return float64(out), toErr(errC) } // CellAreaKm2 returns the exact area of specific cell in square kilometers. -func CellAreaKm2(c Cell) float64 { +func CellAreaKm2(c Cell) (float64, error) { var out C.double - C.cellAreaKm2(C.H3Index(c), &out) + errC := C.cellAreaKm2(C.H3Index(c), &out) - return float64(out) + return float64(out), toErr(errC) } // CellAreaM2 returns the exact area of specific cell in square meters. -func CellAreaM2(c Cell) float64 { +func CellAreaM2(c Cell) (float64, error) { var out C.double - C.cellAreaM2(C.H3Index(c), &out) + errC := C.cellAreaM2(C.H3Index(c), &out) - return float64(out) + return float64(out), toErr(errC) } // HexagonEdgeLengthAvgKm returns the average hexagon edge length in kilometers // at the given resolution. -func HexagonEdgeLengthAvgKm(resolution int) float64 { +func HexagonEdgeLengthAvgKm(resolution int) (float64, error) { var out C.double - C.getHexagonEdgeLengthAvgKm(C.int(resolution), &out) + errC := C.getHexagonEdgeLengthAvgKm(C.int(resolution), &out) - return float64(out) + return float64(out), toErr(errC) } // HexagonEdgeLengthAvgM returns the average hexagon edge length in meters at // the given resolution. -func HexagonEdgeLengthAvgM(resolution int) float64 { +func HexagonEdgeLengthAvgM(resolution int) (float64, error) { var out C.double - C.getHexagonEdgeLengthAvgM(C.int(resolution), &out) + errC := C.getHexagonEdgeLengthAvgM(C.int(resolution), &out) - return float64(out) + return float64(out), toErr(errC) } // EdgeLengthRads returns the exact edge length of specific unidirectional edge // in radians. -func EdgeLengthRads(e DirectedEdge) float64 { +func EdgeLengthRads(e DirectedEdge) (float64, error) { var out C.double - C.edgeLengthRads(C.H3Index(e), &out) + errC := C.edgeLengthRads(C.H3Index(e), &out) - return float64(out) + return float64(out), toErr(errC) } // EdgeLengthKm returns the exact edge length of specific unidirectional // edge in kilometers. -func EdgeLengthKm(e DirectedEdge) float64 { +func EdgeLengthKm(e DirectedEdge) (float64, error) { var out C.double - C.edgeLengthKm(C.H3Index(e), &out) + errC := C.edgeLengthKm(C.H3Index(e), &out) - return float64(out) + return float64(out), toErr(errC) } // EdgeLengthM returns the exact edge length of specific unidirectional // edge in meters. -func EdgeLengthM(e DirectedEdge) float64 { +func EdgeLengthM(e DirectedEdge) (float64, error) { var out C.double - C.edgeLengthM(C.H3Index(e), &out) + errC := C.edgeLengthM(C.H3Index(e), &out) - return float64(out) + return float64(out), toErr(errC) } // NumCells returns the number of cells at the given resolution. @@ -411,19 +458,19 @@ func NumCells(resolution int) int { } // Res0Cells returns all the cells at resolution 0. -func Res0Cells() []Cell { +func Res0Cells() ([]Cell, error) { out := make([]C.H3Index, C.res0CellCount()) - C.getRes0Cells(&out[0]) + errC := C.getRes0Cells(&out[0]) - return cellsFromC(out, false, false) + return cellsFromC(out, false, false), toErr(errC) } // Pentagons returns all the pentagons at resolution. -func Pentagons(resolution int) []Cell { +func Pentagons(resolution int) ([]Cell, error) { out := make([]C.H3Index, NumPentagons) - C.getPentagons(C.int(resolution), &out[0]) + errC := C.getPentagons(C.int(resolution), &out[0]) - return cellsFromC(out, false, false) + return cellsFromC(out, false, false), toErr(errC) } func (c Cell) Resolution() int { @@ -489,43 +536,46 @@ func (c Cell) IsValid() bool { } // Parent returns the parent or grandparent Cell of this Cell. -func (c Cell) Parent(resolution int) Cell { +func (c Cell) Parent(resolution int) (Cell, error) { var out C.H3Index - C.cellToParent(C.H3Index(c), C.int(resolution), &out) + errC := C.cellToParent(C.H3Index(c), C.int(resolution), &out) - return Cell(out) + return Cell(out), toErr(errC) } // Parent returns the parent or grandparent Cell of this Cell. -func (c Cell) ImmediateParent() Cell { +func (c Cell) ImmediateParent() (Cell, error) { return c.Parent(c.Resolution() - 1) } // Children returns the children or grandchildren cells of this Cell. -func (c Cell) Children(resolution int) []Cell { +func (c Cell) Children(resolution int) ([]Cell, error) { var outsz C.int64_t - C.cellToChildrenSize(C.H3Index(c), C.int(resolution), &outsz) + if err := toErr(C.cellToChildrenSize(C.H3Index(c), C.int(resolution), &outsz)); err != nil { + return nil, err + } out := make([]C.H3Index, outsz) - C.cellToChildren(C.H3Index(c), C.int(resolution), &out[0]) + // Seems like this function always returns E_SUCCESS. + errC := C.cellToChildren(C.H3Index(c), C.int(resolution), &out[0]) - return cellsFromC(out, false, false) + return cellsFromC(out, false, false), toErr(errC) } // ImmediateChildren returns the children or grandchildren cells of this Cell. -func (c Cell) ImmediateChildren() []Cell { +func (c Cell) ImmediateChildren() ([]Cell, error) { return c.Children(c.Resolution() + 1) } // CenterChild returns the center child Cell of this Cell. -func (c Cell) CenterChild(resolution int) Cell { +func (c Cell) CenterChild(resolution int) (Cell, error) { var out C.H3Index - C.cellToCenterChild(C.H3Index(c), C.int(resolution), &out) + errC := C.cellToCenterChild(C.H3Index(c), C.int(resolution), &out) - return Cell(out) + return Cell(out), toErr(errC) } // IsResClassIII returns true if this is a class III index. If false, this is a @@ -540,39 +590,42 @@ func (c Cell) IsPentagon() bool { } // IcosahedronFaces finds all icosahedron faces (0-19) intersected by this Cell. -func (c Cell) IcosahedronFaces() []int { +func (c Cell) IcosahedronFaces() ([]int, error) { var outsz C.int + // Seems like this function always returns E_SUCCESS. C.maxFaceCount(C.H3Index(c), &outsz) - out := make([]C.int, outsz) - C.getIcosahedronFaces(C.H3Index(c), &out[0]) + out := make([]C.int, outsz) + errC := C.getIcosahedronFaces(C.H3Index(c), &out[0]) - return intsFromC(out) + return intsFromC(out), toErr(errC) } // IsNeighbor returns true if this Cell is a neighbor of the other Cell. -func (c Cell) IsNeighbor(other Cell) bool { +func (c Cell) IsNeighbor(other Cell) (bool, error) { var out C.int - C.areNeighborCells(C.H3Index(c), C.H3Index(other), &out) + errC := C.areNeighborCells(C.H3Index(c), C.H3Index(other), &out) - return out == 1 + return out == 1, toErr(errC) } // DirectedEdge returns a DirectedEdge from this Cell to other. -func (c Cell) DirectedEdge(other Cell) DirectedEdge { +func (c Cell) DirectedEdge(other Cell) (DirectedEdge, error) { var out C.H3Index - C.cellsToDirectedEdge(C.H3Index(c), C.H3Index(other), &out) + errC := C.cellsToDirectedEdge(C.H3Index(c), C.H3Index(other), &out) - return DirectedEdge(out) + return DirectedEdge(out), toErr(errC) } // DirectedEdges returns 6 directed edges with h as the origin. -func (c Cell) DirectedEdges() []DirectedEdge { +func (c Cell) DirectedEdges() ([]DirectedEdge, error) { out := make([]C.H3Index, numCellEdges) // always 6 directed edges - C.originToDirectedEdges(C.H3Index(c), &out[0]) - return edgesFromC(out) + // Seems like this function always returns E_SUCCESS. + errC := C.originToDirectedEdges(C.H3Index(c), &out[0]) + + return edgesFromC(out), toErr(errC) } func (e DirectedEdge) IsValid() bool { @@ -580,159 +633,171 @@ func (e DirectedEdge) IsValid() bool { } // Origin returns the origin cell of this directed edge. -func (e DirectedEdge) Origin() Cell { +func (e DirectedEdge) Origin() (Cell, error) { var out C.H3Index - C.getDirectedEdgeOrigin(C.H3Index(e), &out) + errC := C.getDirectedEdgeOrigin(C.H3Index(e), &out) - return Cell(out) + return Cell(out), toErr(errC) } // Destination returns the destination cell of this directed edge. -func (e DirectedEdge) Destination() Cell { +func (e DirectedEdge) Destination() (Cell, error) { var out C.H3Index - C.getDirectedEdgeDestination(C.H3Index(e), &out) + errC := C.getDirectedEdgeDestination(C.H3Index(e), &out) - return Cell(out) + return Cell(out), toErr(errC) } // Cells returns the origin and destination cells in that order. -func (e DirectedEdge) Cells() []Cell { +func (e DirectedEdge) Cells() ([]Cell, error) { out := make([]C.H3Index, numEdgeCells) - C.directedEdgeToCells(C.H3Index(e), &out[0]) + if err := toErr(C.directedEdgeToCells(C.H3Index(e), &out[0])); err != nil { + return nil, err + } - return cellsFromC(out, false, false) + return cellsFromC(out, false, false), nil } // Boundary provides the coordinates of the boundary of the directed edge. Note, // the type returned is CellBoundary, but the coordinates will be from the // center of the origin to the center of the destination. There may be more than // 2 coordinates to account for crossing faces. -func (e DirectedEdge) Boundary() CellBoundary { +func (e DirectedEdge) Boundary() (CellBoundary, error) { var out C.CellBoundary - C.directedEdgeToBoundary(C.H3Index(e), &out) + if err := toErr(C.directedEdgeToBoundary(C.H3Index(e), &out)); err != nil { + return nil, err + } - return cellBndryFromC(&out) + return cellBndryFromC(&out), nil } // CompactCells merges full sets of children into their parent H3Index // recursively, until no more merges are possible. -func CompactCells(in []Cell) []Cell { +func CompactCells(in []Cell) ([]Cell, error) { cin := cellsToC(in) csz := C.int64_t(len(in)) // worst case no compaction so we need a set **at least** as large as the // input cout := make([]C.H3Index, csz) - C.compactCells(&cin[0], &cout[0], csz) + errC := C.compactCells(&cin[0], &cout[0], csz) - return cellsFromC(cout, false, true) + return cellsFromC(cout, false, true), toErr(errC) } // UncompactCells splits every H3Index in in if its resolution is greater // than resolution recursively. Returns all the H3Indexes at resolution resolution. -func UncompactCells(in []Cell, resolution int) []Cell { +func UncompactCells(in []Cell, resolution int) ([]Cell, error) { cin := cellsToC(in) var csz C.int64_t - C.uncompactCellsSize(&cin[0], C.int64_t(len(cin)), C.int(resolution), &csz) + if err := toErr(C.uncompactCellsSize(&cin[0], C.int64_t(len(cin)), C.int(resolution), &csz)); err != nil { + return nil, err + } cout := make([]C.H3Index, csz) - C.uncompactCells( + errC := C.uncompactCells( &cin[0], C.int64_t(len(in)), &cout[0], csz, C.int(resolution)) - return cellsFromC(cout, false, true) + return cellsFromC(cout, false, true), toErr(errC) } // ChildPosToCell returns the child of cell a at a given position within an ordered list of all // children at the specified resolution. -func ChildPosToCell(position int, a Cell, resolution int) Cell { +func ChildPosToCell(position int, a Cell, resolution int) (Cell, error) { var out C.H3Index - C.childPosToCell(C.int64_t(position), C.H3Index(a), C.int(resolution), &out) + errC := C.childPosToCell(C.int64_t(position), C.H3Index(a), C.int(resolution), &out) - return Cell(out) + return Cell(out), toErr(errC) } // ChildPosToCell returns the child cell at a given position within an ordered list of all // children at the specified resolution. -func (c Cell) ChildPosToCell(position int, resolution int) Cell { +func (c Cell) ChildPosToCell(position int, resolution int) (Cell, error) { return ChildPosToCell(position, c, resolution) } // CellToChildPos returns the position of the cell a within an ordered list of all children of the cell's parent // at the specified resolution. -func CellToChildPos(a Cell, resolution int) int { +func CellToChildPos(a Cell, resolution int) (int, error) { var out C.int64_t - C.cellToChildPos(C.H3Index(a), C.int(resolution), &out) + errC := C.cellToChildPos(C.H3Index(a), C.int(resolution), &out) - return int(out) + return int(out), toErr(errC) } // ChildPos returns the position of the cell within an ordered list of all children of the cell's parent // at the specified resolution. -func (c Cell) ChildPos(resolution int) int { +func (c Cell) ChildPos(resolution int) (int, error) { return CellToChildPos(c, resolution) } -func GridDistance(a, b Cell) int { +func GridDistance(a, b Cell) (int, error) { var out C.int64_t - C.gridDistance(C.H3Index(a), C.H3Index(b), &out) + errC := C.gridDistance(C.H3Index(a), C.H3Index(b), &out) - return int(out) + return int(out), toErr(errC) } -func (c Cell) GridDistance(other Cell) int { +func (c Cell) GridDistance(other Cell) (int, error) { return GridDistance(c, other) } -func GridPath(a, b Cell) []Cell { +func GridPath(a, b Cell) ([]Cell, error) { var outsz C.int64_t - C.gridPathCellsSize(C.H3Index(a), C.H3Index(b), &outsz) + if err := toErr(C.gridPathCellsSize(C.H3Index(a), C.H3Index(b), &outsz)); err != nil { + return nil, err + } out := make([]C.H3Index, outsz) - C.gridPathCells(C.H3Index(a), C.H3Index(b), &out[0]) + if err := toErr(C.gridPathCells(C.H3Index(a), C.H3Index(b), &out[0])); err != nil { + return nil, err + } - return cellsFromC(out, false, false) + return cellsFromC(out, false, false), nil } -func (c Cell) GridPath(other Cell) []Cell { +func (c Cell) GridPath(other Cell) ([]Cell, error) { return GridPath(c, other) } -func CellToLocalIJ(origin, cell Cell) CoordIJ { +func CellToLocalIJ(origin, cell Cell) (CoordIJ, error) { var out C.CoordIJ - C.cellToLocalIj(C.H3Index(origin), C.H3Index(cell), 0, &out) + errC := C.cellToLocalIj(C.H3Index(origin), C.H3Index(cell), 0, &out) - return CoordIJ{int(out.i), int(out.j)} + return CoordIJ{int(out.i), int(out.j)}, toErr(errC) } -func LocalIJToCell(origin Cell, ij CoordIJ) Cell { +func LocalIJToCell(origin Cell, ij CoordIJ) (Cell, error) { var out C.H3Index - C.localIjToCell(C.H3Index(origin), ij.toCPtr(), 0, &out) + errC := C.localIjToCell(C.H3Index(origin), ij.toCPtr(), 0, &out) - return Cell(out) + return Cell(out), toErr(errC) } -func CellToVertex(c Cell, vertexNum int) Cell { +func CellToVertex(c Cell, vertexNum int) (Cell, error) { var out C.H3Index - C.cellToVertex(C.H3Index(c), C.int(vertexNum), &out) + errC := C.cellToVertex(C.H3Index(c), C.int(vertexNum), &out) - return Cell(out) + return Cell(out), toErr(errC) } -func CellToVertexes(c Cell) []Cell { +func CellToVertexes(c Cell) ([]Cell, error) { out := make([]C.H3Index, numCellVertexes) - C.cellToVertexes(C.H3Index(c), &out[0]) + if err := toErr(C.cellToVertexes(C.H3Index(c), &out[0])); err != nil { + return nil, err + } - return cellsFromC(out, true, false) + return cellsFromC(out, true, false), nil } -func VertexToLatLng(vertex Cell) LatLng { +func VertexToLatLng(vertex Cell) (LatLng, error) { var out C.LatLng - C.vertexToLatLng(C.H3Index(vertex), &out) + errC := C.vertexToLatLng(C.H3Index(vertex), &out) - return latLngFromC(out) + return latLngFromC(out), toErr(errC) } func IsValidVertex(c Cell) bool { @@ -942,3 +1007,12 @@ func (ij CoordIJ) toCPtr() *C.CoordIJ { j: C.int(ij.J), } } + +func toErr(errC C.uint32_t) error { + err, ok := errMap[errC] + if ok { + return err + } + + return ErrUnknown +} diff --git a/h3_test.go b/h3_test.go index db853b3..fcd409b 100644 --- a/h3_test.go +++ b/h3_test.go @@ -16,8 +16,10 @@ package h3 import ( + "errors" "fmt" "math" + "reflect" "sort" "testing" ) @@ -108,54 +110,125 @@ var ( func TestLatLngToCell(t *testing.T) { t.Parallel() - c := LatLngToCell(validLatLng1, 5) + + c, err := LatLngToCell(validLatLng1, 5) assertEqual(t, validCell, c) + assertNoErr(t, err) + + _, err = LatLngToCell(NewLatLng(0, 0), MaxResolution+1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) } func TestCellToLatLng(t *testing.T) { t.Parallel() - g := CellToLatLng(validCell) + + g, err := CellToLatLng(validCell) assertEqualLatLng(t, validLatLng1, g) + assertNoErr(t, err) + + _, err = CellToLatLng(-1) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) } func TestToCellBoundary(t *testing.T) { t.Parallel() - boundary := validCell.Boundary() + + boundary, err := validCell.Boundary() assertEqualLatLngs(t, validGeoLoop[:], boundary[:]) + assertNoErr(t, err) + + c := Cell(-1) + _, err = c.Boundary() + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) +} + +func TestCellToLocalIJ(t *testing.T) { + t.Parallel() + + _, err := CellToLocalIJ(validCell, validCell) + assertNoErr(t, err) + + _, err = CellToLocalIJ(-1, -1) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) +} + +func TestLocalIJToCell(t *testing.T) { + t.Parallel() + + ij, _ := CellToLocalIJ(validCell, validCell) + c, err := LocalIJToCell(validCell, ij) + assertNoErr(t, err) + assertEqual(t, c, validCell) + + _, err = LocalIJToCell(-1, ij) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) } func TestGridDisk(t *testing.T) { t.Parallel() + t.Run("no pentagon", func(t *testing.T) { t.Parallel() + + gd, err := validCell.GridDisk(len(validDiskDist3_1) - 1) assertEqualDisks(t, flattenDisks(validDiskDist3_1), - validCell.GridDisk(len(validDiskDist3_1)-1)) + gd, + ) + assertNoErr(t, err) }) + t.Run("pentagon ok", func(t *testing.T) { t.Parallel() + assertNoPanic(t, func() { - disk := GridDisk(pentagonCell, 1) + disk, err := GridDisk(pentagonCell, 1) assertEqual(t, 6, len(disk), "expected pentagon disk to have 6 cells") + assertNoErr(t, err) }) }) + + t.Run("invalid cell", func(t *testing.T) { + t.Parallel() + + c := Cell(-1) + _, err := c.GridDisk(1) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) + }) } func TestGridDiskDistances(t *testing.T) { t.Parallel() + t.Run("no pentagon", func(t *testing.T) { t.Parallel() - rings := validCell.GridDiskDistances(len(validDiskDist3_1) - 1) + rings, err := validCell.GridDiskDistances(len(validDiskDist3_1) - 1) + assertNoErr(t, err) assertEqualDiskDistances(t, validDiskDist3_1, rings) }) + t.Run("pentagon centered", func(t *testing.T) { t.Parallel() assertNoPanic(t, func() { - rings := GridDiskDistances(pentagonCell, 1) + rings, err := GridDiskDistances(pentagonCell, 1) + assertNoErr(t, err) assertEqual(t, 2, len(rings), "expected 2 rings") assertEqual(t, 5, len(rings[1]), "expected 5 cells in second ring") }) }) + + t.Run("invalid k-ring", func(t *testing.T) { + rings, err := GridDiskDistances(pentagonCell, -1) + assertErr(t, err) + assertErrIs(t, err, ErrDomain) + assertNil(t, rings) + }) } func TestIsValid(t *testing.T) { @@ -166,18 +239,23 @@ func TestIsValid(t *testing.T) { func TestRoundtrip(t *testing.T) { t.Parallel() + t.Run("latlng", func(t *testing.T) { t.Parallel() expectedGeo := LatLng{Lat: 1, Lng: 2} - c := LatLngToCell(expectedGeo, MaxResolution) - actualGeo := CellToLatLng(c) + c, _ := LatLngToCell(expectedGeo, MaxResolution) + actualGeo, _ := CellToLatLng(c) assertEqualLatLng(t, expectedGeo, actualGeo) - assertEqualLatLng(t, expectedGeo, expectedGeo.Cell(MaxResolution).LatLng()) + + expectedCell, _ := expectedGeo.Cell(MaxResolution) + expectedLatLng, _ := expectedCell.LatLng() + assertEqualLatLng(t, expectedGeo, expectedLatLng) }) + t.Run("cell", func(t *testing.T) { t.Parallel() - geo := CellToLatLng(validCell) - actualCell := LatLngToCell(geo, validCell.Resolution()) + geo, _ := CellToLatLng(validCell) + actualCell, _ := LatLngToCell(geo, validCell.Resolution()) assertEqual(t, validCell, actualCell) }) } @@ -186,11 +264,12 @@ func TestResolution(t *testing.T) { t.Parallel() for i := 1; i <= MaxResolution; i++ { - c := LatLngToCell(validLatLng1, i) + c, _ := LatLngToCell(validLatLng1, i) assertEqual(t, i, c.Resolution()) } - for _, e := range validCell.DirectedEdges() { + edges, _ := validCell.DirectedEdges() + for _, e := range edges { assertEqual(t, validCell.Resolution(), e.Resolution()) } } @@ -204,12 +283,26 @@ func TestBaseCellNumber(t *testing.T) { func TestParent(t *testing.T) { t.Parallel() // get the index's parent by requesting that index's resolution+1 - parent := validCell.ImmediateParent() + parent, err := validCell.ImmediateParent() + assertNoErr(t, err) // get the children at the resolution of the original index - children := parent.ImmediateChildren() + children, _ := parent.ImmediateChildren() assertCellIn(t, validCell, children) + + _, err = validCell.Parent(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) +} + +func TestChildren_Error(t *testing.T) { + t.Parallel() + + children, err := validCell.Children(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) + assertNil(t, children) } func TestCompactCells(t *testing.T) { @@ -217,43 +310,76 @@ func TestCompactCells(t *testing.T) { in := flattenDisks(validDiskDist3_1[:2]) t.Logf("in: %v", in) - out := CompactCells(in) + out, err := CompactCells(in) t.Logf("out: %v", in) + assertNoErr(t, err) assertEqual(t, 1, len(out)) - assertEqual(t, validDiskDist3_1[0][0].ImmediateParent(), out[0]) + + p, _ := validDiskDist3_1[0][0].ImmediateParent() + assertEqual(t, p, out[0]) + + _, err = CompactCells([]Cell{-1}) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) } func TestUncompactCells(t *testing.T) { t.Parallel() + // get the index's parent by requesting that index's resolution+1 - parent := validCell.ImmediateParent() - out := UncompactCells([]Cell{parent}, parent.Resolution()+1) + parent, _ := validCell.ImmediateParent() + out, err := UncompactCells([]Cell{parent}, parent.Resolution()+1) + assertNoErr(t, err) assertCellIn(t, validCell, out) + + out, err = UncompactCells([]Cell{parent}, -1) + assertErr(t, err) + assertErrIs(t, err, ErrRsolutionMismatch) + assertNil(t, out) } func TestChildPosToCell(t *testing.T) { t.Parallel() - children := validCell.Children(6) + children, _ := validCell.Children(6) - assertEqual(t, children[0], validCell.ChildPosToCell(0, 6)) - assertEqual(t, children[0], ChildPosToCell(0, validCell, 6)) + cell, err := validCell.ChildPosToCell(0, 6) + assertNoErr(t, err) + assertEqual(t, children[0], cell) + + cell, err = ChildPosToCell(0, validCell, 6) + assertNoErr(t, err) + assertEqual(t, children[0], cell) + + _, err = validCell.ChildPosToCell(0, -1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) } func TestChildPos(t *testing.T) { t.Parallel() - children := validCell.Children(7) + children, _ := validCell.Children(7) + + pos, err := children[32].ChildPos(validCell.Resolution()) + assertNoErr(t, err) + assertEqual(t, 32, pos) - assertEqual(t, 32, children[32].ChildPos(validCell.Resolution())) - assertEqual(t, 32, CellToChildPos(children[32], validCell.Resolution())) + pos, err = CellToChildPos(children[32], validCell.Resolution()) + assertNoErr(t, err) + assertEqual(t, 32, pos) + + _, err = validCell.ChildPos(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) } func TestIsResClassIII(t *testing.T) { t.Parallel() + p, _ := validCell.ImmediateParent() assertTrue(t, validCell.IsResClassIII()) - assertFalse(t, validCell.ImmediateParent().IsResClassIII()) + assertFalse(t, p.IsResClassIII()) } func TestIsPentagon(t *testing.T) { @@ -264,16 +390,31 @@ func TestIsPentagon(t *testing.T) { func TestIsNeighbor(t *testing.T) { t.Parallel() - assertFalse(t, validCell.IsNeighbor(pentagonCell)) - assertTrue(t, validCell.DirectedEdges()[0].Destination().IsNeighbor(validCell)) + + isNeighbor, err := validCell.IsNeighbor(pentagonCell) + assertErr(t, err) + assertErrIs(t, err, ErrRsolutionMismatch) + assertFalse(t, isNeighbor) + + edges, _ := validCell.DirectedEdges() + dest, _ := edges[0].Destination() + isNeighbor, err = dest.IsNeighbor(validCell) + assertNoErr(t, err) + assertTrue(t, isNeighbor) } func TestDirectedEdge(t *testing.T) { t.Parallel() origin := validDiskDist3_1[1][0] - destination := origin.DirectedEdges()[0].Destination() - edge := origin.DirectedEdge(destination) + edges, err := origin.DirectedEdges() + assertNoErr(t, err) + + destination, err := edges[0].Destination() + assertNoErr(t, err) + + edge, err := origin.DirectedEdge(destination) + assertNoErr(t, err) t.Run("is valid", func(t *testing.T) { t.Parallel() @@ -283,33 +424,66 @@ func TestDirectedEdge(t *testing.T) { t.Run("get origin/destination from edge", func(t *testing.T) { t.Parallel() - assertEqual(t, origin, edge.Origin()) - assertEqual(t, destination, edge.Destination()) + edgeOrigin, err := edge.Origin() + assertNoErr(t, err) + assertEqual(t, origin, edgeOrigin) + + edgeDestination, err := edge.Destination() + assertNoErr(t, err) + assertEqual(t, destination, edgeDestination) // shadow origin/destination - cells := edge.Cells() + cells, err := edge.Cells() + assertNoErr(t, err) + origin, destination := cells[0], cells[1] - assertEqual(t, origin, edge.Origin()) - assertEqual(t, destination, edge.Destination()) + assertEqual(t, origin, edgeOrigin) + assertEqual(t, destination, edgeDestination) + }) + + t.Run("edge cells error", func(t *testing.T) { + t.Parallel() + cells, err := DirectedEdge(-1).Cells() + assertErr(t, err) + assertErrIs(t, err, ErrDirectedEdgeInvalid) + assertNil(t, cells) }) t.Run("get edges from hexagon", func(t *testing.T) { t.Parallel() - edges := validCell.DirectedEdges() + edges, err := validCell.DirectedEdges() + assertNoErr(t, err) assertEqual(t, 6, len(edges), "hexagon has 6 edges") }) t.Run("get edges from pentagon", func(t *testing.T) { t.Parallel() - edges := pentagonCell.DirectedEdges() + edges, err := pentagonCell.DirectedEdges() + assertNoErr(t, err) assertEqual(t, 5, len(edges), "pentagon has 5 edges") }) t.Run("get boundary from edge", func(t *testing.T) { t.Parallel() - gb := edge.Boundary() + gb, err := edge.Boundary() + assertNoErr(t, err) assertEqual(t, 2, len(gb), "edge has 2 boundary cells") }) + + t.Run("boundary error", func(t *testing.T) { + t.Parallel() + gb, err := DirectedEdge(-1).Boundary() + assertErr(t, err) + assertErrIs(t, err, ErrDirectedEdgeInvalid) + assertNil(t, gb) + }) + + t.Run("error", func(t *testing.T) { + t.Parallel() + _, err := validCell.DirectedEdge(-1) + assertErr(t, err) + assertErrIs(t, err, ErrNotNeighbors) + }) } func TestStrings(t *testing.T) { @@ -355,13 +529,17 @@ func TestPolygonToCells(t *testing.T) { t.Run("empty", func(t *testing.T) { t.Parallel() - cells := PolygonToCells(GeoPolygon{}, 6) + cells, err := PolygonToCells(GeoPolygon{}, 6) + assertNoErr(t, err) assertEqual(t, 0, len(cells)) }) t.Run("without holes", func(t *testing.T) { t.Parallel() - cells := validGeoPolygonNoHoles.Cells(6) + + cells, err := validGeoPolygonNoHoles.Cells(6) + assertNoErr(t, err) + expectedIndexes := []Cell{ 0x860dab607ffffff, 0x860dab60fffffff, @@ -376,7 +554,10 @@ func TestPolygonToCells(t *testing.T) { t.Run("with hole", func(t *testing.T) { t.Parallel() - cells := validGeoPolygonHoles.Cells(6) + + cells, err := validGeoPolygonHoles.Cells(6) + assertNoErr(t, err) + expectedIndexes := []Cell{ 0x860dab60fffffff, 0x860dab617ffffff, @@ -387,38 +568,57 @@ func TestPolygonToCells(t *testing.T) { } assertEqualCells(t, expectedIndexes, cells) }) + + t.Run("error", func(t *testing.T) { + t.Parallel() + + cells, err := validGeoPolygonHoles.Cells(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) + assertNil(t, cells) + }) } func TestCellsToMultiPolygon(t *testing.T) { t.Parallel() // 7 cells in disk -> 1 polygon, 18-point loop, and no holes - cells := GridDisk(LatLngToCell(NewLatLng(0, 0), 10), 1) - res := CellsToMultiPolygon(cells) + c, _ := LatLngToCell(NewLatLng(0, 0), 10) + cells, _ := GridDisk(c, 1) + res, err := CellsToMultiPolygon(cells) + assertNoErr(t, err) assertEqual(t, len(res), 1) assertEqual(t, len(res[0].GeoLoop), 18) assertEqual(t, len(res[0].Holes), 0) // 6 cells in ring -> 1 polygon, 18-point loop, and 1 6-point hole - cells = GridDisk(LatLngToCell(NewLatLng(0, 0), 10), 1)[1:] - res = CellsToMultiPolygon(cells) + c, _ = LatLngToCell(NewLatLng(0, 0), 10) + cells, _ = GridDisk(c, 1) + res, err = CellsToMultiPolygon(cells[1:]) + assertNoErr(t, err) assertEqual(t, len(res), 1) assertEqual(t, len(res[0].GeoLoop), 18) assertEqual(t, len(res[0].Holes), 1) assertEqual(t, len(res[0].Holes[0]), 6) // 2 hexagons connected -> 1 polygon, 10-point loop (2 shared points) and no holes - cells = GridDisk(LatLngToCell(NewLatLng(0, 0), 10), 1)[:2] - res = CellsToMultiPolygon(cells) + c, _ = LatLngToCell(NewLatLng(0, 0), 10) + cells, _ = GridDisk(c, 1) + res, err = CellsToMultiPolygon(cells[:2]) + assertNoErr(t, err) assertEqual(t, len(res), 1) assertEqual(t, len(res[0].GeoLoop), 10) assertEqual(t, len(res[0].Holes), 0) // 2 distinct disks -> 2 polygons, 2 18-point loops, and no holes - cells1 := GridDisk(LatLngToCell(NewLatLng(0, 0), 10), 1) - cells2 := GridDisk(LatLngToCell(NewLatLng(10, 10), 10), 1) + c, _ = LatLngToCell(NewLatLng(0, 0), 10) + cells1, _ := GridDisk(c, 1) + + c, _ = LatLngToCell(NewLatLng(10, 10), 10) + cells2, _ := GridDisk(c, 1) cells = append(cells1, cells2...) - res = CellsToMultiPolygon(cells) + res, err = CellsToMultiPolygon(cells) + assertNoErr(t, err) assertEqual(t, len(res), 2) assertEqual(t, len(res[0].GeoLoop), 18) assertEqual(t, len(res[0].Holes), 0) @@ -426,51 +626,104 @@ func TestCellsToMultiPolygon(t *testing.T) { assertEqual(t, len(res[1].Holes), 0) // empty - res = CellsToMultiPolygon([]Cell{}) + res, err = CellsToMultiPolygon([]Cell{}) + assertNoErr(t, err) assertEqual(t, len(res), 0) + + // Error. + res, err = CellsToMultiPolygon([]Cell{-1}) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) + assertNil(t, res) } func TestGridPath(t *testing.T) { t.Parallel() - path := lineStartCell.GridPath(lineEndCell) + path, err := lineStartCell.GridPath(lineEndCell) + assertNoErr(t, err) assertEqual(t, lineStartCell, path[0]) assertEqual(t, lineEndCell, path[len(path)-1]) for i := 0; i < len(path)-1; i++ { - assertTrue(t, path[i].IsNeighbor(path[i+1])) + isNeighbor, _ := path[i].IsNeighbor(path[i+1]) + assertTrue(t, isNeighbor) } + + path, err = GridPath(1, -1) + assertErr(t, err) + assertErrIs(t, err, ErrRsolutionMismatch) + assertNil(t, path) + + c1, _ := NewLatLng(1, 1).Cell(5) + c2, _ := NewLatLng(50.10320148224132, -143.47849001502516).Cell(5) + path, err = GridPath(c1, c2) + assertErr(t, err) + assertErrIs(t, err, ErrFailed) + assertNil(t, path) } -func TestHexAreaKm2(t *testing.T) { +func TestHexAreaKm2(t *testing.T) { //nolint:dupl // // it's ok to have duplication in tests. t.Parallel() + t.Run("min resolution", func(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(4357449.4161), HexagonAreaAvgKm2(0)) + area, err := HexagonAreaAvgKm2(0) + assertNoErr(t, err) + assertEqualEps(t, float64(4357449.4161), area) }) + t.Run("max resolution", func(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(0.0000009), HexagonAreaAvgKm2(15)) + area, err := HexagonAreaAvgKm2(15) + assertNoErr(t, err) + assertEqualEps(t, float64(0.0000009), area) }) + t.Run("mid resolution", func(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(0.7373276), HexagonAreaAvgKm2(8)) + area, err := HexagonAreaAvgKm2(8) + assertNoErr(t, err) + assertEqualEps(t, float64(0.7373276), area) + }) + + t.Run("resolution error", func(t *testing.T) { + t.Parallel() + _, err := HexagonAreaAvgKm2(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) }) } -func TestHexAreaM2(t *testing.T) { +func TestHexAreaM2(t *testing.T) { //nolint:dupl // // it's ok to have duplication in tests. t.Parallel() + t.Run("min resolution", func(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(4357449416078.3901), HexagonAreaAvgM2(0)) + area, err := HexagonAreaAvgM2(0) + assertNoErr(t, err) + assertEqualEps(t, float64(4357449416078.3901), area) }) + t.Run("max resolution", func(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(0.8953), HexagonAreaAvgM2(15)) + area, err := HexagonAreaAvgM2(15) + assertNoErr(t, err) + assertEqualEps(t, float64(0.8953), area) }) + t.Run("mid resolution", func(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(737327.5976), HexagonAreaAvgM2(8)) + area, err := HexagonAreaAvgM2(8) + assertNoErr(t, err) + assertEqualEps(t, float64(737327.5976), area) + }) + + t.Run("resolution error", func(t *testing.T) { + t.Parallel() + _, err := HexagonAreaAvgM2(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) }) } @@ -494,71 +747,126 @@ func TestPointDistM(t *testing.T) { func TestCellAreaRads2(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(0.000006643967854567278), CellAreaRads2(validCell)) + area, err := CellAreaRads2(validCell) + assertNoErr(t, err) + assertEqualEps(t, float64(0.000006643967854567278), area) + + _, err = CellAreaRads2(-1) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) } func TestCellAreaKm2(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(269.6768779509321), CellAreaKm2(validCell)) + area, err := CellAreaKm2(validCell) + assertNoErr(t, err) + assertEqualEps(t, float64(269.6768779509321), area) + + _, err = CellAreaKm2(-1) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) } func TestCellAreaM2(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(269676877.95093215), CellAreaM2(validCell)) + area, err := CellAreaM2(validCell) + assertNoErr(t, err) + assertEqualEps(t, float64(269676877.95093215), area) + + _, err = CellAreaM2(-1) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) } -func TestHexagonEdgeLengthKm(t *testing.T) { +func TestHexagonEdgeLengthKm(t *testing.T) { //nolint:dupl // // it's ok to have duplication in tests. t.Parallel() t.Run("min resolution", func(t *testing.T) { t.Parallel() - assertEqual(t, float64(1107.712591), HexagonEdgeLengthAvgKm(0)) + length, err := HexagonEdgeLengthAvgKm(0) + assertNoErr(t, err) + assertEqual(t, float64(1107.712591), length) }) t.Run("max resolution", func(t *testing.T) { t.Parallel() - assertEqual(t, float64(0.000509713), HexagonEdgeLengthAvgKm(15)) + length, err := HexagonEdgeLengthAvgKm(15) + assertNoErr(t, err) + assertEqual(t, float64(0.000509713), length) }) t.Run("mid resolution", func(t *testing.T) { t.Parallel() - assertEqual(t, float64(0.461354684), HexagonEdgeLengthAvgKm(8)) + length, err := HexagonEdgeLengthAvgKm(8) + assertNoErr(t, err) + assertEqual(t, float64(0.461354684), length) + }) + t.Run("invalid resolution", func(t *testing.T) { + t.Parallel() + _, err := HexagonEdgeLengthAvgKm(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) }) } -func TestHexagonEdgeLengthM(t *testing.T) { +func TestHexagonEdgeLengthM(t *testing.T) { //nolint:dupl // // it's ok to have duplication in tests. t.Parallel() t.Run("min resolution", func(t *testing.T) { t.Parallel() - area := HexagonEdgeLengthAvgM(0) + area, err := HexagonEdgeLengthAvgM(0) + assertNoErr(t, err) assertEqual(t, float64(1107712.591), area) }) t.Run("max resolution", func(t *testing.T) { t.Parallel() - area := HexagonEdgeLengthAvgM(15) + area, err := HexagonEdgeLengthAvgM(15) + assertNoErr(t, err) assertEqual(t, float64(0.509713273), area) }) t.Run("mid resolution", func(t *testing.T) { t.Parallel() - area := HexagonEdgeLengthAvgM(8) + area, err := HexagonEdgeLengthAvgM(8) + assertNoErr(t, err) assertEqual(t, float64(461.3546837), area) }) + t.Run("invalid resolution", func(t *testing.T) { + t.Parallel() + _, err := HexagonEdgeLengthAvgM(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) + }) } func TestEdgeLengthRads(t *testing.T) { t.Parallel() - assertEqualEps(t, float64(0.001569665746947077), EdgeLengthRads(validEdge)) + length, err := EdgeLengthRads(validEdge) + assertNoErr(t, err) + assertEqualEps(t, float64(0.001569665746947077), length) + + _, err = EdgeLengthRads(-1) + assertErr(t, err) + assertErrIs(t, err, ErrDirectedEdgeInvalid) } func TestEdgeLengthKm(t *testing.T) { t.Parallel() - distance := EdgeLengthKm(validEdge) + distance, err := EdgeLengthKm(validEdge) + assertNoErr(t, err) assertEqualEps(t, float64(10.00035174544159), distance) + + _, err = EdgeLengthKm(-1) + assertErr(t, err) + assertErrIs(t, err, ErrDirectedEdgeInvalid) } func TestEdgeLengthM(t *testing.T) { t.Parallel() - distance := EdgeLengthM(validEdge) + distance, err := EdgeLengthM(validEdge) + assertNoErr(t, err) assertEqualEps(t, float64(10000.351745441589), distance) + + _, err = EdgeLengthM(-1) + assertErr(t, err) + assertErrIs(t, err, ErrDirectedEdgeInvalid) } func TestNumCells(t *testing.T) { @@ -579,8 +887,9 @@ func TestNumCells(t *testing.T) { func TestRes0Cells(t *testing.T) { t.Parallel() - cells := Res0Cells() + cells, err := Res0Cells() + assertNoErr(t, err) assertEqual(t, 122, len(cells)) assertEqual(t, Cell(0x8001fffffffffff), cells[0]) assertEqual(t, Cell(0x80f3fffffffffff), cells[121]) @@ -588,23 +897,42 @@ func TestRes0Cells(t *testing.T) { func TestGridDistance(t *testing.T) { t.Parallel() - assertEqual(t, 1823, lineStartCell.GridDistance(lineEndCell)) + + dist, err := lineStartCell.GridDistance(lineEndCell) + assertEqual(t, 1823, dist) + assertNoErr(t, err) + + _, err = GridDistance(-1, -2) + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) } func TestCenterChild(t *testing.T) { t.Parallel() - child := validCell.CenterChild(15) + child, err := validCell.CenterChild(15) + assertNoErr(t, err) assertEqual(t, Cell(0x8f0dab600000000), child) + + _, err = validCell.CenterChild(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) } func TestIcosahedronFaces(t *testing.T) { t.Parallel() - faces := validDiskDist3_1[1][1].IcosahedronFaces() + faces, err := validDiskDist3_1[1][1].IcosahedronFaces() assertEqual(t, 1, len(faces)) assertEqual(t, 1, faces[0]) + assertNoErr(t, err) + + c := Cell(-1) + + _, err = c.IcosahedronFaces() + assertErr(t, err) + assertErrIs(t, err, ErrCellInvalid) } func TestPentagons(t *testing.T) { @@ -615,7 +943,8 @@ func TestPentagons(t *testing.T) { t.Run(fmt.Sprintf("res=%d", res), func(t *testing.T) { t.Parallel() - pentagons := Pentagons(res) + pentagons, err := Pentagons(res) + assertNoErr(t, err) assertEqual(t, 12, len(pentagons)) for _, pentagon := range pentagons { @@ -624,18 +953,23 @@ func TestPentagons(t *testing.T) { } }) } + + _, err := Pentagons(-1) + assertErr(t, err) + assertErrIs(t, err, ErrResolutionDomain) } func TestCellToVertex(t *testing.T) { t.Parallel() testCases := []struct { + expectedErr error cell Cell expectedVertex Cell vertexNum int }{ - {cell: validCell, expectedVertex: 0x2050dab63fffffff, vertexNum: 0}, - {cell: validCell, expectedVertex: 0, vertexNum: 6}, // vertex num should be between 0 and 5 for hexagonal cells. + {cell: validCell, expectedVertex: 0x2050dab63fffffff, vertexNum: 0, expectedErr: nil}, + {cell: validCell, expectedVertex: 0, vertexNum: 6, expectedErr: ErrDomain}, // vertex num should be between 0 and 5 for hexagonal cells. } for i, tc := range testCases { @@ -644,7 +978,8 @@ func TestCellToVertex(t *testing.T) { t.Run(fmt.Sprint(i), func(t *testing.T) { t.Parallel() - vertex := CellToVertex(tc.cell, tc.vertexNum) + vertex, err := CellToVertex(tc.cell, tc.vertexNum) + assertErrIs(t, err, tc.expectedErr) assertEqual(t, tc.expectedVertex, vertex) }) } @@ -654,12 +989,13 @@ func TestCellToVertexes(t *testing.T) { t.Parallel() testCases := []struct { + expectedErr error cell Cell numVertexes int }{ - {cell: validCell, numVertexes: 6}, - {cell: pentagonCell, numVertexes: 5}, - {cell: -1, numVertexes: 0}, // Invalid cel. + {cell: validCell, numVertexes: 6, expectedErr: nil}, + {cell: pentagonCell, numVertexes: 5, expectedErr: nil}, + {cell: -1, numVertexes: 0, expectedErr: ErrFailed}, // Invalid cell. } for _, tc := range testCases { @@ -667,7 +1003,8 @@ func TestCellToVertexes(t *testing.T) { t.Run(fmt.Sprint(tc.numVertexes), func(t *testing.T) { t.Parallel() - vertexes := CellToVertexes(tc.cell) + vertexes, err := CellToVertexes(tc.cell) + assertErrIs(t, err, tc.expectedErr) assertEqual(t, tc.numVertexes, len(vertexes)) }) } @@ -676,12 +1013,15 @@ func TestCellToVertexes(t *testing.T) { func TestVertexToLatLng(t *testing.T) { t.Parallel() + vertex, _ := CellToVertex(validCell, 0) + testCases := []struct { + expectedErr error vertex Cell expectedLatLng LatLng }{ - {vertex: CellToVertex(validCell, 0), expectedLatLng: LatLng{Lat: 67.22475, Lng: -168.52301}}, - {vertex: -1, expectedLatLng: LatLng{}}, // Invalid vertex. + {vertex: vertex, expectedLatLng: LatLng{Lat: 67.22475, Lng: -168.52301}, expectedErr: nil}, + {vertex: -1, expectedLatLng: LatLng{}, expectedErr: ErrCellInvalid}, // Invalid vertex. } for i, tc := range testCases { @@ -690,7 +1030,8 @@ func TestVertexToLatLng(t *testing.T) { t.Run(fmt.Sprint(i), func(t *testing.T) { t.Parallel() - latLng := VertexToLatLng(tc.vertex) + latLng, err := VertexToLatLng(tc.vertex) + assertErrIs(t, err, tc.expectedErr) assertEqualLatLng(t, tc.expectedLatLng, latLng) }) } @@ -715,6 +1056,16 @@ func assertErr(t *testing.T, err error) { } } +func assertErrIs(t *testing.T, err, target error) { + t.Helper() + + if errors.Is(err, target) { + return + } + + t.Errorf("errors don't match, err: %s, target err: %s", err, target) +} + func assertNoErr(t *testing.T, err error) { t.Helper() @@ -723,14 +1074,14 @@ func assertNoErr(t *testing.T, err error) { } } -func assertEqual[T comparable](t *testing.T, expected, actual T, msgAndArgs ...interface{}) { +func assertEqual[T comparable](t *testing.T, expected, actual T, msgAndArgs ...any) { t.Helper() if expected != actual { var ( expStr, actStr string - e, a interface{} = expected, actual + e, a any = expected, actual ) switch e.(type) { @@ -747,7 +1098,7 @@ func assertEqual[T comparable](t *testing.T, expected, actual T, msgAndArgs ...i } } -func assertEqualEps(t *testing.T, expected, actual float64, msgAndArgs ...interface{}) { +func assertEqualEps(t *testing.T, expected, actual float64, msgAndArgs ...any) { t.Helper() if !equalEps(expected, actual) { @@ -762,7 +1113,7 @@ func assertEqualLatLng(t *testing.T, expected, actual LatLng) { assertEqualEps(t, expected.Lng, actual.Lng, "longitude mismatch") } -func assertEqualLatLngs(t *testing.T, expected, actual []LatLng, msgAndArgs ...interface{}) { +func assertEqualLatLngs(t *testing.T, expected, actual []LatLng, msgAndArgs ...any) { t.Helper() if len(expected) != len(actual) { @@ -795,7 +1146,7 @@ func assertEqualLatLngs(t *testing.T, expected, actual []LatLng, msgAndArgs ...i } } -func assertEqualCells(t *testing.T, expected, actual []Cell, msgAndArgs ...interface{}) { +func assertEqualCells(t *testing.T, expected, actual []Cell, msgAndArgs ...any) { t.Helper() if len(expected) != len(actual) { @@ -918,6 +1269,30 @@ func assertTrue(t *testing.T, b bool) { assertEqual(t, true, b) } +func assertNil(t *testing.T, val any) { + t.Helper() + + if val == nil { + return + } + + value := reflect.ValueOf(val) + switch value.Kind() { + case + reflect.Chan, reflect.Func, + reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice, reflect.UnsafePointer: + if value.IsNil() { + return + } + default: + t.Errorf("expected value to be nil, got: %v", val) + return + } + + t.Errorf("expected value to be nil, got: %v", val) +} + func sortCells(s []Cell) []Cell { sort.SliceStable(s, func(i, j int) bool { return s[i] < s[j] @@ -926,7 +1301,7 @@ func sortCells(s []Cell) []Cell { return s } -func logMsgAndArgs(t *testing.T, msgAndArgs ...interface{}) { +func logMsgAndArgs(t *testing.T, msgAndArgs ...any) { t.Helper() if len(msgAndArgs) > 0 { @@ -972,3 +1347,22 @@ func copyCells(s []Cell) []Cell { return c } + +func TestToErr(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + assertNoErr(t, toErr(0)) + }) + + t.Run("pentagon error", func(t *testing.T) { + t.Parallel() + assertErrIs(t, toErr(9), ErrPentagon) + }) + + t.Run("unknown error", func(t *testing.T) { + t.Parallel() + assertErrIs(t, toErr(999), ErrUnknown) + }) +}