geohash_test.gno
7.01 Kb · 244 lines
1package geohash
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9func TestEncode(t *testing.T) {
10 tests := []struct {
11 name string
12 lat, lon float64
13 precision int
14 want string
15 }{
16 // The two reference vectors every geohash implementation is checked
17 // against; both come from the algorithm's public description.
18 {"jutland 11 chars", 57.64911, 10.40744, 11, "u4pruydqqvj"},
19 {"leon 5 chars", 42.6, -5.6, 5, "ezs42"},
20 // Null island sits on both midpoints, so every comparison takes the
21 // "greater or equal" branch.
22 {"null island", 0, 0, 5, "s0000"},
23 {"south west corner", -90, -180, 5, "00000"},
24 {"north east corner", 90, 180, 5, "zzzzz"},
25 {"precision one", 57.64911, 10.40744, 1, "u"},
26 {"shorter hash is a prefix", 57.64911, 10.40744, 4, "u4pr"},
27 }
28 for _, tt := range tests {
29 got, err := Encode(tt.lat, tt.lon, tt.precision)
30 uassert.NoError(t, err, tt.name)
31 uassert.Equal(t, tt.want, got, tt.name)
32 }
33}
34
35func TestEncodeErrors(t *testing.T) {
36 tests := []struct {
37 name string
38 lat, lon float64
39 precision int
40 }{
41 {"precision zero", 0, 0, 0},
42 {"precision negative", 0, 0, -1},
43 {"precision too large", 0, 0, MaxPrecision + 1},
44 {"latitude above range", 90.1, 0, 5},
45 {"latitude below range", -90.1, 0, 5},
46 {"longitude above range", 0, 180.1, 5},
47 {"longitude below range", 0, -180.1, 5},
48 }
49 for _, tt := range tests {
50 got, err := Encode(tt.lat, tt.lon, tt.precision)
51 uassert.Error(t, err, tt.name)
52 uassert.Equal(t, "", got, tt.name)
53 }
54}
55
56func TestDecodeBox(t *testing.T) {
57 // The cell the Leon vector lands in. Every bound is an exact binary
58 // fraction, which is why they can be compared literally.
59 b, err := DecodeBox("ezs42")
60 uassert.NoError(t, err)
61 uassert.Equal(t, 42.5830078125, b.MinLat)
62 uassert.Equal(t, 42.626953125, b.MaxLat)
63 uassert.Equal(t, -5.625, b.MinLon)
64 uassert.Equal(t, -5.5810546875, b.MaxLon)
65
66 uassert.True(t, b.Contains(42.6, -5.6), "contains its own point")
67 uassert.False(t, b.Contains(0, 0), "does not contain null island")
68
69 lat, lon := b.Center()
70 uassert.Equal(t, 42.60498046875, lat)
71 uassert.Equal(t, -5.60302734375, lon)
72}
73
74func TestDecodeIsTheBoxCenter(t *testing.T) {
75 lat, lon, err := Decode("ezs42")
76 uassert.NoError(t, err)
77 b, err := DecodeBox("ezs42")
78 uassert.NoError(t, err)
79 clat, clon := b.Center()
80 uassert.Equal(t, clat, lat)
81 uassert.Equal(t, clon, lon)
82}
83
84func TestRoundTrip(t *testing.T) {
85 // Decoding a hash and re-encoding its centre at the same length gives the
86 // hash back: the centre can never fall in a neighbouring cell.
87 hashes := []string{"u", "u4", "u4pr", "ezs42", "s0000", "00000", "zzzzz", "u4pruydqqvj"}
88 for _, h := range hashes {
89 lat, lon, err := Decode(h)
90 uassert.NoError(t, err, h)
91 got, err := Encode(lat, lon, len(h))
92 uassert.NoError(t, err, h)
93 uassert.Equal(t, h, got, h)
94 }
95}
96
97func TestDecodeErrors(t *testing.T) {
98 // The alphabet drops a, i, l and o on purpose; anything outside it, and an
99 // over-long hash, are refused rather than silently truncated.
100 for _, h := range []string{"", "a", "i", "l", "o", "ezs4A", "ezs4 ", "u4pruydqqvjxx"} {
101 _, _, err := Decode(h)
102 uassert.Error(t, err, h)
103 }
104}
105
106func TestNeighborIsReversible(t *testing.T) {
107 // A cell well inside the grid: stepping away and back is the identity, in
108 // all eight directions.
109 const h = "ezs42"
110 for _, d := range []Direction{North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest} {
111 n, err := Neighbor(h, d)
112 uassert.NoError(t, err, d.String())
113 uassert.NotEqual(t, h, n, d.String())
114 back, err := Neighbor(n, opposite(d))
115 uassert.NoError(t, err, d.String())
116 uassert.Equal(t, h, back, d.String())
117 }
118}
119
120func TestNeighborKnownCells(t *testing.T) {
121 // Cross-checked against the classic border/neighbour lookup tables, which
122 // are a completely different way of computing the same answer.
123 tests := []struct {
124 dir Direction
125 want string
126 }{
127 {North, "ezs48"},
128 {South, "ezs40"},
129 {East, "ezs43"},
130 {West, "ezefr"},
131 {NorthEast, "ezs49"},
132 {SouthWest, "ezefp"},
133 }
134 for _, tt := range tests {
135 got, err := Neighbor("ezs42", tt.dir)
136 uassert.NoError(t, err, tt.dir.String())
137 uassert.Equal(t, tt.want, got, tt.dir.String())
138 }
139}
140
141func TestNeighborWrapsLongitude(t *testing.T) {
142 // The antimeridian is not an edge of the world: the column east of the
143 // last one is the first column again.
144 east, err := Neighbor("z", East)
145 uassert.NoError(t, err)
146 uassert.Equal(t, "b", east)
147
148 west, err := Neighbor("b", West)
149 uassert.NoError(t, err)
150 uassert.Equal(t, "z", west)
151}
152
153func TestNeighborStopsAtThePoles(t *testing.T) {
154 // Latitude does not wrap: there is nothing north of the top row.
155 _, err := Neighbor("z", North)
156 uassert.ErrorIs(t, err, ErrNoNeighbor)
157
158 _, err = Neighbor("0", South)
159 uassert.ErrorIs(t, err, ErrNoNeighbor)
160
161 // The same cell still has neighbours sideways and downwards.
162 got, err := Neighbor("z", South)
163 uassert.NoError(t, err)
164 uassert.Equal(t, "x", got)
165}
166
167func TestNeighborErrors(t *testing.T) {
168 _, err := Neighbor("", North)
169 uassert.Error(t, err)
170
171 _, err = Neighbor("ezs4a", North)
172 uassert.Error(t, err)
173
174 _, err = Neighbor("ezs42", Direction(42))
175 uassert.ErrorIs(t, err, ErrDirection)
176}
177
178func TestNeighbors(t *testing.T) {
179 got, err := Neighbors("ezs42")
180 uassert.NoError(t, err)
181 uassert.Equal(t, 8, len(got))
182 uassert.Equal(t, "ezs48", got[0]) // North
183 uassert.Equal(t, "ezs43", got[2]) // East
184 uassert.Equal(t, "ezs40", got[4]) // South
185 uassert.Equal(t, "ezefr", got[6]) // West
186
187 // Unique: a repeat would mean the stepping collapsed two directions.
188 seen := map[string]bool{}
189 for _, n := range got {
190 uassert.False(t, seen[n], n)
191 seen[n] = true
192 }
193
194 // At the pole the row above is missing, and the list is short rather than
195 // carrying empty strings a caller would have to filter.
196 got, err = Neighbors("z")
197 uassert.NoError(t, err)
198 uassert.Equal(t, 5, len(got))
199 for _, n := range got {
200 uassert.NotEqual(t, "", n)
201 }
202}
203
204func TestDirectionString(t *testing.T) {
205 tests := []struct {
206 d Direction
207 want string
208 }{
209 {North, "north"},
210 {NorthEast, "north-east"},
211 {East, "east"},
212 {SouthEast, "south-east"},
213 {South, "south"},
214 {SouthWest, "south-west"},
215 {West, "west"},
216 {NorthWest, "north-west"},
217 {Direction(42), "unknown"},
218 }
219 for _, tt := range tests {
220 uassert.Equal(t, tt.want, tt.d.String())
221 }
222}
223
224func TestBoxDimensions(t *testing.T) {
225 b, err := DecodeBox("ezs42")
226 uassert.NoError(t, err)
227 // Five characters is 25 bits: 13 go to longitude, 12 to latitude, and the
228 // two halves of the world differ by exactly that factor of two, so the
229 // cell comes out square in degrees.
230 uassert.Equal(t, 180.0/4096.0, b.HeightDeg())
231 uassert.Equal(t, 360.0/8192.0, b.WidthDeg())
232
233 // One more character is five more bits, so the cell is 32 times smaller.
234 b6, err := DecodeBox("ezs42d")
235 uassert.NoError(t, err)
236 uassert.Equal(t, b.HeightDeg()/8, b6.HeightDeg())
237 uassert.Equal(t, b.WidthDeg()/4, b6.WidthDeg())
238}
239
240// opposite is the test's own inverse of a direction, kept here rather than
241// exported so the package surface stays the part callers need.
242func opposite(d Direction) Direction {
243 return Direction((int(d) + 4) % 8)
244}