Skip to content

Commit 2fb70a4

Browse files
author
Alexandru Popescu
committed
Add Coffee Shops Finder service
1 parent 9231f80 commit 2fb70a4

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require 'csv'
4+
require 'httparty'
5+
6+
class CoffeeShopFinderService
7+
def closest_shops(user_lat, user_lon, limit = 3)
8+
coffee_shops = fetch_and_parse_coffee_shops
9+
10+
# Used for faster distance retrieval
11+
distances = Hash.new { |h, shop| h[shop] = shop.distance_to(user_lat, user_lon) }
12+
closest_shops = coffee_shops.min_by(limit) { |shop| distances[shop] }
13+
14+
closest_shops.map { |shop| { shop: shop, distance: distances[shop] } }
15+
end
16+
17+
private
18+
19+
def fetch_and_parse_coffee_shops
20+
response = fetch_csv
21+
parse_coffee_shops(response)
22+
end
23+
24+
def parse_coffee_shops(response)
25+
CSV.parse(response.body, headers: true).map do |row|
26+
validate_csv_row!(row)
27+
CoffeeShop.new(row['Name'], row['X Coordinate'], row['Y Coordinate'])
28+
end
29+
rescue CSV::MalformedCSVError => e
30+
raise "Malformed CSV: #{e.message}"
31+
end
32+
33+
def fetch_csv
34+
url = ENV['CSV_URL'] || APP_CONFIG[:csv_url]
35+
response = HTTParty.get(url)
36+
raise "Failed to fetch CSV: #{response.code}" unless response.success?
37+
38+
response
39+
end
40+
41+
# Validate CSV row structure
42+
def validate_csv_row!(row)
43+
missing = %w[Name X Coordinate Y Coordinate].reject { |h| row[h] }
44+
raise "Invalid CSV headers: #{missing.join(', ')}" if missing.any?
45+
end
46+
end
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../../app/services/coffee_shop_finder_service'
4+
require_relative '../../app/models/coffee_shop'
5+
6+
RSpec.describe CoffeeShopFinderService do # rubocop:disable RSpec/BlockLength
7+
let(:finder) { CoffeeShopFinderService.new }
8+
9+
context '#closest_shops' do # rubocop:disable RSpec/BlockLength
10+
subject { finder.closest_shops(user_lat, user_lon) }
11+
12+
let(:user_lat) { 40.7128 }
13+
let(:user_lon) { -74.0060 }
14+
15+
let(:shop_same) { CoffeeShop.new('Same Location', user_lat, user_lon) } # 0.0 distance
16+
let(:shop_near) { CoffeeShop.new('Starbucks Seattle', 47.5869, -122.316) }
17+
let(:shop_mid) { CoffeeShop.new('Starbucks Moscow', 55.752047, 37.595242) }
18+
let(:shop_far) { CoffeeShop.new('Starbucks Sydney', -33.871843, 151.206767) }
19+
let(:all_shops) { [shop_far, shop_near, shop_mid, shop_same] }
20+
21+
before do
22+
allow_any_instance_of(CoffeeShopFinderService)
23+
.to receive(:fetch_and_parse_coffee_shops)
24+
.and_return(all_shops)
25+
end
26+
27+
describe 'limit functionality' do
28+
it 'returns the specified number of shops' do
29+
results = finder.closest_shops(user_lat, user_lon, 2)
30+
expect(results.size).to eq(2)
31+
end
32+
33+
it 'returns all shops if limit is greater than the number of shops' do
34+
results = finder.closest_shops(user_lat, user_lon, 20)
35+
expect(results.size).to eq(4)
36+
end
37+
end
38+
39+
describe 'normal functionality' do
40+
it 'returns shops in ascending order' do
41+
expect(subject).to eq(
42+
[
43+
{ shop: shop_same, distance: 0.0 },
44+
{ shop: shop_near, distance: 48.7966 },
45+
{ shop: shop_mid, distance: 112.61 }
46+
]
47+
)
48+
end
49+
end
50+
51+
describe 'edge cases' do
52+
context 'with empty shop list' do
53+
it 'returns empty array' do
54+
allow_any_instance_of(CoffeeShopFinderService)
55+
.to receive(:fetch_and_parse_coffee_shops).and_return([])
56+
57+
expect(subject).to be_empty
58+
end
59+
end
60+
61+
context 'with identical locations' do
62+
let(:dupe_shop) { CoffeeShop.new('Dupe', user_lat, user_lon) }
63+
64+
it 'returns all matching shops' do
65+
allow_any_instance_of(CoffeeShopFinderService)
66+
.to receive(:fetch_and_parse_coffee_shops).and_return([shop_same, dupe_shop])
67+
68+
expect(subject.size).to eq(2)
69+
expect(subject.map { |r| r[:distance] }).to all(eq(0.0))
70+
end
71+
end
72+
end
73+
end
74+
end

0 commit comments

Comments
 (0)