Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion geohash.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
_base32_map = {}
for i in range(len(_base32)):
_base32_map[_base32[i]] = i
# The _geohash extension accepts upper-case base32 letters (see
# build_base32_decode_map in src/geohash_core.rs); mirror that here so the
# pure-Python fallback decodes the same codes the extension does.
if _base32[i].isalpha():
_base32_map[_base32[i].upper()] = i
del i

LONG_ZERO = 0
Expand Down Expand Up @@ -146,7 +151,12 @@ def _decode_c2i(hashcode):
lat_length = 0
lon_length = 0
for i in hashcode:
t = _base32_map[i]
try:
t = _base32_map[i]
except KeyError:
# Match the extension's error (map_error in src/lib.rs) instead of
# leaking a bare KeyError for an out-of-alphabet character.
raise ValueError("geohash code is [0123456789bcdefghjkmnpqrstuvwxyz]+") from None
if bit_length%2==0:
lon = lon<<3
lat = lat<<2
Expand Down
52 changes: 52 additions & 0 deletions tests/test_geohash.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,57 @@ def test_one(self):
self.assertEqual(set(['w', 'x', 'y', '8', 'b']), set(geohash.neighbors("z")))
self.assertEqual(set(['2', '6', '1', '0', '4', '9', '8', 'd']), set(geohash.neighbors("3")))

class TestUpperCase(unittest.TestCase):
# The _geohash extension accepts upper-case base32 letters, so the
# pure-Python fallback (used whenever the extension is not built, e.g. the
# README's "copy geohash.py" path and every non-x86_64-Linux install) must
# decode the same codes to the same result.
codes = ['ezs42', 'u4pruydqqvj', 'dr5regw3pg', 'sunny', 'bgr96qxvpd46']

def test_decode_upper_equals_lower(self):
for code in self.codes:
self.assertEqual(geohash.decode(code.upper()), geohash.decode(code))

def test_decode_exactly_upper_equals_lower(self):
for code in self.codes:
self.assertEqual(geohash.decode_exactly(code.upper()), geohash.decode_exactly(code))

def test_bbox_upper_equals_lower(self):
for code in self.codes:
self.assertEqual(geohash.bbox(code.upper()), geohash.bbox(code))

def test_neighbors_upper_equals_lower(self):
for code in self.codes:
self.assertEqual(set(geohash.neighbors(code.upper())), set(geohash.neighbors(code)))

def test_expand_upper_equals_lower(self):
for code in self.codes:
expanded = geohash.expand(code.upper())
self.assertIn(code.upper(), expanded)
self.assertEqual(set(expanded) - {code.upper()}, set(geohash.neighbors(code)))

def test_mixed_case(self):
self.assertEqual(geohash.decode('Ezs42'), geohash.decode('ezs42'))
self.assertEqual(geohash.decode('eZS42'), geohash.decode('ezs42'))

def test_every_base32_letter_uppercases(self):
for c in '0123456789bcdefghjkmnpqrstuvwxyz':
code = 'e' + c + 's'
self.assertEqual(geohash.decode(code.upper()), geohash.decode(code))

class TestInvalidCode(unittest.TestCase):
# Letters a, i, l, o (and their upper-case forms) are not in the geohash
# base32 alphabet; the extension raises ValueError for them, so the
# fallback must too rather than leaking a bare KeyError.
invalid = ['abc', 'io', 'ezs42a', 'ezs42L', 'EZS42O', 'ezs 42', 'ezs42!', 'A', 'I', 'l', 'o']

def test_invalid_char_raises_value_error(self):
for code in self.invalid:
for fn in (geohash.decode, geohash.decode_exactly, geohash.bbox,
geohash.neighbors, geohash.expand):
with self.assertRaises(ValueError):
fn(code)


if __name__=='__main__':
unittest.main()