From 8b9d0a6d182f7982f186844d1222f94beec31257 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Mon, 10 Nov 2025 22:04:55 -0800 Subject: [PATCH 01/23] bump h3lib to v4.4.0 --- src/h3lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/h3lib b/src/h3lib index f6d1b0ba..ae8d908a 160000 --- a/src/h3lib +++ b/src/h3lib @@ -1 +1 @@ -Subproject commit f6d1b0ba0a0eb076abe655ffdd576a445373a39e +Subproject commit ae8d908a5e6eb458a5d18a7eec4a00d9f3cb0a89 From 8fee5a54b56c17eb2c4e6bb01044ea3c229b7c11 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Tue, 18 Nov 2025 11:11:20 -0800 Subject: [PATCH 02/23] bump h3lib to v4.4.1 --- src/h3lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/h3lib b/src/h3lib index ae8d908a..70d4c9e5 160000 --- a/src/h3lib +++ b/src/h3lib @@ -1 +1 @@ -Subproject commit ae8d908a5e6eb458a5d18a7eec4a00d9f3cb0a89 +Subproject commit 70d4c9e5007d74d77714afa44d5a2e52606828bf From 352c9555e0535e91d9c69cf8f94f9e79cf95eda6 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Tue, 18 Nov 2025 11:16:04 -0800 Subject: [PATCH 03/23] tests pass with new version --- justfile | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 justfile diff --git a/justfile b/justfile new file mode 100644 index 00000000..d17ba2b5 --- /dev/null +++ b/justfile @@ -0,0 +1 @@ +# REMOVE BEFORE LANDING diff --git a/pyproject.toml b/pyproject.toml index 9f88ce87..218b761d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = 'scikit_build_core.build' [project] name = 'h3' -version = '4.3.1' +version = '4.4.0' description = "Uber's hierarchical hexagonal geospatial indexing system" readme = 'readme.md' license = {file = 'LICENSE'} From 364d03d1e2b0aaefdc33efdf7bdd7fb1b8c3f687 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Tue, 18 Nov 2025 13:19:36 -0800 Subject: [PATCH 04/23] add new error codes and some helper functions around error codes --- justfile | 3 +++ src/h3/__init__.py | 7 ++++++ src/h3/_cy/__init__.py | 7 ++++++ src/h3/_cy/error_system.pxd | 3 +++ src/h3/_cy/error_system.pyx | 37 +++++++++++++++++++++++++---- src/h3/_cy/h3lib.pxd | 5 ++++ tests/test_lib/test_error_codes.py | 38 +++++++++++++++++------------- 7 files changed, 80 insertions(+), 20 deletions(-) diff --git a/justfile b/justfile index d17ba2b5..5d3270f7 100644 --- a/justfile +++ b/justfile @@ -1 +1,4 @@ # REMOVE BEFORE LANDING +test: + make init + make test diff --git a/src/h3/__init__.py b/src/h3/__init__.py index 4f7cb32f..09d9b131 100644 --- a/src/h3/__init__.py +++ b/src/h3/__init__.py @@ -26,4 +26,11 @@ H3MemoryAllocError, H3MemoryBoundsError, H3OptionInvalidError, + H3IndexInvalidError, + H3BaseCellDomainError, + H3DigitDomainError, + H3DeletedDigitError, + + get_H3_ERROR_END, + error_code_to_exception, ) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index 627f3b97..a01dba86 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -109,4 +109,11 @@ H3MemoryAllocError, H3MemoryBoundsError, H3OptionInvalidError, + H3IndexInvalidError, + H3BaseCellDomainError, + H3DigitDomainError, + H3DeletedDigitError, + + get_H3_ERROR_END, + error_code_to_exception, ) diff --git a/src/h3/_cy/error_system.pxd b/src/h3/_cy/error_system.pxd index af8c709b..4a1e3b45 100644 --- a/src/h3/_cy/error_system.pxd +++ b/src/h3/_cy/error_system.pxd @@ -1,3 +1,6 @@ from .h3lib cimport H3Error + +cpdef error_code_to_exception(H3Error err) cdef check_for_error(H3Error err) cdef check_for_error_msg(H3Error err, str msg) +cpdef H3Error get_H3_ERROR_END() diff --git a/src/h3/_cy/error_system.pyx b/src/h3/_cy/error_system.pyx index fbdd9185..fc18f467 100644 --- a/src/h3/_cy/error_system.pyx +++ b/src/h3/_cy/error_system.pyx @@ -60,6 +60,10 @@ Summarizing, all exceptions originating from the C library inherit from - H3MemoryAllocError - H3MemoryBoundsError - H3OptionInvalidError +- H3IndexInvalidError +- H3BaseCellDomainError +- H3DigitDomainError +- H3DeletedDigitError # TODO: add tests verifying that concrete exception classes have the right error codes associated with them @@ -87,6 +91,11 @@ from .h3lib cimport ( E_MEMORY_ALLOC, E_MEMORY_BOUNDS, E_OPTION_INVALID, + E_INDEX_INVALID, + E_BASE_CELL_DOMAIN, + E_DIGIT_DOMAIN, + E_DELETED_DIGIT, + H3_ERROR_END # sentinel value ) @contextmanager @@ -169,6 +178,10 @@ with _the_error(H3ValueError) as e: class H3NotNeighborsError(e): ... class H3ResMismatchError(e): ... class H3OptionInvalidError(e): ... + class H3IndexInvalidError(e): ... + class H3BaseCellDomainError(e): ... + class H3DigitDomainError(e): ... + class H3DeletedDigitError(e): ... """ @@ -192,6 +205,10 @@ error_mapping = { E_MEMORY_ALLOC: H3MemoryAllocError, E_MEMORY_BOUNDS: H3MemoryBoundsError, E_OPTION_INVALID: H3OptionInvalidError, + E_INDEX_INVALID: H3IndexInvalidError, + E_BASE_CELL_DOMAIN: H3BaseCellDomainError, + E_DIGIT_DOMAIN: H3DigitDomainError, + E_DELETED_DIGIT: H3DeletedDigitError, } # Go back and modify the class definitions so that each concrete exception @@ -207,25 +224,37 @@ for code, ex in error_mapping.items(): # TODO: Move the helpers to util? # TODO: Unclear how/where to expose these functions. cdef/cpdef? -cdef code_to_exception(H3Error err): +cpdef error_code_to_exception(H3Error err): + """ + Return Python exception corresponding to integer error code + given via the H3ErrorCodes enum in `h3api.h.in` in the C library. + """ if err == E_SUCCESS: return None elif err in error_mapping: return error_mapping[err] else: - raise UnknownH3ErrorCode(err) + return UnknownH3ErrorCode(err) cdef check_for_error(H3Error err): - ex = code_to_exception(err) + ex = error_code_to_exception(err) if ex: raise ex +cpdef H3Error get_H3_ERROR_END(): + """ + Return integer H3_ERROR_END from the H3ErrorCodes enum + in `h3api.h.in` in the C library, which is one greater than + the last valid error code. + """ + return H3_ERROR_END + # todo: There's no easy way to do `*args` in `cdef` functions, but I'm also # not sure this even needs to be a Cython `cdef` function at all, or that # any of the other helper functions need to be in Cython. # todo: Revisit after we've played with this a bit. # todo: also: maybe the extra messages aren't that much more helpful... cdef check_for_error_msg(H3Error err, str msg): - ex = code_to_exception(err) + ex = error_code_to_exception(err) if ex: raise ex(msg) diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index e94f655b..5c911b7a 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -29,6 +29,11 @@ cdef extern from 'h3api.h': E_MEMORY_ALLOC = 13 E_MEMORY_BOUNDS = 14 E_OPTION_INVALID = 15 + E_INDEX_INVALID = 16 + E_BASE_CELL_DOMAIN = 17 + E_DIGIT_DOMAIN = 18 + E_DELETED_DIGIT = 19 + H3_ERROR_END # sentinel value ctypedef struct LatLng: double lat # in radians diff --git a/tests/test_lib/test_error_codes.py b/tests/test_lib/test_error_codes.py index 9961178d..ae0aa452 100644 --- a/tests/test_lib/test_error_codes.py +++ b/tests/test_lib/test_error_codes.py @@ -12,24 +12,30 @@ h3.H3GridNavigationError: None, h3.H3MemoryError: None, h3.H3ValueError: None, - - h3.H3FailedError: 1, - h3.H3DomainError: 2, - h3.H3LatLngDomainError: 3, - h3.H3ResDomainError: 4, - h3.H3CellInvalidError: 5, - h3.H3DirEdgeInvalidError: 6, - h3.H3UndirEdgeInvalidError: 7, - h3.H3VertexInvalidError: 8, - h3.H3PentagonError: 9, - h3.H3DuplicateInputError: 10, - h3.H3NotNeighborsError: 11, - h3.H3ResMismatchError: 12, - h3.H3MemoryAllocError: 13, - h3.H3MemoryBoundsError: 14, - h3.H3OptionInvalidError: 15, } +for e in range(1, h3.get_H3_ERROR_END()): + ex = h3.error_code_to_exception(e) + h3_exceptions[ex] = e + + +def test_num_error_codes(): + assert h3.get_H3_ERROR_END() >= 20 + assert h3.error_code_to_exception(19) == h3.H3DeletedDigitError + + # H3_ERROR_END (and beyond) shouldn't be a valid error code + code = h3.get_H3_ERROR_END() + assert isinstance( + h3.error_code_to_exception(code), + h3.UnknownH3ErrorCode + ) + + code = h3.get_H3_ERROR_END() + 1 + assert isinstance( + h3.error_code_to_exception(code), + h3.UnknownH3ErrorCode + ) + def test_error_codes_match(): """ From 988e4df46d84c674f3018109b6c920d2ae9c009a Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Tue, 18 Nov 2025 14:28:40 -0800 Subject: [PATCH 05/23] jupyter-book<2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 218b761d..4a5e3193 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ numpy = ['numpy'] test = ['pytest', 'pytest-cov', 'ruff', 'numpy'] all = [ 'h3[test]', - 'jupyter-book', + 'jupyter-book<2', 'sphinx>=7.3.3', # https://github.com/sphinx-doc/sphinx/issues/12290 'jupyterlab', 'jupyterlab-geojson', From ed7061533c984228a426cc15d52c3f1da20b5c79 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Tue, 18 Nov 2025 18:54:22 -0800 Subject: [PATCH 06/23] is_valid_index --- .gitignore | 1 + CHANGELOG.md | 2 +- src/h3/_cy/__init__.py | 1 + src/h3/_cy/cells.pxd | 1 + src/h3/_cy/cells.pyx | 8 ++++++++ src/h3/_cy/h3lib.pxd | 1 + src/h3/api/basic_int/__init__.py | 14 ++++++++++++++ tests/test_lib/test_cells_and_edges.py | 7 +++++++ 8 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ad8e4f33..cf6fcf38 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ h3c/ .cache/ h3/out/ .ruff_cache/ +_build/ # Generated C code from Cython test tests/**/*.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 77fc0912..0c769b50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ avoid adding features or APIs which do not map onto the ## Unreleased -None. +- Add new error codes and helper functions ## [4.3.1] - 2025-08-10 diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index a01dba86..c6a858ec 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -14,6 +14,7 @@ """ from .cells import ( + is_valid_index, is_valid_cell, is_pentagon, get_base_cell_number, diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index 06b35ee6..7552aded 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -1,5 +1,6 @@ from .h3lib cimport bool, int64_t, H3int +cpdef bool is_valid_index(H3int h) cpdef bool is_valid_cell(H3int h) cpdef bool is_pentagon(H3int h) cpdef int get_base_cell_number(H3int h) except -1 diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 928b665c..6aadc6ca 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -19,6 +19,14 @@ from .memory cimport ( # todo: add notes about Cython exception handling +cpdef bool is_valid_index(H3int h): + """Validates an H3 index (cell, vertex, or directed edge). + + Returns + ------- + boolean + """ + return h3lib.isValidIndex(h) == 1 # bool is a python type, so we don't need the except clause cpdef bool is_valid_cell(H3int h): diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 5c911b7a..958e2dc5 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -76,6 +76,7 @@ cdef extern from 'h3api.h': int isResClassIII(H3int h) nogil int isValidDirectedEdge(H3int edge) nogil int isValidVertex(H3int v) nogil + int isValidIndex(H3int h) nogil double degsToRads(double degrees) nogil double radsToDegs(double radians) nogil diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index 09f7deb8..bf227899 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -128,6 +128,20 @@ def average_hexagon_edge_length(res, unit='km'): return _cy.average_hexagon_edge_length(res, unit) +def is_valid_index(h): + """Validates an H3 index (cell, vertex, or directed edge). + + Returns + ------- + bool + """ + try: + h = _in_scalar(h) + return _cy.is_valid_index(h) + except (ValueError, TypeError): + return False + + def is_valid_cell(h): """ Validates an H3 cell (hexagon or pentagon). diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index f918019a..3c784e6e 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -597,3 +597,10 @@ def test_to_local_ij_self(): out = h3.cell_to_local_ij(h, h) assert out == (-858, -2766) + + +def test_is_valid_index(): + assert h3.is_valid_index('8001fffffffffff') + assert not h3.is_valid_index('8a28308280fffff') + assert not h3.is_valid_index(123) + assert not h3.is_valid_index('abcd') From 6ed3079a048a1afd42374c6d38ac8e74c1bf44cf Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 09:51:44 -0800 Subject: [PATCH 07/23] tests for is_valid_index and get_index_digit --- src/h3/_cy/__init__.py | 2 ++ src/h3/_cy/cells.pxd | 2 ++ src/h3/_cy/cells.pyx | 28 ++++++++++++++++++ src/h3/_cy/h3lib.pxd | 2 ++ src/h3/api/basic_int/__init__.py | 22 ++++++++++++++ tests/test_lib/test_cells_and_edges.py | 40 ++++++++++++++++++++++++++ 6 files changed, 96 insertions(+) diff --git a/src/h3/_cy/__init__.py b/src/h3/_cy/__init__.py index c6a858ec..70fa087c 100644 --- a/src/h3/_cy/__init__.py +++ b/src/h3/_cy/__init__.py @@ -19,6 +19,8 @@ is_pentagon, get_base_cell_number, get_resolution, + get_index_digit, + construct_cell, cell_to_parent, grid_distance, grid_disk, diff --git a/src/h3/_cy/cells.pxd b/src/h3/_cy/cells.pxd index 7552aded..1bc31b84 100644 --- a/src/h3/_cy/cells.pxd +++ b/src/h3/_cy/cells.pxd @@ -5,6 +5,8 @@ cpdef bool is_valid_cell(H3int h) cpdef bool is_pentagon(H3int h) cpdef int get_base_cell_number(H3int h) except -1 cpdef int get_resolution(H3int h) except -1 +cpdef int get_index_digit(H3int h, int res) except -1 +cpdef H3int construct_cell(int baseCellNumber, const int[:] digits) except 0 cpdef int grid_distance(H3int h1, H3int h2) except -1 cpdef H3int[:] grid_disk(H3int h, int k) cpdef H3int[:] grid_ring(H3int h, int k) diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 6aadc6ca..8ea5dfb3 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -8,6 +8,7 @@ from .util cimport ( ) from .error_system cimport ( + H3Error, check_for_error, check_for_error_msg, ) @@ -57,6 +58,33 @@ cpdef int get_resolution(H3int h) except -1: return h3lib.getResolution(h) +cpdef int get_index_digit(H3int h, int res) except -1: + cdef: + int digit + + check_cell(h) + + check_for_error( + h3lib.getIndexDigit(h, res, &digit) + ) + + return digit + +cpdef H3int construct_cell(int base_cell_number, const int[:] digits) except 0: + cdef: + H3int out + int res = len(digits) + H3Error err + + if res > 0: + err = h3lib.constructCell(res, base_cell_number, &digits[0], &out) + else: + err = h3lib.constructCell(res, base_cell_number, NULL, &out) + + check_for_error(err) + + return out + cpdef int grid_distance(H3int h1, H3int h2) except -1: """ Compute the grid distance between two cells diff --git a/src/h3/_cy/h3lib.pxd b/src/h3/_cy/h3lib.pxd index 958e2dc5..8cf74a7e 100644 --- a/src/h3/_cy/h3lib.pxd +++ b/src/h3/_cy/h3lib.pxd @@ -83,6 +83,8 @@ cdef extern from 'h3api.h': int getResolution(H3int h) nogil int getBaseCellNumber(H3int h) nogil + H3Error getIndexDigit(H3int h, int res, int *out) nogil + H3Error constructCell(int res, int baseCellNumber, const int *digits, H3int *out) nogil H3Error latLngToCell(const LatLng *g, int res, H3int *out) nogil H3Error cellToLatLng(H3int h, LatLng *) nogil diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index bf227899..a07d517e 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -1,5 +1,6 @@ # This file is **symlinked** across the APIs to ensure they are exactly the same. from typing import Literal +from array import array from ... import _cy from ..._h3shape import ( @@ -754,6 +755,27 @@ def get_base_cell_number(h): return _cy.get_base_cell_number(_in_scalar(h)) +def get_index_digit(h, res): + return _cy.get_index_digit(_in_scalar(h), res) + + +def construct_cell(base_cell_number, *digits, res=None): + if (res is not None) and (len(digits) != res): + raise ValueError('Resolution must match number of digits.') + + digits = array('i', digits) + o = _cy.construct_cell(base_cell_number, digits) + return _out_scalar(o) + + +def deconstruct_cell(h): + res = get_resolution(h) + bc = get_base_cell_number(h) + digits = [get_index_digit(h, r) for r in range(res)] + + return (bc, *digits) + + def are_neighbor_cells(h1, h2): """ Returns ``True`` if ``h1`` and ``h2`` are neighboring cells. diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index 3c784e6e..5b97f2b4 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -604,3 +604,43 @@ def test_is_valid_index(): assert not h3.is_valid_index('8a28308280fffff') assert not h3.is_valid_index(123) assert not h3.is_valid_index('abcd') + + +def test_is_valid_index_2(): + import h3.api.basic_int as h3 + + h = h3.latlng_to_cell(0,0,9) + assert h3.is_valid_index(h) + assert not h3.is_valid_index(h + 1) + assert not h3.is_valid_directed_edge(h) + assert not h3.is_valid_vertex(h) + + e = h3.origin_to_directed_edges(h)[0] + assert h3.is_valid_index(e) + assert not h3.is_valid_index(e + 1) + assert not h3.is_valid_cell(e) + assert not h3.is_valid_vertex(e) + + v = h3.cell_to_vertex(h, 0) + assert h3.is_valid_index(v) + assert not h3.is_valid_index(v + 1) + assert not h3.is_valid_cell(v) + assert not h3.is_valid_directed_edge(v) + + + +def test_get_index_digit(): + assert h3.get_index_digit('822377fffffffff', 1) == 5 + assert h3.get_index_digit('822377fffffffff', 2) == 6 + assert h3.get_index_digit('822377fffffffff', 3) == 7 + assert h3.get_index_digit('822377fffffffff', 15) == 7 + + with pytest.raises(H3ResDomainError): + h3.get_index_digit('822377fffffffff', 16) + + with pytest.raises(H3ResDomainError): + h3.get_index_digit('822377fffffffff', 0) + + with pytest.raises(H3ResDomainError): + h3.get_index_digit('822377fffffffff',-1) + From 6b267aa43e8cc46d21f7ccb21ed679aa4efd823c Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 09:52:07 -0800 Subject: [PATCH 08/23] linting --- tests/test_lib/test_cells_and_edges.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index 5b97f2b4..8fbf7477 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -628,7 +628,6 @@ def test_is_valid_index_2(): assert not h3.is_valid_directed_edge(v) - def test_get_index_digit(): assert h3.get_index_digit('822377fffffffff', 1) == 5 assert h3.get_index_digit('822377fffffffff', 2) == 6 @@ -643,4 +642,3 @@ def test_get_index_digit(): with pytest.raises(H3ResDomainError): h3.get_index_digit('822377fffffffff',-1) - From 2c58e301daa671db915e4ca8141d86b0dce4e9b4 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 10:47:07 -0800 Subject: [PATCH 09/23] good set of tests --- src/h3/api/basic_int/__init__.py | 2 +- tests/test_lib/test_cells_and_edges.py | 57 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index a07d517e..9e6f7ba4 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -771,7 +771,7 @@ def construct_cell(base_cell_number, *digits, res=None): def deconstruct_cell(h): res = get_resolution(h) bc = get_base_cell_number(h) - digits = [get_index_digit(h, r) for r in range(res)] + digits = [get_index_digit(h, r+1) for r in range(res)] return (bc, *digits) diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index 8fbf7477..67e73915 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -8,6 +8,9 @@ H3ResMismatchError, H3CellInvalidError, H3NotNeighborsError, + H3DigitDomainError, + H3BaseCellDomainError, + H3DeletedDigitError, ) @@ -642,3 +645,57 @@ def test_get_index_digit(): with pytest.raises(H3ResDomainError): h3.get_index_digit('822377fffffffff',-1) + + +def test_construct_cell(): + bc = 17 + assert h3.construct_cell(bc, 5, 6) == '822377fffffffff' + assert h3.construct_cell(bc) == '8023fffffffffff' + + digits = [0]*15 + assert h3.construct_cell(bc, *digits) == '8f2200000000000' + + with pytest.raises(H3ResDomainError): + digits = [0]*16 + h3.construct_cell(bc, *digits) + + with pytest.raises(ValueError): + digits = [1,2,3,4] + h3.construct_cell(bc, *digits, res=2) + + with pytest.raises(H3DigitDomainError): + h3.construct_cell(121, 1, 7) # 7 is not a valid digit + + with pytest.raises(H3DigitDomainError): + h3.construct_cell(121, 45) # 45 is not a valid digit + + with pytest.raises(H3DigitDomainError): + h3.construct_cell(121, -1) # -1 is not a valid digit + + with pytest.raises(H3BaseCellDomainError): + h3.construct_cell(122) # one past last base cell number + + with pytest.raises(H3DeletedDigitError): + # 4 is a pentagon base cell. first nonzero digit can't be a 1 + h3.construct_cell(4, 1) + + with pytest.raises(H3DeletedDigitError): + # 4 is a pentagon base cell. first nonzero digit can't be a 1 + h3.construct_cell(4, 0,0,0, 0, 1) + + +def test_deconstruct_cell(): + h = '822377fffffffff' + assert h3.deconstruct_cell(h) == (17, 5, 6) + +def construct_cell_inverses(): + # demonstrate functions are inverses of each other + + h = '8ff3ac688d63446' + components = (121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6) + + assert h3.construct_cell(*components) == h + assert h3.deconstruct_cell(h) == components + + assert h3.construct_cell(*h3.deconstruct_cell(h)) == h + assert h3.deconstruct_cell(h3.construct_cell(*components)) == components From 5f8fc9993cd5c329acc8e39f9c040d7b54badc3a Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 10:47:44 -0800 Subject: [PATCH 10/23] lint --- src/h3/api/basic_int/__init__.py | 2 +- tests/test_lib/test_cells_and_edges.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index 9e6f7ba4..fa5d3b04 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -771,7 +771,7 @@ def construct_cell(base_cell_number, *digits, res=None): def deconstruct_cell(h): res = get_resolution(h) bc = get_base_cell_number(h) - digits = [get_index_digit(h, r+1) for r in range(res)] + digits = [get_index_digit(h, r + 1) for r in range(res)] return (bc, *digits) diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index 67e73915..a6d3b1c1 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -652,11 +652,11 @@ def test_construct_cell(): assert h3.construct_cell(bc, 5, 6) == '822377fffffffff' assert h3.construct_cell(bc) == '8023fffffffffff' - digits = [0]*15 + digits = [0] * 15 assert h3.construct_cell(bc, *digits) == '8f2200000000000' with pytest.raises(H3ResDomainError): - digits = [0]*16 + digits = [0] * 16 h3.construct_cell(bc, *digits) with pytest.raises(ValueError): @@ -688,6 +688,7 @@ def test_deconstruct_cell(): h = '822377fffffffff' assert h3.deconstruct_cell(h) == (17, 5, 6) + def construct_cell_inverses(): # demonstrate functions are inverses of each other From 7ec82e4a8911d7b1a2241a75dffb2aff606e9977 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 10:49:20 -0800 Subject: [PATCH 11/23] thank you, coverage tests for tests! --- tests/test_lib/test_cells_and_edges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index a6d3b1c1..eec8d6b0 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -689,7 +689,7 @@ def test_deconstruct_cell(): assert h3.deconstruct_cell(h) == (17, 5, 6) -def construct_cell_inverses(): +def test_construct_cell_inverses(): # demonstrate functions are inverses of each other h = '8ff3ac688d63446' From c9e42a0a1c15ffaf817cdd8d22257a209d940a39 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 11:00:53 -0800 Subject: [PATCH 12/23] int api test --- tests/test_lib/test_cells_and_edges.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index eec8d6b0..c73d94c5 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -700,3 +700,17 @@ def test_construct_cell_inverses(): assert h3.construct_cell(*h3.deconstruct_cell(h)) == h assert h3.deconstruct_cell(h3.construct_cell(*components)) == components + + +def test_construct_cell_inverses_int_api(): + # same inverse test as above, but in int api + import h3.api.basic_int as h3 + + h = 0x8ff3ac688d63446 + components = (121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6) + + assert h3.construct_cell(*components) == h + assert h3.deconstruct_cell(h) == components + + assert h3.construct_cell(*h3.deconstruct_cell(h)) == h + assert h3.deconstruct_cell(h3.construct_cell(*components)) == components From e7fb9cc4641ff0c7c2c99936f225b7230eba2481 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 14:13:20 -0800 Subject: [PATCH 13/23] function docstrings --- docs/api_quick.md | 4 ++ src/h3/api/basic_int/__init__.py | 96 +++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/docs/api_quick.md b/docs/api_quick.md index 7b296384..656696f5 100644 --- a/docs/api_quick.md +++ b/docs/api_quick.md @@ -25,6 +25,7 @@ and should be generally aligned with the is_res_class_III is_valid_directed_edge is_valid_vertex + is_valid_index ``` ## Index representation @@ -49,6 +50,9 @@ and should be generally aligned with the get_num_cells get_resolution get_base_cell_number + get_index_digit + deconstruct_cell + construct_cell ``` ## Geographic coordinates diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index fa5d3b04..a27be4c7 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -130,7 +130,7 @@ def average_hexagon_edge_length(res, unit='km'): def is_valid_index(h): - """Validates an H3 index (cell, vertex, or directed edge). + """Validates *any* H3 index (cell, vertex, or directed edge). Returns ------- @@ -751,16 +751,82 @@ def get_base_cell_number(h): Returns ------- int + + Examples + -------- + >>> h = construct_cell(57, 2, 1, 4) + >>> h + '83728cfffffffff' + >> get_base_cell_number(h) + 57 """ return _cy.get_base_cell_number(_in_scalar(h)) def get_index_digit(h, res): + """ + Get the index digit of a cell at the given resolution. + + Parameters + ---------- + h : H3Cell + Cell whose index digit will be returned. + res : int + Resolution (``>= 1``) at which to read the digit. + + Returns + ------- + int + The index digit at the requested resolution. + + Examples + -------- + >>> h = construct_cell(7, 2, 1, 4) + >>> h + '830e8cfffffffff' + >>> get_index_digit(h, 1) + 2 + >>> get_index_digit(h, 2) + 1 + >>> get_index_digit(h, 3) + 4 + """ return _cy.get_index_digit(_in_scalar(h), res) def construct_cell(base_cell_number, *digits, res=None): + """ + Construct cell from base cell and digits. + + Parameters + ---------- + base_cell_number : int + Base cell *number* (``0`` to ``121``). + *digits : int + Sequence of index digits (``0`` to ``6``). + Length of digits will be the resulting resolution of the output cell. + res : int, optional + Resolution of the constructed cell. If provided, it must equal + ``len(digits)``; otherwise it is inferred from the number of digits. + + Returns + ------- + H3Cell + The constructed cell. + + Examples + -------- + >>> construct_cell(7, 2, 1, 4) # resolution 3 cell + '830e8cfffffffff' + + >>> construct_cell(15, 0, 0, 5, 3) # resolution 4 cell + '841e057ffffffff' + + >>> construct_cell(15, 0, 0, 5, 3, res=4) + '841e057ffffffff' + """ if (res is not None) and (len(digits) != res): + # TODO: do a res mismatch error raise ValueError('Resolution must match number of digits.') digits = array('i', digits) @@ -769,6 +835,34 @@ def construct_cell(base_cell_number, *digits, res=None): def deconstruct_cell(h): + """ + Deconstruct cell into base cell and digits. + + Parameters + ---------- + h : H3Cell + Cell to deconstruct. + + Returns + ------- + tuple + A tuple containing the base cell *number* followed by the index + digits for resolutions ``1`` through the cell's resolution. + + Examples + -------- + >>> h = construct_cell(7, 2, 1, 4) # resolution 3 cell + >>> h + '830e8cfffffffff' + >>> deconstruct_cell(h) + (7, 2, 1, 4) + + >>> h = construct_cell(15, 0, 0, 5, 3) # resolution 4 cell + >>> h + '841e057ffffffff' + >>> deconstruct_cell(h) + (15, 0, 0, 5, 3) + """ res = get_resolution(h) bc = get_base_cell_number(h) digits = [get_index_digit(h, r + 1) for r in range(res)] From dbf4c93cbacb811228d87018537cbadec1c845bc Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 14:37:42 -0800 Subject: [PATCH 14/23] maybe not --- src/h3/api/basic_int/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index a27be4c7..7b7a7221 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -826,7 +826,6 @@ def construct_cell(base_cell_number, *digits, res=None): '841e057ffffffff' """ if (res is not None) and (len(digits) != res): - # TODO: do a res mismatch error raise ValueError('Resolution must match number of digits.') digits = array('i', digits) From ec88fe68c4295bed802ab139dbd106b5ac2ad7b8 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 14:57:23 -0800 Subject: [PATCH 15/23] changelog. don't expose error code helper functions (yet) --- CHANGELOG.md | 14 +++++++++++++- src/h3/__init__.py | 3 --- tests/test_lib/test_error_codes.py | 20 ++++++++++++-------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c769b50..254c89b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,19 @@ avoid adding features or APIs which do not map onto the ## Unreleased -- Add new error codes and helper functions +- Update h3lib to v4.4.1. (#470) + - Add new error codes and error helper functions: + - `H3IndexInvalidError` + - `H3BaseCellDomainError` + - `H3DigitDomainError` + - `H3DeletedDigitError` + - `get_H3_ERROR_END` (not in public API) + - `error_code_to_exception` (not in public API) + - Add new functions: + - `is_valid_index` + - `get_index_digit` + - `construct_cell` + - `deconstruct_cell` ## [4.3.1] - 2025-08-10 diff --git a/src/h3/__init__.py b/src/h3/__init__.py index 09d9b131..1e4f4b8d 100644 --- a/src/h3/__init__.py +++ b/src/h3/__init__.py @@ -30,7 +30,4 @@ H3BaseCellDomainError, H3DigitDomainError, H3DeletedDigitError, - - get_H3_ERROR_END, - error_code_to_exception, ) diff --git a/tests/test_lib/test_error_codes.py b/tests/test_lib/test_error_codes.py index ae0aa452..4ae1d375 100644 --- a/tests/test_lib/test_error_codes.py +++ b/tests/test_lib/test_error_codes.py @@ -1,6 +1,10 @@ import pytest import h3 +from h3._cy import ( + error_code_to_exception, + get_H3_ERROR_END, +) # todo: maybe check the `check_for_error` function behavior directly? @@ -14,25 +18,25 @@ h3.H3ValueError: None, } -for e in range(1, h3.get_H3_ERROR_END()): - ex = h3.error_code_to_exception(e) +for e in range(1, get_H3_ERROR_END()): + ex = error_code_to_exception(e) h3_exceptions[ex] = e def test_num_error_codes(): - assert h3.get_H3_ERROR_END() >= 20 - assert h3.error_code_to_exception(19) == h3.H3DeletedDigitError + assert get_H3_ERROR_END() >= 20 + assert error_code_to_exception(19) == h3.H3DeletedDigitError # H3_ERROR_END (and beyond) shouldn't be a valid error code - code = h3.get_H3_ERROR_END() + code = get_H3_ERROR_END() assert isinstance( - h3.error_code_to_exception(code), + error_code_to_exception(code), h3.UnknownH3ErrorCode ) - code = h3.get_H3_ERROR_END() + 1 + code = get_H3_ERROR_END() + 1 assert isinstance( - h3.error_code_to_exception(code), + error_code_to_exception(code), h3.UnknownH3ErrorCode ) From f598ae9329981ebbc6097bf53c4af21ee5422907 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 14:58:13 -0800 Subject: [PATCH 16/23] ticks --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 254c89b2..f5a061ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ avoid adding features or APIs which do not map onto the ## Unreleased -- Update h3lib to v4.4.1. (#470) +- Update `h3lib` to v4.4.1. (#470) - Add new error codes and error helper functions: - `H3IndexInvalidError` - `H3BaseCellDomainError` From 77d3c5924c35c2e87b14d1b08d41ae0bf40e072d Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 15:14:17 -0800 Subject: [PATCH 17/23] don't even mention 'em --- CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a061ed..53497899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,13 +17,11 @@ avoid adding features or APIs which do not map onto the ## Unreleased - Update `h3lib` to v4.4.1. (#470) - - Add new error codes and error helper functions: + - Add new error codes: - `H3IndexInvalidError` - `H3BaseCellDomainError` - `H3DigitDomainError` - `H3DeletedDigitError` - - `get_H3_ERROR_END` (not in public API) - - `error_code_to_exception` (not in public API) - Add new functions: - `is_valid_index` - `get_index_digit` From d0c0b359f2dcfff424ce3dd1823440707fdc4506 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 15:37:42 -0800 Subject: [PATCH 18/23] `list` rather than `tuple`, since users are likely to mutate result --- src/h3/api/basic_int/__init__.py | 8 ++++---- tests/test_lib/test_cells_and_edges.py | 11 ++++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/h3/api/basic_int/__init__.py b/src/h3/api/basic_int/__init__.py index 7b7a7221..47f1c7ec 100644 --- a/src/h3/api/basic_int/__init__.py +++ b/src/h3/api/basic_int/__init__.py @@ -844,9 +844,8 @@ def deconstruct_cell(h): Returns ------- - tuple - A tuple containing the base cell *number* followed by the index - digits for resolutions ``1`` through the cell's resolution. + list of int + [base_cell_number, digit1, digit2, ..., digitN] Examples -------- @@ -861,12 +860,13 @@ def deconstruct_cell(h): '841e057ffffffff' >>> deconstruct_cell(h) (15, 0, 0, 5, 3) + >>> construct_cell(*deconstruct_cell(h), 0) == cell_to_center_child(h) """ res = get_resolution(h) bc = get_base_cell_number(h) digits = [get_index_digit(h, r + 1) for r in range(res)] - return (bc, *digits) + return [bc, *digits] def are_neighbor_cells(h1, h2): diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index c73d94c5..74e71baf 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -686,14 +686,14 @@ def test_construct_cell(): def test_deconstruct_cell(): h = '822377fffffffff' - assert h3.deconstruct_cell(h) == (17, 5, 6) + assert h3.deconstruct_cell(h) == [17, 5, 6] def test_construct_cell_inverses(): # demonstrate functions are inverses of each other h = '8ff3ac688d63446' - components = (121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6) + components = [121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6] assert h3.construct_cell(*components) == h assert h3.deconstruct_cell(h) == components @@ -707,10 +707,15 @@ def test_construct_cell_inverses_int_api(): import h3.api.basic_int as h3 h = 0x8ff3ac688d63446 - components = (121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6) + components = [121, 6, 5, 4, 3, 2, 1, 0, 6, 5, 4, 3, 2, 1, 0, 6] assert h3.construct_cell(*components) == h assert h3.deconstruct_cell(h) == components assert h3.construct_cell(*h3.deconstruct_cell(h)) == h assert h3.deconstruct_cell(h3.construct_cell(*components)) == components + + +def test_center_child_with_deconstruct(): + for h in h3.get_res0_cells(): + h3.construct_cell(*h3.deconstruct_cell(h), 0) == h3.cell_to_center_child(h) From db0325944ed144bf4944d140d4f7ca488878ab77 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 16:36:14 -0800 Subject: [PATCH 19/23] remove justfile --- justfile | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 justfile diff --git a/justfile b/justfile deleted file mode 100644 index 5d3270f7..00000000 --- a/justfile +++ /dev/null @@ -1,4 +0,0 @@ -# REMOVE BEFORE LANDING -test: - make init - make test From 260875ce0cb39335f59ff7dcdd69293c5545f6b8 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Wed, 19 Nov 2025 20:47:07 -0800 Subject: [PATCH 20/23] failing tests --- tests/test_lib/test_cells_and_edges.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index 74e71baf..759be171 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -647,6 +647,25 @@ def test_get_index_digit(): h3.get_index_digit('822377fffffffff',-1) +def test_get_index_digit_non_cells(): + h = h3.construct_cell(111, 3, 6, 4) + assert h == '83def4fffffffff' + + e = h3.origin_to_directed_edges(h)[0] + assert e == '113def4fffffffff' + assert h3.is_valid_directed_edge(e) + assert h3.get_index_digit(e, 1) == 3 + assert h3.get_index_digit(e, 2) == 6 + assert h3.get_index_digit(e, 3) == 4 + + v = h3.cell_to_vertexes(h)[0] + assert v == '223de1afffffffff' + assert h3.is_valid_vertex(v) + assert h3.get_index_digit(v, 1) == 3 + assert h3.get_index_digit(v, 2) == 6 + assert h3.get_index_digit(v, 3) == 4 + + def test_construct_cell(): bc = 17 assert h3.construct_cell(bc, 5, 6) == '822377fffffffff' From 1e1749d016cb1b1dbfa7a981da97afd109dc39e8 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Thu, 20 Nov 2025 10:04:19 -0800 Subject: [PATCH 21/23] tests should be passing --- src/h3/_cy/cells.pyx | 4 ++-- src/h3/_cy/util.pxd | 3 ++- src/h3/_cy/util.pyx | 16 ++++++++++++++-- tests/test_lib/test_cells_and_edges.py | 6 +++--- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/h3/_cy/cells.pyx b/src/h3/_cy/cells.pyx index 8ea5dfb3..78d25613 100644 --- a/src/h3/_cy/cells.pyx +++ b/src/h3/_cy/cells.pyx @@ -3,7 +3,7 @@ from .h3lib cimport bool, int64_t, H3int, H3ErrorCodes from .util cimport ( check_cell, - check_res, # we don't use? + check_index, check_distance, ) @@ -62,7 +62,7 @@ cpdef int get_index_digit(H3int h, int res) except -1: cdef: int digit - check_cell(h) + check_index(h) check_for_error( h3lib.getIndexDigit(h, res, &digit) diff --git a/src/h3/_cy/util.pxd b/src/h3/_cy/util.pxd index 4d7d962c..b584d3aa 100644 --- a/src/h3/_cy/util.pxd +++ b/src/h3/_cy/util.pxd @@ -8,6 +8,7 @@ cpdef H3str int_to_str(H3int x) cdef check_cell(H3int h) cdef check_edge(H3int e) -cdef check_res(int res) cdef check_vertex(H3int v) +cdef check_index(H3int h) +cdef check_res(int res) cdef check_distance(int k) diff --git a/src/h3/_cy/util.pyx b/src/h3/_cy/util.pyx index 7b8a9f20..9b070f9c 100644 --- a/src/h3/_cy/util.pyx +++ b/src/h3/_cy/util.pyx @@ -1,4 +1,11 @@ -from .h3lib cimport H3int, H3str, isValidCell, isValidDirectedEdge, isValidVertex +from .h3lib cimport ( + H3int, + H3str, + isValidCell, + isValidDirectedEdge, + isValidVertex, + isValidIndex, +) cimport h3lib @@ -7,7 +14,8 @@ from .error_system import ( H3DomainError, H3DirEdgeInvalidError, H3CellInvalidError, - H3VertexInvalidError + H3VertexInvalidError, + H3IndexInvalidError, ) cdef h3lib.LatLng deg2coord(double lat, double lng) nogil: @@ -78,6 +86,10 @@ cdef check_vertex(H3int v): if isValidVertex(v) == 0: raise H3VertexInvalidError('Integer is not a valid H3 vertex: {}'.format(hex(v))) +cdef check_index(H3int h): + if isValidIndex(h) == 0: + raise H3IndexInvalidError('Integer is not a valid H3 index: {}'.format(hex(h))) + cdef check_res(int res): if (res < 0) or (res > 15): raise H3ResDomainError(res) diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index 759be171..2a9a4c74 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -661,9 +661,9 @@ def test_get_index_digit_non_cells(): v = h3.cell_to_vertexes(h)[0] assert v == '223de1afffffffff' assert h3.is_valid_vertex(v) - assert h3.get_index_digit(v, 1) == 3 - assert h3.get_index_digit(v, 2) == 6 - assert h3.get_index_digit(v, 3) == 4 + assert h3.get_index_digit(v, 1) == 0 + assert h3.get_index_digit(v, 2) == 3 + assert h3.get_index_digit(v, 3) == 2 def test_construct_cell(): From e8c7209b0a995eff6e93bda0daeb80a81dc075a3 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Thu, 20 Nov 2025 10:09:24 -0800 Subject: [PATCH 22/23] more tests --- tests/test_lib/test_cells_and_edges.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_lib/test_cells_and_edges.py b/tests/test_lib/test_cells_and_edges.py index 2a9a4c74..c4615829 100644 --- a/tests/test_lib/test_cells_and_edges.py +++ b/tests/test_lib/test_cells_and_edges.py @@ -11,6 +11,7 @@ H3DigitDomainError, H3BaseCellDomainError, H3DeletedDigitError, + H3IndexInvalidError, ) @@ -658,6 +659,12 @@ def test_get_index_digit_non_cells(): assert h3.get_index_digit(e, 2) == 6 assert h3.get_index_digit(e, 3) == 4 + e = h3.int_to_str(1 + h3.str_to_int(e)) + assert not h3.is_valid_directed_edge(e) + assert not h3.is_valid_index(e) + with pytest.raises(H3IndexInvalidError): + h3.get_index_digit(e, 1) + v = h3.cell_to_vertexes(h)[0] assert v == '223de1afffffffff' assert h3.is_valid_vertex(v) @@ -665,6 +672,12 @@ def test_get_index_digit_non_cells(): assert h3.get_index_digit(v, 2) == 3 assert h3.get_index_digit(v, 3) == 2 + v = h3.int_to_str(1 + h3.str_to_int(v)) + assert not h3.is_valid_vertex(v) + assert not h3.is_valid_index(v) + with pytest.raises(H3IndexInvalidError): + h3.get_index_digit(v, 1) + def test_construct_cell(): bc = 17 From d2ae40d0b7af1dcb41ea7ff0f9b283206d9c6e85 Mon Sep 17 00:00:00 2001 From: AJ Friend Date: Thu, 20 Nov 2025 17:46:43 -0600 Subject: [PATCH 23/23] swap docs order --- docs/api_quick.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api_quick.md b/docs/api_quick.md index 656696f5..6694ed4a 100644 --- a/docs/api_quick.md +++ b/docs/api_quick.md @@ -51,8 +51,8 @@ and should be generally aligned with the get_resolution get_base_cell_number get_index_digit - deconstruct_cell construct_cell + deconstruct_cell ``` ## Geographic coordinates