Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}