Skip to content

Commit bf9179b

Browse files
authored
Merge pull request #131 from kyeett/add-georadiusbymember
add command GEORADIUSBYMEMBER
2 parents dea7631 + b0442e6 commit bf9179b

File tree

3 files changed

+519
-0
lines changed

3 files changed

+519
-0
lines changed

cmd_geo.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ func commandsGeo(m *Miniredis) {
1818
m.srv.Register("GEOPOS", m.cmdGeopos)
1919
m.srv.Register("GEORADIUS", m.cmdGeoradius)
2020
m.srv.Register("GEORADIUS_RO", m.cmdGeoradius)
21+
m.srv.Register("GEORADIUSBYMEMBER", m.cmdGeoradiusbymember)
22+
m.srv.Register("GEORADIUSBYMEMBER_RO", m.cmdGeoradiusbymember)
2123
}
2224

2325
// GEOADD
@@ -370,6 +372,200 @@ func (m *Miniredis) cmdGeoradius(c *server.Peer, cmd string, args []string) {
370372
})
371373
}
372374

375+
// GEORADIUSBYMEMBER and GEORADIUSBYMEMBER_RO
376+
func (m *Miniredis) cmdGeoradiusbymember(c *server.Peer, cmd string, args []string) {
377+
if len(args) < 4 {
378+
setDirty(c)
379+
c.WriteError(errWrongNumber(cmd))
380+
return
381+
}
382+
if !m.handleAuth(c) {
383+
return
384+
}
385+
if m.checkPubsub(c) {
386+
return
387+
}
388+
389+
key := args[0]
390+
member := args[1]
391+
392+
radius, err := strconv.ParseFloat(args[2], 64)
393+
if err != nil || radius < 0 {
394+
setDirty(c)
395+
c.WriteError(errWrongNumber(cmd))
396+
return
397+
}
398+
toMeter := parseUnit(args[3])
399+
if toMeter == 0 {
400+
setDirty(c)
401+
c.WriteError(errWrongNumber(cmd))
402+
return
403+
}
404+
args = args[4:]
405+
406+
var (
407+
withDist = false
408+
withCoord = false
409+
direction = unsorted
410+
count = 0
411+
withStore = false
412+
storeKey = ""
413+
withStoredist = false
414+
storedistKey = ""
415+
)
416+
for len(args) > 0 {
417+
arg := args[0]
418+
args = args[1:]
419+
switch strings.ToUpper(arg) {
420+
case "WITHCOORD":
421+
withCoord = true
422+
case "WITHDIST":
423+
withDist = true
424+
case "ASC":
425+
direction = asc
426+
case "DESC":
427+
direction = desc
428+
case "COUNT":
429+
if len(args) == 0 {
430+
setDirty(c)
431+
c.WriteError("ERR syntax error")
432+
return
433+
}
434+
n, err := strconv.Atoi(args[0])
435+
if err != nil {
436+
setDirty(c)
437+
c.WriteError(msgInvalidInt)
438+
return
439+
}
440+
if n <= 0 {
441+
setDirty(c)
442+
c.WriteError("ERR COUNT must be > 0")
443+
return
444+
}
445+
args = args[1:]
446+
count = n
447+
case "STORE":
448+
if len(args) == 0 {
449+
setDirty(c)
450+
c.WriteError("ERR syntax error")
451+
return
452+
}
453+
withStore = true
454+
storeKey = args[0]
455+
args = args[1:]
456+
case "STOREDIST":
457+
if len(args) == 0 {
458+
setDirty(c)
459+
c.WriteError("ERR syntax error")
460+
return
461+
}
462+
withStoredist = true
463+
storedistKey = args[0]
464+
args = args[1:]
465+
default:
466+
setDirty(c)
467+
c.WriteError("ERR syntax error")
468+
return
469+
}
470+
}
471+
472+
if strings.ToUpper(cmd) == "GEORADIUSBYMEMBER_RO" && (withStore || withStoredist) {
473+
setDirty(c)
474+
c.WriteError("ERR syntax error")
475+
return
476+
}
477+
478+
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
479+
if (withStore || withStoredist) && (withDist || withCoord) {
480+
c.WriteError("ERR STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options")
481+
return
482+
}
483+
484+
db := m.db(ctx.selectedDB)
485+
if !db.exists(key) {
486+
c.WriteNull()
487+
return
488+
}
489+
490+
if db.t(key) != "zset" {
491+
c.WriteError(ErrWrongType.Error())
492+
return
493+
}
494+
495+
// get position of member
496+
if !db.ssetExists(key, member) {
497+
c.WriteError("ERR could not decode requested zset member")
498+
return
499+
}
500+
score := db.ssetScore(key, member)
501+
longitude, latitude := fromGeohash(uint64(score))
502+
503+
members := db.ssetElements(key)
504+
matches := withinRadius(members, longitude, latitude, radius*toMeter)
505+
506+
// deal with ASC/DESC
507+
if direction != unsorted {
508+
sort.Slice(matches, func(i, j int) bool {
509+
if direction == desc {
510+
return matches[i].Distance > matches[j].Distance
511+
}
512+
return matches[i].Distance < matches[j].Distance
513+
})
514+
}
515+
516+
// deal with COUNT
517+
if count > 0 && len(matches) > count {
518+
matches = matches[:count]
519+
}
520+
521+
// deal with "STORE x"
522+
if withStore {
523+
db.del(storeKey, true)
524+
for _, member := range matches {
525+
db.ssetAdd(storeKey, member.Score, member.Name)
526+
}
527+
c.WriteInt(len(matches))
528+
return
529+
}
530+
531+
// deal with "STOREDIST x"
532+
if withStoredist {
533+
db.del(storedistKey, true)
534+
for _, member := range matches {
535+
db.ssetAdd(storedistKey, member.Distance/toMeter, member.Name)
536+
}
537+
c.WriteInt(len(matches))
538+
return
539+
}
540+
541+
c.WriteLen(len(matches))
542+
for _, member := range matches {
543+
if !withDist && !withCoord {
544+
c.WriteBulk(member.Name)
545+
continue
546+
}
547+
548+
len := 1
549+
if withDist {
550+
len++
551+
}
552+
if withCoord {
553+
len++
554+
}
555+
c.WriteLen(len)
556+
c.WriteBulk(member.Name)
557+
if withDist {
558+
c.WriteBulk(fmt.Sprintf("%.4f", member.Distance/toMeter))
559+
}
560+
if withCoord {
561+
c.WriteLen(2)
562+
c.WriteBulk(fmt.Sprintf("%f", member.Longitude))
563+
c.WriteBulk(fmt.Sprintf("%f", member.Latitude))
564+
}
565+
}
566+
})
567+
}
568+
373569
func withinRadius(members []ssElem, longitude, latitude, radius float64) []geoDistance {
374570
matches := []geoDistance{}
375571
for _, el := range members {

cmd_geo_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,160 @@ func TestGeodist(t *testing.T) {
289289
mustFail(t, err, "WRONGTYPE Operation against a key holding the wrong kind of value")
290290
})
291291
}
292+
293+
// Test GEOADD / GEORADIUSBYMEMBER / GEORADIUSBYMEMBER_RO
294+
func TestGeobymember(t *testing.T) {
295+
s, err := Run()
296+
ok(t, err)
297+
defer s.Close()
298+
c, err := redis.Dial("tcp", s.Addr())
299+
ok(t, err)
300+
defer c.Close()
301+
302+
_, err = c.Do("GEOADD", "Sicily", 13.361389, 38.115556, "Palermo")
303+
ok(t, err)
304+
_, err = c.Do("GEOADD", "Sicily", 15.087269, 37.502669, "Catania")
305+
ok(t, err)
306+
307+
t.Run("WITHDIST WITHCOORD", func(t *testing.T) {
308+
res, err := redis.Values(c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "WITHDIST", "WITHCOORD"))
309+
ok(t, err)
310+
equals(t, 2, len(res))
311+
for _, loc := range res {
312+
item := loc.([]interface{})
313+
var (
314+
name, _ = redis.String(item[0], nil)
315+
coord, _ = redis.Float64s(item[2], nil)
316+
)
317+
if name != "Catania" && name != "Palermo" {
318+
t.Errorf("unexpected name %q", name)
319+
}
320+
321+
equals(t, 2, len(coord))
322+
if coord[0] == 0.00 || coord[1] == 0.00 {
323+
t.Errorf("latitude/longitude shouldn't be empty")
324+
}
325+
}
326+
})
327+
328+
t.Run("WITHCOORD", func(t *testing.T) {
329+
res, err := redis.Values(c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "WITHCOORD"))
330+
ok(t, err)
331+
equals(t, 2, len(res))
332+
for _, loc := range res {
333+
item := loc.([]interface{})
334+
var (
335+
name, _ = redis.String(item[0], nil)
336+
coord, _ = redis.Float64s(item[1], nil)
337+
)
338+
equals(t, 2, len(item))
339+
if name != "Catania" && name != "Palermo" {
340+
t.Errorf("unexpected name %q", name)
341+
}
342+
equals(t, 2, len(coord))
343+
if coord[0] == 0.00 || coord[1] == 0.00 {
344+
t.Errorf("latitude/longitude shouldn't be empty")
345+
}
346+
}
347+
})
348+
349+
t.Run("WITHDIST", func(t *testing.T) {
350+
// in km
351+
res, err := redis.Values(c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "WITHDIST"))
352+
ok(t, err)
353+
equals(t, 2, len(res))
354+
var (
355+
name1, name2 string
356+
dist1, dist2 float64
357+
)
358+
leftover, err := redis.Scan(res[0].([]interface{}), &name1, &dist1)
359+
ok(t, err)
360+
equals(t, 0, len(leftover))
361+
equals(t, "Palermo", name1)
362+
equals(t, 0.0, dist1) // in km
363+
_, err = redis.Scan(res[1].([]interface{}), &name2, &dist2)
364+
ok(t, err)
365+
equals(t, "Catania", name2)
366+
equals(t, 166.2742, dist2)
367+
368+
// in meter
369+
res, err = redis.Values(c.Do("GEORADIUSBYMEMBER", "Sicily", "Catania", 200000, "m", "WITHDIST"))
370+
ok(t, err)
371+
equals(t, 2, len(res))
372+
distance, err := redis.Float64(res[0].([]interface{})[1], nil)
373+
ok(t, err)
374+
equals(t, 166274.1514, distance) // in meter
375+
})
376+
377+
t.Run("ASC DESC", func(t *testing.T) {
378+
asc, err := redis.Strings(c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "ASC"))
379+
ok(t, err)
380+
equals(t, []string{"Palermo", "Catania"}, asc)
381+
382+
asc2, err := redis.Strings(c.Do("GEORADIUSBYMEMBER", "Sicily", "Catania", 200, "km", "ASC"))
383+
ok(t, err)
384+
equals(t, []string{"Catania", "Palermo"}, asc2)
385+
386+
desc, err := redis.Strings(c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "DESC"))
387+
ok(t, err)
388+
equals(t, []string{"Catania", "Palermo"}, desc)
389+
})
390+
391+
t.Run("COUNT", func(t *testing.T) {
392+
count1, err := redis.Strings(c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "ASC", "COUNT", 1))
393+
ok(t, err)
394+
equals(t, []string{"Palermo"}, count1)
395+
396+
count99, err := redis.Strings(c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "ASC", "COUNT", 99))
397+
ok(t, err)
398+
equals(t, []string{"Palermo", "Catania"}, count99)
399+
400+
_, err = c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "COUNT")
401+
mustFail(t, err, "ERR syntax error")
402+
403+
_, err = c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "COUNT", "notanumber")
404+
mustFail(t, err, msgInvalidInt)
405+
406+
_, err = c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km", "COUNT", -12)
407+
mustFail(t, err, "ERR COUNT must be > 0")
408+
})
409+
410+
t.Run("no args", func(t *testing.T) {
411+
res, err := redis.Strings(c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "km"))
412+
ok(t, err)
413+
equals(t, 2, len(res))
414+
equals(t, []string{"Palermo", "Catania"}, res)
415+
416+
// Wrong map key
417+
n, err := c.Do("GEORADIUSBYMEMBER", "Capri", "Palermo", 200, "km")
418+
ok(t, err)
419+
equals(t, nil, n)
420+
421+
// Missing member
422+
res, err = redis.Strings(c.Do("GEORADIUSBYMEMBER", "Sicily", "nosuch", 200, "km"))
423+
mustFail(t, err, "ERR could not decode requested zset member")
424+
equals(t, 0, len(res))
425+
426+
// Unsupported/unknown distance unit
427+
res, err = redis.Strings(c.Do("GEORADIUSBYMEMBER", "Sicily", "Palermo", 200, "mm"))
428+
mustFail(t, err, "ERR wrong number of arguments for 'georadiusbymember' command")
429+
equals(t, 0, len(res))
430+
431+
// Wrong parameter type
432+
res, err = redis.Strings(c.Do("GEORADIUSBYMEMBER", "Sicily", "abc", "def", "ghi", "m"))
433+
mustFail(t, err, "ERR wrong number of arguments for 'georadiusbymember' command")
434+
equals(t, 0, len(res))
435+
})
436+
437+
t.Run("GEORADIUSBYMEMBER_RO", func(t *testing.T) {
438+
asc, err := redis.Strings(c.Do("GEORADIUSBYMEMBER_RO", "Sicily", "Palermo", 200, "km", "ASC"))
439+
ok(t, err)
440+
equals(t, []string{"Palermo", "Catania"}, asc)
441+
442+
_, err = c.Do("GEORADIUSBYMEMBER_RO", "Sicily", "Palermo", 200, "km", "STORE", "foo")
443+
mustFail(t, err, "ERR syntax error")
444+
445+
_, err = c.Do("GEORADIUSBYMEMBER_RO", "Sicily", "Palermo", 200, "km", "STOREDIST", "foo")
446+
mustFail(t, err, "ERR syntax error")
447+
})
448+
}

0 commit comments

Comments
 (0)