diff --git a/Guardfile b/Guardfile index 6760f9177..fa59fc3ef 100644 --- a/Guardfile +++ b/Guardfile @@ -1,4 +1,4 @@ -guard :minitest, bundler: false, rubygems: false do +guard :minitest, bundler: false, autorun: false, rubygems: false do # with Minitest::Spec watch(%r{^spec/(.*)_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } diff --git a/design-activity.md b/design-activity.md new file mode 100644 index 000000000..a2a33e960 --- /dev/null +++ b/design-activity.md @@ -0,0 +1,49 @@ +### What classes does each implementation include? Are the lists the same? + +Each implementation includes the classes CartEntry, ShoppingCart, and Order. In Implementation A, CartEntry and ShoppingCart both have initialize as only methods and class Order has initialize and total_price. In Implementation B, CartEntry and ShoppingCart has price method in addition. + +### Write down a sentence to describe each class. + +CartEntry initializes with the unity price and quantity of the entry. ShoppingCart initializes with an empty array of entries. Order initializes by creating a new instance of ShoppingCart and has a method to calculate the total price. In Implementation B, each class can calculate their own price on their own (whether it's price for entry, price for shopping cart, or total price in order including tax). + +### How do the classes relate to each other? It might be helpful to draw a diagram on a whiteboard or piece of paper. + +CartEntry is added to an array in ShoppingCart. Order can calculate the total price from the shopping cart's entries. In implementation B, the price is calculated in every class so that Order doesn't need to do the calculating logic using CartEntry and ShoppingCart's instance variable and can instead get the subtotal by using @cart.price. + +### What data does each class store? How (if at all) does this differ between the two implementations? + +CartEntry stores the unit_price and quantity, ShoppingCart stores an array of CartEntry objects. Order stores one instance of ShoppingCart. There are additional methods in implementation B (the price method) but those calculate and return the sum, not store the data. + +### What methods does each class have? How (if at all) does this differ between the two implementations? + +Other than initialize, CartEntry and ShoppingCart have a price method that can return the price of each entry or cart in implementation B. In implementation A, only Order had a total_price method which did not adhere to single responsibility and used variables from ShoppingCart and CartEntry. + + +### Consider the Order#total_price method. In each implementation: +### Is logic to compute the price delegated to "lower level" classes like ShoppingCart and CartEntry, or is it retained in Order? + +In implementation A, it's retained in Order. In implementation B, it's delegated to CartEntry and ShoppingCart. + +### Does total_price directly manipulate the instance variables of other classes? + +In A, yes. in B, no. + +### If we decide items are cheaper if bought in bulk, how would this change the code? Which implementation is easier to modify? + +Implementation B makes the change easier to modify the code in CartEntry (such as a conditional loop) instead of trying to make that logic work in Order. + +### Which implementation better adheres to the single responsibility principle? + +Implementation B. + +### Bonus question once you've read Metz ch. 3: Which implementation is more loosely coupled? + +Implementation B. The total_price in Order doesn't require knowing the unit price and quantity of each CartEntry. + +### Changes made in Hotel + +In my original HotelBooker class, I had a method for make_block and directly changed the cost of the Room as such: + +` available[i].cost = discount ` + +To make this code less coupled, I created a wrapping method in Room `set_discount` so HotelBooker wouldn't directly handle the instance variable from Room. diff --git a/lib/date_range.rb b/lib/date_range.rb new file mode 100644 index 000000000..06a4e9469 --- /dev/null +++ b/lib/date_range.rb @@ -0,0 +1,22 @@ +require 'date' + +module Hotel + class DateRange + attr_reader :check_in, :check_out, :dates_booked + + def initialize(check_in, check_out) + if check_in >= check_out + raise StandardError, "Your check in and check out date is not valid!" + elsif !check_in.is_a?(Date) || !check_out.is_a?(Date) + raise ArgumentError, "You must input Dates in the date range!" + end + @check_in = check_in + @check_out = check_out + @dates_booked = (check_in...check_out).to_a + end + + def overlaps?(range) + @dates_booked & range.dates_booked == [] ? false : true + end + end +end diff --git a/lib/hotel_booker.rb b/lib/hotel_booker.rb new file mode 100644 index 000000000..aa0af1811 --- /dev/null +++ b/lib/hotel_booker.rb @@ -0,0 +1,135 @@ +require 'date' +require_relative 'room' +require_relative 'reservation' +require_relative 'date_range' + +module Hotel + class HotelBooker + attr_accessor :rooms, :reservations, :unreserved_block, :reserved_block + NUM_ROOMS = 20 + BLOCK_MAX = 5 + + def initialize() + @unreserved_block = [] + @reserved_block = [] + @reservations = [] + @rooms = [] + + NUM_ROOMS.times do |i| + @rooms << Room.new(i+1) + end + end + + def available?(available_rooms) + if available_rooms.empty? + raise StandardError, "There are no more available rooms for this date range!" + end + end + + def range(check_in, check_out) + DateRange.new(check_in, check_out) + end + + # arguments for check_in and check_out are Strings + def make_reservation(id, check_in, check_out) + check_in = Date.parse(check_in) + check_out = Date.parse(check_out) + reservation = Reservation.new(id, range(check_in, check_out)) + + available = unreserved_rooms(check_in, check_out) + available?(available) + + reservation.room = available[0] + @reservations << reservation + end + + + def make_block(rooms, discount, check_in, check_out) + check_in = Date.parse(check_in) + check_out = Date.parse(check_out) + date_range = range(check_in, check_out) + available = unreserved_rooms(check_in, check_out) + if rooms > BLOCK_MAX + raise StandardError, "Five is the maximum number of rooms in a block." + elsif available.length < rooms + raise StandardError, "There are not enough available rooms to create a block." + end + + rooms.times do |i| + reservation = Reservation.new("BLOCK ROOM #{i+1}", date_range) + available[i].set_discount(discount) + reservation.room = available[i] + @unreserved_block << reservation + end + + return @unreserved_block + end + + + def make_block_reservation(id) + available?(@unreserved_block) + @reserved_block << @unreserved_block[0] + @unreserved_block.delete_at(0) + end + + + # arguments for check_in and check_out are Dates + def unreserved_rooms(check_in, check_out) + reserved_rooms = [] + unreserved_block_rooms = [] + reserved_block_rooms = [] + new_range = range(check_in, check_out) + + @reservations.each do |reservation| + if reservation.date_range.overlaps?(new_range) + reserved_rooms << reservation.room + end + end + + @unreserved_block.each do |reservation| + if reservation.date_range.overlaps?(new_range) + unreserved_block_rooms << reservation.room + end + end + + @reserved_block.each do |reservation| + if reservation.date_range.overlaps?(new_range) + reserved_block_rooms << reservation.room + end + end + + unreserved = @rooms - reserved_rooms - unreserved_block_rooms - reserved_block_rooms + + return unreserved + end + + def unreserved_block_rooms + unreserved_block_rooms = + @unreserved_block.map { |reservation| reservation.room } + return unreserved_block_rooms + end + + + def find_reservations(date) + matching_reservations = [] + date = Date.parse(date) + + @reservations.each do |reservation| + reservation_dates = reservation.date_range.dates_booked + if reservation_dates.include?(date) + matching_reservations << reservation + end + end + + @reserved_block.each do |reservation| + reservation_dates = reservation.date_range.dates_booked + if reservation_dates.include?(date) + matching_reservations << reservation + end + end + + return matching_reservations + end + + end +end diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..253445e80 --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,25 @@ +require_relative 'room' + +module Hotel + class Reservation + attr_reader :id, :date_range + attr_accessor :room + + def initialize(id, date_range) + @id = id + @date_range = date_range + # placeholder room initation + @room = Room.new(0) + end + + def cost + @cost = calculate_total_cost + end + + def calculate_total_cost + total_days = @date_range.dates_booked.length + total_cost = @room.cost * total_days + return total_cost + end + end +end diff --git a/lib/room.rb b/lib/room.rb new file mode 100644 index 000000000..08557ce1f --- /dev/null +++ b/lib/room.rb @@ -0,0 +1,16 @@ +module Hotel + class Room + attr_reader :id + attr_accessor :cost + + def initialize(id) + @id = id + @cost = 200 + end + + def set_discount(new_cost) + @cost = new_cost + end + + end +end diff --git a/refactors.txt b/refactors.txt new file mode 100644 index 000000000..d9fd94eec --- /dev/null +++ b/refactors.txt @@ -0,0 +1,7 @@ +1. I...lowkey cheated and made a collection of Reservations instead of a collection of Rooms. I would rewrite this part by making a subclass under Room called BookedRoom? + +2. HotelBooker has a lot of WET code because I have to loop through several data structures (@unreserved_block, @reserved_block, @reservations)!! If I had focused the Wave 3 blocks of rooms on the ROOMS instead of reservations, probably wouldn't have created this mess ¯\_(ツ)_/¯ + +3. there are certain check_in and check_out arguments that are strings (as in Hotel::HotelBooker.new.make_reservation) and some that are an instance of Date (as in Hotel::HotelBooker.new.unreserved_rooms). Other arguments pass in a Hotel::DateRange (such as Hotel::Reservations). note to future goeun: make this less terrible + +4. How many ways can you say "range"? ;__; diff --git a/spec/date_range_spec.rb b/spec/date_range_spec.rb new file mode 100644 index 000000000..040d71055 --- /dev/null +++ b/spec/date_range_spec.rb @@ -0,0 +1,74 @@ +require_relative 'spec_helper' + +describe "DateRange Class" do + before do + @date1 = Date.parse('2018-09-05') + @date2 = Date.parse('2018-09-09') + @date3 = Date.parse('2018-09-10') + @date4 = Date.parse('2018-09-11') + + @date_range1 = Hotel::DateRange.new(@date1, @date2) + @date_range2 = Hotel::DateRange.new(@date1, @date3) + @date_range3 = Hotel::DateRange.new(@date2, @date3) + @date_range4 = Hotel::DateRange.new(@date3, @date4) + end + + describe "Date Range initation " do + it "is an instance of Date Range" do + expect(@date_range1).must_be_kind_of Hotel::DateRange + end + + it "passes Dates as arguments" do + expect(@date_range1.check_in).must_be_kind_of Date + expect(@date_range2.check_out).must_be_kind_of Date + end + + it "raises a StandardError if invalid date range is provided" do + expect{ Hotel::DateRange.new(@date1, @date1) }.must_raise StandardError + expect{ Hotel::DateRange.new(@date3, @date1) }.must_raise StandardError + end + it "raises an ArgumentError if user do not put in a Date" do + expect{ Hotel::DateRange.new(@date1, '12/08/23') }.must_raise ArgumentError + expect{ Hotel::DateRange.new('12/06/30', @date1) }.must_raise ArgumentError + expect{ Hotel::DateRange.new('12/06/30', '12/08/23') }.must_raise ArgumentError + end + + it "creates an array of Dates between check_in and check_out & excluding check_out" do + expect(@date_range3.dates_booked).must_be_kind_of Array + expect(@date_range3.dates_booked.length).must_equal 1 + expect(@date_range3.dates_booked[0]).must_equal @date2 + expect(@date_range2.dates_booked.length).must_equal 5 + expect(@date_range2.dates_booked[0]).must_equal @date1 + end + end + + describe "overlaps? method to check if ranges overlap" do + it "returns true if date ranges are the same" do + expect(@date_range1.overlaps?(@date_range1)).must_equal true + end + + it "returns true if date ranges overlaps in the front" do + expect(@date_range1.overlaps?(@date_range2)).must_equal true + end + + it "returns true if date ranges overlaps in the back" do + expect(@date_range2.overlaps?(@date_range1)).must_equal true + end + + it "returns true if one range is completely contained in another range" do + expect(@date_range2.overlaps?(@date_range3)).must_equal true + end + + it "returns false if one range is completely after" do + expect(@date_range1.overlaps?(@date_range4)).must_equal false + end + + it "returns false if one range is completely before" do + expect(@date_range4.overlaps?(@date_range1)).must_equal false + end + it "returns false if a range starts on another's check in date" do + expect(@date_range1.overlaps?(@date_range3)).must_equal false + expect(@date_range2.overlaps?(@date_range4)).must_equal false + end + end +end diff --git a/spec/hotel_booker_spec.rb b/spec/hotel_booker_spec.rb new file mode 100644 index 000000000..f54137cca --- /dev/null +++ b/spec/hotel_booker_spec.rb @@ -0,0 +1,177 @@ +require_relative 'spec_helper' + +describe "HotelBooker Class" do + describe "HotelBooker initation" do + let(:booker) {Hotel::HotelBooker.new} + + it "Returns an instance of HotelBooker" do + expect(booker).must_be_kind_of Hotel::HotelBooker + end + + it "Establishes the base data structures when instantiated" do + [:rooms, :reservations].each do |prop| + expect(booker).must_respond_to prop + end + expect(booker.rooms).must_be_kind_of Array + expect(booker.reservations).must_be_kind_of Array + end + + it "Loads 20 Rooms" do + expect(booker.rooms.length).must_equal 20 + 20.times do |i| + expect(booker.rooms[i]).must_be_kind_of Hotel::Room + end + end + end + + describe "Make a reservation method" do + before do + @booker = Hotel::HotelBooker.new + end + + it "adds a Reservation to the list of reservations" do + @booker.make_reservation(1, '2018-09-05', '2018-09-07') + expect(@booker.reservations.length).must_equal 1 + expect(@booker.reservations[0]).must_be_kind_of Hotel::Reservation + end + + it "adds a Reservation which contains an instance of Room" do + @booker.make_reservation(1, '2018-09-05', '2018-09-07') + expect(@booker.reservations[0].room).must_be_kind_of Hotel::Room + end + + it "raises a StandardError if there are no available rooms for the date range" do + 20.times do |i| + @booker.make_reservation(i+1, '2018-09-05', '2018-09-07') + end + + expect{ @booker.make_reservation(21, '2018-09-05', '2018-09-07') }.must_raise StandardError + end + end + + describe "Find a reservation by date method" do + before do + @booker = Hotel::HotelBooker.new + @booker.make_reservation(1, '2018-09-05', '2018-09-08') + @booker.make_reservation(2, '2018-09-06', '2018-09-08') + @booker.make_reservation(3, '2018-09-07', '2018-09-09') + end + + it "returns an array of reservations if there are reservations" do + expect(@booker.find_reservations('2018-09-05')).must_be_kind_of Array + expect(@booker.find_reservations('2018-09-06')[0]).must_be_kind_of Hotel::Reservation + expect(@booker.find_reservations('2018-09-07').length).must_equal 3 + end + + it "returns an empty array if there are no reservations for that date" do + expect(@booker.find_reservations('2019-10-13')).must_equal [] + end + end + + describe "unreserved_rooms method" do + before do + @date1 = Date.parse('2018-09-05') + @date2 = Date.parse('2018-09-09') + @date3 = Date.parse('2018-09-10') + @date4 = Date.parse('2018-09-11') + + @booker = Hotel::HotelBooker.new + @booker.make_reservation(1, '2018-09-05', '2018-09-08') + @booker.make_reservation(2, '2018-09-06', '2018-09-08') + @booker.make_reservation(3, '2018-09-07', '2018-09-09') + end + + it "returns an array of unreserved rooms for date range given unreserved rooms exist" do + expect(@booker.unreserved_rooms(@date1, @date2)).must_be_kind_of Array + expect(@booker.unreserved_rooms(@date1, @date2).length).must_equal 17 + expect(@booker.unreserved_rooms(@date1, @date2)[0]).must_be_kind_of Hotel::Room + expect(@booker.unreserved_rooms(@date1, @date2)[0].id).must_equal 4 + end + + it "returns array of 20 rooms given if there no reservations for the date" do + expect(@booker.unreserved_rooms(@date3, @date4)).must_be_kind_of Array + expect(@booker.unreserved_rooms(@date3, @date4).length).must_equal 20 + end + end + + describe "Make block of rooms" do + before do + @booker = Hotel::HotelBooker.new + end + + it "raises StandardError if user tries to create a block with more than 5 rooms" do + expect{ @booker.make_block(6, 150, '2018-09-05', '2018-09-10') }.must_raise StandardError + end + + it "raises StandardError if there are not enough available rooms for a block" do + 20.times do |i| + @booker.make_reservation(i+1, '2018-09-05', '2018-09-10') + end + + expect{ @booker.make_block(1, 150, '2018-09-05', '2018-09-10') }.must_raise StandardError + end + + it "creates an array of available block reservations" do + expect(@booker.make_block(5, 150, '2018-09-05', '2018-09-10')).must_be_kind_of Array + end + + it "creates an array the size of Reservations described by rooms" do + expect(@booker.make_block(5, 150, '2018-09-05', '2018-09-10').length).must_equal 5 + end + + it "creates an array carrying instances of Reservation" do + expect(@booker.make_block(5, 150, '2018-09-05', '2018-09-10')[4]).must_be_kind_of Hotel::Reservation + end + + it "carries instances of Reservation with adjusted cost" do + expect(@booker.make_block(1, 150, '2018-09-05', '2018-09-10')[0].cost).must_equal 750 + end + + it "assigns rooms starting from available rooms" do + 10.times do |i| + @booker.make_reservation(i+1, '2018-09-05', '2018-09-10') + end + expect(@booker.make_block(5, 150, '2018-09-05', '2018-09-10')[0].room.id).must_equal 11 + end + + it "reservation dates match the date range of the block" do + @booker.make_block(1, 150, '2018-09-05', '2018-09-10') + dates = @booker.unreserved_block[0].date_range + same_range = @booker.range(Date.parse('2018-09-05'), Date.parse('2018-09-10')) + expect(dates.check_in).must_equal same_range.check_in + expect(dates.check_out).must_equal same_range.check_out + end + end + + describe "make_block_reservation method" do + before do + @booker = Hotel::HotelBooker.new + end + + it "raises an error if there are no avaiable block reservations" do + expect{ @booker.make_block_reservation(1) }.must_raise StandardError + end + + it "moves a reservation from @unreserved_block to @reserve_block" do + @booker.make_block(1, 150, '2018-09-05', '2018-09-10') + @booker.make_block_reservation(1) + + expect(@booker.unreserved_block.length).must_equal 0 + expect(@booker.reserved_block.length).must_equal 1 + expect(@booker.reserved_block[0]).must_be_kind_of Hotel::Reservation + end + end + + describe "unreserved_block_rooms method" do + before do + @booker = Hotel::HotelBooker.new + @booker.make_block(5, 150, '2018-09-05', '2018-09-10') + end + + it "returns an array of available rooms marked for block reservations" do + expect(@booker.unreserved_block_rooms).must_be_kind_of Array + expect(@booker.unreserved_block_rooms[0]).must_be_kind_of Hotel::Room + expect(@booker.unreserved_block_rooms.length).must_equal 5 + end + end +end diff --git a/spec/reservation_spec.rb b/spec/reservation_spec.rb new file mode 100644 index 000000000..582817ce6 --- /dev/null +++ b/spec/reservation_spec.rb @@ -0,0 +1,34 @@ +require_relative 'spec_helper' + +describe "Reservation Class" do + let (:date1) { Date.parse('2018-09-05') } + let (:date2) { Date.parse('2018-09-07') } + let (:date_range) { Hotel::DateRange.new(date1, date2) } + let (:reservation) { Hotel::Reservation.new(1, date_range) } + + describe "Reservation initiation" do + it "is an instance of Reservation" do + expect(reservation).must_be_kind_of Hotel::Reservation + end + + it "has a DateRange as an argument" do + expect(reservation.date_range).must_be_kind_of Hotel::DateRange + expect(reservation.date_range.check_in).must_be_kind_of Date + expect(reservation.date_range.check_out).must_be_kind_of Date + end + end + + describe "calculate_total_cost helper method" do + it "calculates the cost of the reservation " do + date3 = Date.parse('2018-09-06') + date4 = Date.parse('2018-12-06') + date_range2 = Hotel::DateRange.new(date3, date4) + reservation_1 = Hotel::Reservation.new(1, date_range) + reservation_2 = Hotel::Reservation.new(2, date_range2) + + expect(reservation_1.cost).must_equal 400 + expect(reservation_2.cost).must_equal 18200 + end + end + +end diff --git a/spec/room_spec.rb b/spec/room_spec.rb new file mode 100644 index 000000000..7ffe73742 --- /dev/null +++ b/spec/room_spec.rb @@ -0,0 +1,14 @@ +require_relative 'spec_helper' + +describe "Room Class" do + describe "Room initiation" do + before do + @room = Hotel::Room.new(1) + end + + it "is an instance of Room" do + expect(@room).must_be_kind_of Hotel::Room + end + + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4d1e3fdc8..ad8ac1985 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,16 @@ +require 'simplecov' +SimpleCov.start require 'minitest' require 'minitest/autorun' require 'minitest/reporters' -# Add simplecov +require 'minitest/pride' +require 'date' +require 'pry' Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new # Require_relative your lib files here! +require_relative '../lib/hotel_booker' +require_relative '../lib/room' +require_relative '../lib/reservation' +require_relative '../lib/date_range'