require "fakeredis/geo_set" module FakeRedis module GeoCommands DISTANCE_UNITS = { "m" => 1, "km" => 1000, "ft" => 0.3048, "mi" => 1609.34 } REDIS_DOUBLE_PRECISION = 4 REDIS_GEOHASH_SIZE = 10 def geoadd(key, *members) raise_argument_error("geoadd") if members.empty? || members.size % 3 != 0 set = (data[key] ||= GeoSet.new) prev_size = set.size members.each_slice(3) do |member| set.add(*member) end set.size - prev_size end def geodist(key, member1, member2, unit = "m") unit = unit.to_s raise_command_error("ERR unsupported unit provided. please use #{DISTANCE_UNITS.keys.join(', ')}") unless DISTANCE_UNITS.include?(unit) set = (data[key] || GeoSet.new) point1 = set.get(member1) point2 = set.get(member2) if point1 && point2 distance = point1.distance_to(point2) distance_in_units = distance / DISTANCE_UNITS[unit] distance_in_units.round(REDIS_DOUBLE_PRECISION).to_s end end def geohash(key, member) members = Array(member) raise_argument_error("geohash") if members.empty? set = (data[key] || GeoSet.new) members.map do |member| point = set.get(member) point.geohash(REDIS_GEOHASH_SIZE) if point end end def geopos(key, member) return nil unless data[key] members = Array(member) set = (data[key] || GeoSet.new) members.map do |member| point = set.get(member) [point.lon.to_s, point.lat.to_s] if point end end def georadius(*args) args = args.dup raise_argument_error("georadius") if args.size < 5 key, lon, lat, radius, unit, *rest = args raise_argument_error("georadius") unless DISTANCE_UNITS.has_key?(unit) radius *= DISTANCE_UNITS[unit] set = (data[key] || GeoSet.new) center = GeoSet::Point.new(lon, lat, nil) do_georadius(set, center, radius, unit, rest) end def georadiusbymember(*args) args = args.dup raise_argument_error("georadiusbymember") if args.size < 4 key, member, radius, unit, *_ = args raise_argument_error("georadiusbymember") unless DISTANCE_UNITS.has_key?(unit) radius *= DISTANCE_UNITS[unit] set = (data[key] || GeoSet.new) center = set.get(member) raise_command_error("ERR could not decode requested zset member") unless center do_georadius(set, center, radius, unit, args) end private def do_georadius(set, center, radius, unit, args) points = set.points_within_radius(center, radius) options = georadius_options(args) if options[:asc] points.sort_by! { |p| p.distance_to(center) } elsif options[:desc] points.sort_by! { |p| -p.distance_to(center) } end points = points.take(options[:count]) if options[:count] extras = options[:extras] return points.map(&:name) if extras.empty? points.map do |point| member = [point.name] extras.each do |extra| case extra when "WITHCOORD" member << [point.lon.to_s, point.lat.to_s] when "WITHDIST" distance = point.distance_to(center) distance_in_units = distance / DISTANCE_UNITS[unit] member << distance_in_units.round(REDIS_DOUBLE_PRECISION).to_s when "WITHHASH" member << point.geohash(REDIS_GEOHASH_SIZE) end end member end end def georadius_options(args) options = {} args = args.map { |arg| arg.to_s.upcase } if idx = args.index("COUNT") options[:count] = Integer(args[idx + 1]) end options[:asc] = true if args.include?("ASC") options[:desc] = true if args.include?("DESC") extras = args & ["WITHCOORD", "WITHDIST", "WITHHASH"] options[:extras] = extras options end end end