-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9231f80
commit 2fb70a4
Showing
2 changed files
with
120 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'csv' | ||
require 'httparty' | ||
|
||
class CoffeeShopFinderService | ||
def closest_shops(user_lat, user_lon, limit = 3) | ||
coffee_shops = fetch_and_parse_coffee_shops | ||
|
||
# Used for faster distance retrieval | ||
distances = Hash.new { |h, shop| h[shop] = shop.distance_to(user_lat, user_lon) } | ||
closest_shops = coffee_shops.min_by(limit) { |shop| distances[shop] } | ||
|
||
closest_shops.map { |shop| { shop: shop, distance: distances[shop] } } | ||
end | ||
|
||
private | ||
|
||
def fetch_and_parse_coffee_shops | ||
response = fetch_csv | ||
parse_coffee_shops(response) | ||
end | ||
|
||
def parse_coffee_shops(response) | ||
CSV.parse(response.body, headers: true).map do |row| | ||
validate_csv_row!(row) | ||
CoffeeShop.new(row['Name'], row['X Coordinate'], row['Y Coordinate']) | ||
end | ||
rescue CSV::MalformedCSVError => e | ||
raise "Malformed CSV: #{e.message}" | ||
end | ||
|
||
def fetch_csv | ||
url = ENV['CSV_URL'] || APP_CONFIG[:csv_url] | ||
response = HTTParty.get(url) | ||
raise "Failed to fetch CSV: #{response.code}" unless response.success? | ||
|
||
response | ||
end | ||
|
||
# Validate CSV row structure | ||
def validate_csv_row!(row) | ||
missing = %w[Name X Coordinate Y Coordinate].reject { |h| row[h] } | ||
raise "Invalid CSV headers: #{missing.join(', ')}" if missing.any? | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative '../../app/services/coffee_shop_finder_service' | ||
require_relative '../../app/models/coffee_shop' | ||
|
||
RSpec.describe CoffeeShopFinderService do # rubocop:disable RSpec/BlockLength | ||
let(:finder) { CoffeeShopFinderService.new } | ||
|
||
context '#closest_shops' do # rubocop:disable RSpec/BlockLength | ||
subject { finder.closest_shops(user_lat, user_lon) } | ||
|
||
let(:user_lat) { 40.7128 } | ||
let(:user_lon) { -74.0060 } | ||
|
||
let(:shop_same) { CoffeeShop.new('Same Location', user_lat, user_lon) } # 0.0 distance | ||
let(:shop_near) { CoffeeShop.new('Starbucks Seattle', 47.5869, -122.316) } | ||
let(:shop_mid) { CoffeeShop.new('Starbucks Moscow', 55.752047, 37.595242) } | ||
let(:shop_far) { CoffeeShop.new('Starbucks Sydney', -33.871843, 151.206767) } | ||
let(:all_shops) { [shop_far, shop_near, shop_mid, shop_same] } | ||
|
||
before do | ||
allow_any_instance_of(CoffeeShopFinderService) | ||
.to receive(:fetch_and_parse_coffee_shops) | ||
.and_return(all_shops) | ||
end | ||
|
||
describe 'limit functionality' do | ||
it 'returns the specified number of shops' do | ||
results = finder.closest_shops(user_lat, user_lon, 2) | ||
expect(results.size).to eq(2) | ||
end | ||
|
||
it 'returns all shops if limit is greater than the number of shops' do | ||
results = finder.closest_shops(user_lat, user_lon, 20) | ||
expect(results.size).to eq(4) | ||
end | ||
end | ||
|
||
describe 'normal functionality' do | ||
it 'returns shops in ascending order' do | ||
expect(subject).to eq( | ||
[ | ||
{ shop: shop_same, distance: 0.0 }, | ||
{ shop: shop_near, distance: 48.7966 }, | ||
{ shop: shop_mid, distance: 112.61 } | ||
] | ||
) | ||
end | ||
end | ||
|
||
describe 'edge cases' do | ||
context 'with empty shop list' do | ||
it 'returns empty array' do | ||
allow_any_instance_of(CoffeeShopFinderService) | ||
.to receive(:fetch_and_parse_coffee_shops).and_return([]) | ||
|
||
expect(subject).to be_empty | ||
end | ||
end | ||
|
||
context 'with identical locations' do | ||
let(:dupe_shop) { CoffeeShop.new('Dupe', user_lat, user_lon) } | ||
|
||
it 'returns all matching shops' do | ||
allow_any_instance_of(CoffeeShopFinderService) | ||
.to receive(:fetch_and_parse_coffee_shops).and_return([shop_same, dupe_shop]) | ||
|
||
expect(subject.size).to eq(2) | ||
expect(subject.map { |r| r[:distance] }).to all(eq(0.0)) | ||
end | ||
end | ||
end | ||
end | ||
end |