diff --git a/Gemfile b/Gemfile index 63ffa5acd..769f5f038 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'http://rubygems.org' -ruby '2.5.5' +ruby '2.6.5' gem 'rake' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..9c5e909d0 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,34 @@ +GEM + remote: http://rubygems.org/ + specs: + ansi (1.5.0) + awesome_print (1.8.0) + builder (3.2.4) + csv (3.1.2) + minitest (5.14.0) + minitest-reporters (1.4.2) + ansi + builder + minitest (>= 5.0) + ruby-progressbar + minitest-skip (0.0.1) + minitest (~> 5.0) + rake (13.0.1) + ruby-progressbar (1.10.1) + +PLATFORMS + ruby + +DEPENDENCIES + awesome_print + csv + minitest + minitest-reporters + minitest-skip + rake + +RUBY VERSION + ruby 2.5.5p157 + +BUNDLED WITH + 2.1.4 diff --git a/lib/csv_record.rb b/lib/csv_record.rb index 96319ab87..a4d413153 100644 --- a/lib/csv_record.rb +++ b/lib/csv_record.rb @@ -23,9 +23,7 @@ def self.load_all(full_path: nil, directory: nil, file_name: nil) end def self.validate_id(id) - if id.nil? || id <= 0 - raise ArgumentError, 'ID cannot be blank or less than one.' - end + raise ArgumentError.new('ID cannot be blank or less than one.') if id.nil? || id <= 0 end private @@ -35,9 +33,7 @@ def self.from_csv(record) end def self.build_path(directory, file_name) - unless directory - raise ArgumentError, "Either full_path or directory is required" - end + raise ArgumentError.new("Either full_path or directory is required") unless directory unless file_name class_name = self.to_s.split('::').last diff --git a/lib/driver.rb b/lib/driver.rb new file mode 100644 index 000000000..5b4357e22 --- /dev/null +++ b/lib/driver.rb @@ -0,0 +1,65 @@ +require 'csv' +require_relative 'csv_record' + +module RideShare + class Driver < CsvRecord + attr_reader :id, :name, :vin, :trips + attr_accessor :status + + def initialize( + id:, + name:, + vin:, + status: :AVAILABLE, + trips: nil + ) + super(id) + + (vin.length == 17) ? (@vin = vin) : (raise ArgumentError) + [:AVAILABLE, :UNAVAILABLE].include?(status) ? (@status = status.to_sym) : (raise ArgumentError) + + @name = name + @trips = trips || [] + + end + + def trip_in_progress(trip) + @status = :UNAVAILABLE + add_trip(trip) + end + + def add_trip(trip) + @trips << trip + end + + def average_rating + # get all trips where rating is not nil + completed_trips = trips.select{ |trip| trip.rating != nil } + + trip_ratings = completed_trips.map { |trip| trip.rating.to_f } + + return 0 if trip_ratings.length == 0 + return trip_ratings.sum / trip_ratings.length + end + + def total_revenue + # get all trips where cost is not nil and is more than 1.65 + completed_trips = trips.select{ |trip| trip.cost != nil && trip.cost > 1.65 } + + trip_revenues = completed_trips.map { |trip| (trip.cost - 1.65) * 0.8 } + + return trip_revenues.sum.round(2) + end + + private + + def self.from_csv(record) + return new( + id: record[:id], + name: record[:name], + vin: record[:vin], + status: record[:status].to_sym + ) + end + end +end \ No newline at end of file diff --git a/lib/passenger.rb b/lib/passenger.rb index afd13d73b..401b982a3 100644 --- a/lib/passenger.rb +++ b/lib/passenger.rb @@ -16,6 +16,18 @@ def add_trip(trip) @trips << trip end + def net_expenditures + all_trips = @trips.map { |trip| trip.cost } + + return all_trips.sum + end + + def total_time_spent + all_trips_duration = @trips.map { |trip| trip.calculate_duration } + + return all_trips_duration.sum + end + private def self.from_csv(record) diff --git a/lib/trip.rb b/lib/trip.rb index 87ce496e9..c80f4e0a2 100644 --- a/lib/trip.rb +++ b/lib/trip.rb @@ -1,13 +1,16 @@ require 'csv' +require 'time' require_relative 'csv_record' module RideShare class Trip < CsvRecord - attr_reader :id, :passenger, :passenger_id, :start_time, :end_time, :cost, :rating + attr_reader :id, :passenger, :passenger_id, :start_time, :end_time, :cost, :rating, :driver_id, :driver def initialize( id:, + driver_id: nil, + driver: nil, passenger: nil, passenger_id: nil, start_time:, @@ -16,25 +19,37 @@ def initialize( rating: ) super(id) + if end_time != nil + raise ArgumentError if end_time < start_time + end if passenger @passenger = passenger @passenger_id = passenger.id - elsif passenger_id @passenger_id = passenger_id - else raise ArgumentError, 'Passenger or passenger_id is required' end + if driver + @driver = driver + @driver_id = driver.id + elsif driver_id + @driver_id = driver_id + else + raise ArgumentError, 'driver or driver_id is required' + end + @start_time = start_time @end_time = end_time @cost = cost @rating = rating - if @rating > 5 || @rating < 1 - raise ArgumentError.new("Invalid rating #{@rating}") + if @rating != nil + if @rating > 5 || @rating < 1 + raise ArgumentError.new("Invalid rating #{@rating}") + end end end @@ -46,9 +61,15 @@ def inspect "PassengerID=#{passenger&.id.inspect}>" end - def connect(passenger) + def connect(passenger, driver) @passenger = passenger passenger.add_trip(self) + @driver = driver + driver.add_trip(self) + end + + def calculate_duration() + return end_time - start_time end private @@ -57,10 +78,11 @@ def self.from_csv(record) return self.new( id: record[:id], passenger_id: record[:passenger_id], - start_time: record[:start_time], - end_time: record[:end_time], + start_time: Time.parse(record[:start_time]), + end_time: Time.parse(record[:end_time]), cost: record[:cost], - rating: record[:rating] + rating: record[:rating], + driver_id: record[:driver_id] ) end end diff --git a/lib/trip_dispatcher.rb b/lib/trip_dispatcher.rb index 5130849f8..8a3765507 100644 --- a/lib/trip_dispatcher.rb +++ b/lib/trip_dispatcher.rb @@ -11,6 +11,7 @@ class TripDispatcher def initialize(directory: './support') @passengers = Passenger.load_all(directory: directory) @trips = Trip.load_all(directory: directory) + @drivers = Driver.load_all(directory: directory) connect_trips end @@ -19,6 +20,11 @@ def find_passenger(id) return @passengers.find { |passenger| passenger.id == id } end + def find_driver(id) + Driver.validate_id(id) + return @drivers.find { |driver| driver.id == id } + end + def inspect # Make puts output more useful return "#<#{self.class.name}:0x#{object_id.to_s(16)} \ @@ -27,12 +33,61 @@ def inspect #{passengers.count} passengers>" end + def pick_driver_for_trip + # gets all drivers who are available + available_drivers = @drivers.select{ |driver| driver.status == :AVAILABLE} + raise "No drivers are currently available" if available_drivers.length == 0 + + picked_driver = "" + oldest_end_time = Time.now + + available_drivers.each do |driver| + # picks first driver who has never driven + if driver.trips.length == 0 + picked_driver = driver + return picked_driver + else + # picks driver whos most recent trip is the oldest compared to today + driver_most_recent_end_time = driver.trips.max_by { |trip| trip.end_time }.end_time + if driver_most_recent_end_time < oldest_end_time + oldest_end_time = driver_most_recent_end_time + picked_driver = driver + end + end + end + + return picked_driver + end + + def request_trip(passenger_id) + + found_passenger = find_passenger(passenger_id) + available_driver = pick_driver_for_trip + new_trip = Trip.new( + id: @trips.length + 1, + passenger: found_passenger, + start_time: Time.now, + end_time: nil, + cost: nil, + rating: nil, + driver_id: available_driver.id, + driver: available_driver + ) + + available_driver.trip_in_progress(new_trip) + found_passenger.add_trip(new_trip) + @trips.push(new_trip) + + return new_trip + end + private def connect_trips @trips.each do |trip| passenger = find_passenger(trip.passenger_id) - trip.connect(passenger) + driver = find_driver(trip.driver_id) + trip.connect(passenger, driver) end return trips diff --git a/test/driver_test.rb b/test/driver_test.rb index 4e6076ec2..a22bd7a58 100644 --- a/test/driver_test.rb +++ b/test/driver_test.rb @@ -1,6 +1,6 @@ require_relative 'test_helper' -xdescribe "Driver class" do +describe "Driver class" do describe "Driver instantiation" do before do @driver = RideShare::Driver.new( @@ -128,9 +128,85 @@ expect(@driver.average_rating).must_be_close_to (5.0 + 1.0) / 2.0, 0.01 end + + it "returns accurate average rating when there are in progress trips" do + tripdispatcher = RideShare::TripDispatcher.new + + # get the first available driver id + picked_driver = tripdispatcher.pick_driver_for_trip + current_rating = picked_driver.average_rating + + # add an in progress trip to the driver + requested_trip = tripdispatcher.request_trip(1) + + expect(picked_driver.average_rating).must_equal current_rating + end + end describe "total_revenue" do # You add tests for the total_revenue method + before do + @driver = RideShare::Driver.new( + id: 54, + name: "Rogers Bartell IV", + vin: "1C9EVBRM0YBC564DZ" + ) + trip1 = RideShare::Trip.new( + id: 8, + driver: @driver, + passenger_id: 3, + start_time: Time.new(2016, 8, 8), + end_time: Time.new(2016, 8, 8), + cost: 5, + rating: 5 + ) + trip2 = RideShare::Trip.new( + id: 8, + driver: @driver, + passenger_id: 3, + start_time: Time.new(2016, 8, 8), + end_time: Time.new(2016, 8, 8), + cost: 20, + rating: 5 + ) + @driver.add_trip(trip1) + @driver.add_trip(trip2) + end + + it "calculates total revenue of all trips" do + expect(@driver.total_revenue).must_equal 17.36 + end + + it "calculates correct total when there are trips less than 1.65" do + trip3 = RideShare::Trip.new( + id: 8, + driver: @driver, + passenger_id: 3, + start_time: Time.new(2016, 8, 8), + end_time: Time.new(2016, 8, 8), + cost: 1.25, + rating: 5 + ) + @driver.add_trip(trip3) + + expect(@driver.total_revenue).must_equal 17.36 + end + + it "returns 0 if driver has no trips" do + # remove all trips from driver + @driver.trips.clear + + expect(@driver.total_revenue).must_equal 0 + end + + it "returns accurate drivers revenue when in progress trips" do + tripdispatcher = RideShare::TripDispatcher.new + available_driver = tripdispatcher.pick_driver_for_trip + current_revenue = available_driver.total_revenue + requested_trip = tripdispatcher.request_trip(1) + + expect(available_driver.total_revenue).must_equal current_revenue + end end end diff --git a/test/passenger_test.rb b/test/passenger_test.rb index eb3a631df..94ad7483c 100644 --- a/test/passenger_test.rb +++ b/test/passenger_test.rb @@ -49,7 +49,8 @@ passenger: @passenger, start_time: Time.new(2016, 8, 8), end_time: Time.new(2016, 8, 9), - rating: 5 + rating: 5, + driver_id: 20 ) @passenger.add_trip(trip) @@ -68,7 +69,58 @@ end end - describe "net_expenditures" do - # You add tests for the net_expenditures method + describe "passanger calculations" do + before do + @passenger = RideShare::Passenger.new( + id: 9, + name: "Merl Glover III", + phone_number: "1-602-620-2330 x3723", + trips: [] + ) + trip = RideShare::Trip.new( + id: 8, + passenger: @passenger, + start_time: Time.parse("2019-01-19 13:25:41 -0800"), + end_time: Time.parse("2019-01-19 13:37:41 -0800"), + cost: 10, + rating: 5, + driver_id: 5 + ) + trip2 = RideShare::Trip.new( + id: 2, + passenger: @passenger, + start_time: Time.parse("2019-01-20 15:15:41 -0800"), + end_time: Time.parse("2019-01-20 15:48:41 -0800"), + cost: 15, + rating: 5, + driver_id: 5 + ) + @passenger.add_trip(trip) + @passenger.add_trip(trip2) + end + + describe "net_expenditures" do + it "calculates total expenditures of trips" do + expect(@passenger.net_expenditures).must_equal 25 + end + + it "returns 0 if passenger has no trips" do + # remove all trips from passenger + @passenger.trips.clear + expect(@passenger.net_expenditures).must_equal 0 + end + end + + describe "total_time_spent" do + it "calculates total time spent on trips" do + expect(@passenger.total_time_spent).must_equal 2700 + end + + it "returns 0 if there a 0 trips" do + @passenger.trips.clear + expect(@passenger.total_time_spent).must_equal 0 + end + + end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index bdb9bf352..082369790 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -12,4 +12,4 @@ require_relative '../lib/passenger' require_relative '../lib/trip' require_relative '../lib/trip_dispatcher' -# require_relative '../lib/driver' +require_relative '../lib/driver' diff --git a/test/trip_dispatch_test.rb b/test/trip_dispatch_test.rb index 21f4457b7..63a90d0f4 100644 --- a/test/trip_dispatch_test.rb +++ b/test/trip_dispatch_test.rb @@ -78,8 +78,7 @@ def build_test_dispatcher end end - # TODO: un-skip for Wave 2 - xdescribe "drivers" do + describe "drivers" do describe "find_driver method" do before do @dispatcher = build_test_dispatcher @@ -122,4 +121,51 @@ def build_test_dispatcher end end end + + describe "requesting a trip" do + before do + @tripdispatcher = build_test_dispatcher + @passenger_id = 1 + @requested_trip = @tripdispatcher.request_trip(@passenger_id) + end + + it "creates a trip" do + expect(@requested_trip.passenger_id).must_equal @passenger_id + expect(@requested_trip.driver_id).must_equal 3 + expect(@requested_trip.end_time).must_be_nil + expect(@requested_trip.cost).must_be_nil + expect(@requested_trip.rating).must_be_nil + end + + it "updates the driver" do + expect(@requested_trip.driver.trips.include?(@requested_trip)).must_equal true + expect(@requested_trip.driver.status).must_equal :UNAVAILABLE + end + + it "updates the passenger" do + expect(@requested_trip.passenger.trips.include?(@requested_trip)).must_equal true + end + + it "adds trip to tripdispatcher list of trips" do + expect(@tripdispatcher.trips.include?(@requested_trip)).must_equal true + end + + it "raises Runtime Error if no drivers are currently available" do + @tripdispatcher.drivers.each do |driver| + driver.status = :UNAVAILABLE + end + expect{@tripdispatcher.request_trip(@passenger_id)}.must_raise RuntimeError + end + + it "picks first driver with no rides" do + tripdispatcher = build_test_dispatcher + expect(tripdispatcher.pick_driver_for_trip.id).must_equal 3 + end + + it "picks driver whose most recent trip ended the longest time ago if they all have rides" do + tripdispatcher = build_test_dispatcher + tripdispatcher.request_trip(1) + expect(tripdispatcher.pick_driver_for_trip.id).must_equal 2 + end + end end diff --git a/test/trip_test.rb b/test/trip_test.rb index 2063e28a7..268bb2b0d 100644 --- a/test/trip_test.rb +++ b/test/trip_test.rb @@ -15,7 +15,12 @@ start_time: start_time, end_time: end_time, cost: 23.45, - rating: 3 + rating: 3, + driver: RideShare::Driver.new( + id: 50, + name: "Test Driver", + vin: "eiwuqhf73829103j8" + ) } @trip = RideShare::Trip.new(@trip_data) end @@ -29,7 +34,6 @@ end it "stores an instance of driver" do - skip # Unskip after wave 2 expect(@trip.driver).must_be_kind_of RideShare::Driver end @@ -41,5 +45,14 @@ end.must_raise ArgumentError end end + + it "raises ArgumentError if start_time is after end_time" do + @trip_data[:end_time] = @trip_data[:start_time] - 25 * 60 + expect{RideShare::Trip.new(@trip_data)}.must_raise ArgumentError + end + + it "correctly calculates duration of trip in seconds" do + expect(@trip.calculate_duration).must_equal 1500 + end end end