diff --git a/lib/calculates_route.rb b/lib/calculates_route.rb index 4488393..98a093c 100644 --- a/lib/calculates_route.rb +++ b/lib/calculates_route.rb @@ -1,11 +1,10 @@ class CalculatesRoute def self.calculate(points) - - remaining_points = points + remaining_points = points.clone route = [] route << remaining_points.slice!(0) - until remaining_points == [] do + until remaining_points == [] do next_point = shortest_distance(route.last, remaining_points) route << remaining_points.slice!(remaining_points.index(next_point)) end @@ -18,5 +17,17 @@ def self.shortest_distance(from, possible) end distances.sort{|a,b| a.fetch(:distance) <=> b.fetch(:distance)}.first.fetch(:point) end + + def self.distance(route) + total_distance = 0.0 + route.each_cons(2) do |route_combo| + total_distance += Map.distance_between(route.first,route.last) + end + return total_distance + end + + def self.time(route,speed) + (distance(route) / speed).to_f + end end diff --git a/lib/map.rb b/lib/map.rb index e9abfde..51b04d3 100644 --- a/lib/map.rb +++ b/lib/map.rb @@ -1,4 +1,5 @@ require 'geocoder' + class Map def self.search(terms) diff --git a/lib/place.rb b/lib/place.rb index 99eaadf..35f80d5 100644 --- a/lib/place.rb +++ b/lib/place.rb @@ -1,13 +1,14 @@ require_relative "./map" class Place - attr_accessor :name, :coordinates + attr_accessor :name, :coordinates, :starting_point - def self.build(name) + def self.build(name, starting_point = false) results = Map.search(name) Place.new.tap do |p| p.name = name p.coordinates = results.coordinates + p.starting_point ||= starting_point end end diff --git a/lib/sales_person.rb b/lib/sales_person.rb index d0c2890..b5bb7f2 100644 --- a/lib/sales_person.rb +++ b/lib/sales_person.rb @@ -1,15 +1,37 @@ class SalesPerson - attr_reader :cities + MILES_PER_HOUR = 55 + attr_reader :cities, :routed_cities, :unrouted_cities def initialize @cities = [] + @unrouted_cities = [] + @routed_cities = [] end def schedule_city(city) - @cities << city unless @cities.include?(city) + [unrouted_cities, cities].each {|a| a << city} unless cities.include?(city) + starting_city = city if city.starting_point + cities.reject{|c| c.starting_point} + cities.unshift(starting_city) unless (cities.include?(starting_city) || starting_city.nil?) end def route - CalculatesRoute.calculate(cities) + @routed_cities = CalculatesRoute.calculate(cities) + end + + def total_miles + CalculatesRoute.distance(routed_cities) + end + + def total_travel_time + CalculatesRoute.time(routed_cities, MILES_PER_HOUR) + end + + def formatted_total_travel_time + t = total_travel_time * 60 * 60 + mm, ss = t.divmod(60) + hh, mm = mm.divmod(60) + dd, hh = hh.divmod(24) + "%d days, %d hours, %d minutes and %d seconds" % [dd, hh, mm, ss] end end diff --git a/sales_person_bm.rb b/sales_person_bm.rb new file mode 100644 index 0000000..e581698 --- /dev/null +++ b/sales_person_bm.rb @@ -0,0 +1,21 @@ +require 'benchmark' + +Dir["./lib/*.rb"].each {|file| require file } + +def run_route(n) + salesperson = SalesPerson.new + + all_cities = ["Abilene", "Alamo", "Alamo Heights", "Alice", "Allen", "Alpine", "Alvarado", "Alvin", "Amarillo", "Andrews", "Angleton", "Argyle", "Arlington", "Austin", "Azle", "Balch Springs", "Bastrop", "Bay City", "Baytown", "Beaumont", "Bellaire", "Belton", "Benbrook", "Big Spring", "Boerne", "Brenham", "Bridgeport", "Brownfield", "Brownsville", "Brownwood", "Bryan", "Bulverde", "Burkburnett", "Burleson", "Burnet", "Canyon", "Carrollton", "Carthage", "Cedar Hill", "Cedar Park", "Cleburne", "Clute", "College Station", "Colleyville", "Columbus", "Conroe", "Converse", "Coppell", "Copperas Cove", "Corinth", "Corpus Christi", "Corsicana", "Crockett", "Crowley", "Dallas", "Deer Park", "Del Rio", "Denison", "Denton", "Desoto", "Devine", "Dickinson", "Donna", "Dumas", "Duncanville", "Eagle Pass", "Edinburg", "El Campo", "El Paso", "Ennis", "Euless", "Farmers Branch", "Flower Mound", "Fort Stockton", "Fort Worth", "Fredericksburg", "Freeport", "Friendswood", "Frisco", "Gainesville", "Galveston", "Garden Ridge", "Garland", "Gatesville", "Georgetown", "Gonzales", "Granbury", "Grand Prairie", "Granite Shoals", "Grapevine", "Greenville", "Haltom City", "Hamilton", "Harker Heights", "Harlingen", "Hearne", "Henderson", "Hewitt", "Highland Park", "Highland Village", "Houston", "Humble", "Hunters Creek Village", "Huntsville", "Hurst", "Irving", "Jacksonville", "Jasper", "Jersey Village", "Katy", "Keller", "Kemah", "Kennedale", "Kerrville", "Killeen", "Kingsville", "Krum", "Kyle", "La Grange", "La Porte", "Lacy Lakeview", "Lago Vista", "Lake Jackson", "Lakeway", "Lamesa", "Lampasas", "Lancaster", "Laredo", "League City", "Leander", "Leon Valley", "Levelland", "Lewisville", "Littlefield", "Live Oak", "Lockhart", "Longview", "Lowry Crossing", "Lubbock", "Lucas", "Lufkin", "Mansfield", "Manvel", "Marble Falls", "Marshall", "McAllen", "McKinney", "Mesquite", "Midland", "Midlothian", "Mission", "Missouri City", "Mount Pleasant", "Muleshoe", "Murphy", "Nacogdoches", "Nassau Bay", "New Braunfels", "Newark", "Newton", "North Richland Hills", "Oak Ridge North", "Odessa", "Orange", "Overton", "Palacios", "Palestine", "Pampa", "Paris", "Pasadena", "Pearland", "Perryton", "Pflugerville", "Plainview", "Plano", "Port Aransas", "Port Arthur", "Port Lavaca", "Portland", "Richardson", "Richland Hills", "Ridge City", "Rio Grande City", "River Oaks", "Rockport", "Rockwall", "Rosenberg", "Round Rock", "Rowlett", "Royse City", "Rusk", "Sachse", "Saginaw", "San Angelo", "San Antonio", "San Benito", "San Marcos", "San Saba", "Santa Fe", "Schertz", "Seabrook", "Sealy", "Seguin", "Selma", "Seymour", "Shenandoah", "Sherman", "Smithville", "Snyder", "Socorro", "Sonora", "Southlake", "Southside Place", "Spring Valley", "Stafford", "Stephenville", "Sugar Land", "Sulphur Springs", "Sweeny", "Tahoka", "Taylor", "Temple", "Terrell", "Texarkana", "Texas City", "The Colony", "The Woodlands", "Tomball", "Tyler", "Universal City", "University Park", "Victoria", "Waco", "Watauga", "Waxahachie", "Weatherford", "Webster", "Weslaco", "West Columbia", "West Lake Hills", "West Orange", "West University Place", "Weston", "Wharton", "White Settlement", "Wichita Falls", "Willow Park", "Windcrest", "Woodway", "Wylie", "Yoakum"] + + all_cities.shuffle.take(n).each do |city| + salesperson.schedule_city(Place.build("#{city}, TX")) + end +end + + +Benchmark.bm do |x| + x.report("Benchmarking 2 city route:"){ run_route(2) } + x.report("Benchmarking 10 city route:"){ run_route(10) } + x.report("Benchmarking 50 city route:"){ run_route(50) } + x.report("Benchmarking 200 city route:"){ run_route(100) } +end diff --git a/salesperson.rb b/salesperson.rb index 9cafbd7..41c2ce3 100644 --- a/salesperson.rb +++ b/salesperson.rb @@ -4,7 +4,16 @@ phil = SalesPerson.new phil.schedule_city(Place.build("Dallas, TX")) phil.schedule_city(Place.build("El Paso, TX")) -phil.schedule_city(Place.build("Austin, TX")) +phil.schedule_city(Place.build("Austin, TX", true)) phil.schedule_city(Place.build("Lubbock, TX")) -puts phil.route +puts "The original route was:" +phil.unrouted_cities.each do |city| + puts city +end +puts "\nThe new route is:" +phil.route.each do |city| + puts city +end +puts "There are a total of #{phil.total_miles.round(2)} miles on the route." +puts "The total travel time is #{phil.formatted_total_travel_time}" diff --git a/spec/map_spec.rb b/spec/map_spec.rb index 2e2f6f1..64d3952 100644 --- a/spec/map_spec.rb +++ b/spec/map_spec.rb @@ -1,5 +1,6 @@ require_relative "../lib/map" require 'geocoder' +require 'rspec' describe Map do @@ -10,8 +11,8 @@ end it "should use the first item in the array" do - austin = stub("Austin") - dallas = stub("Dallas") + austin = double("Austin") + dallas = double("Dallas") Geocoder.stub(:search) {[austin, dallas]} Map.search("austin, tx").should eq(austin) end @@ -19,8 +20,8 @@ describe ":distance" do it "should calculate distance between two sets of coordinates" do - alpha = stub - beta = stub + alpha = double + beta = double Geocoder::Calculations.should_receive(:distance_between).with(alpha, beta) Map.distance_between(alpha, beta) end diff --git a/spec/place_spec.rb b/spec/place_spec.rb index 7d48250..1f70982 100644 --- a/spec/place_spec.rb +++ b/spec/place_spec.rb @@ -1,5 +1,6 @@ require_relative "../lib/place" require_relative "../lib/map" +require 'rspec' describe Place do @@ -11,19 +12,32 @@ subject.coordinates.should eq([29,-95]) end + it "should have a starting point property" do + subject.should respond_to(:starting_point) + end + it "should not be a starting point by default" do + subject.starting_point.should be_false + end + describe ":build" do let(:name) { "El Paso, TX"} - let(:result) { stub("el paso", coordinates: [29, -95])} + let(:result) { double("el paso", coordinates: [29, -95])} it "should build from the map" do Map.should_receive(:search).with(name).and_return(result) Place.build(name) end - it "should be place" do + it "should be place" do + Map.stub(:search).with(name).and_return(result) + Place.build(name).should be_a(Place) + end + + it "should be set to the starting point when asked" do Map.stub(:search).with(name).and_return(result) - Place.build(name).should be_a(Place) + place = Place.build(name, true) + place.starting_point.should be_true end end diff --git a/spec/sales_person_spec.rb b/spec/sales_person_spec.rb index 08a6ce9..220b79b 100644 --- a/spec/sales_person_spec.rb +++ b/spec/sales_person_spec.rb @@ -1,30 +1,61 @@ require_relative "../lib/sales_person" require_relative "../lib/calculates_route" +require_relative "../lib/place" +require 'rspec' describe SalesPerson do + it "should have many cities" do - city = stub + city = double(:city, starting_point: false) subject.schedule_city(city) subject.cities.should include(city) end - it "should keep the cities only scheduled once" do - city = stub + it "should keep the cities only scheduled once" do + city = double(:city, starting_point: false) expect{ subject.schedule_city(city) subject.schedule_city(city) - }.to change(subject.cities,:count).by(1) + }.to change(subject.cities,:count).by(1) end it "should calculate a route via the CalculatesRoute" do - cities = [stub, stub, stub] - subject.stub(:cities) { cities } + cities = [double, double, double] + subject.stub(:cities) { cities } CalculatesRoute.should_receive(:calculate).with(cities) subject.route end + it "should returns the route from CalculatesRoute" do - route_stub = [stub, stub] + route_stub = [double, double] CalculatesRoute.stub(:calculate) { route_stub } subject.route.should eq(route_stub) end + +context "working with live datasets" do + + let (:city){Place.build("Dallas, TX")} + let (:another_city){Place.build("El Paso, TX", true)} + let (:yet_another_city){Place.build("El Paso, TX")} + + before :each do + subject.schedule_city(city) + subject.schedule_city(another_city) + subject.schedule_city(yet_another_city) + subject.route + end + + it 'should be able to choose his/her starting point' do + subject.cities.should == [another_city, city, yet_another_city] + end + + it 'should log the total miles for the route' do + subject.total_miles.should be_within(201.1).of(1100) + end + + it 'should output the total traveling time (assuming 55 mph)' do + subject.total_travel_time.should be_within(1).of(20) + end + end + end