diff --git a/.gitignore b/.gitignore index 5e1422c9c..1ded4cf46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +coverage + *.gem *.rbc /.config diff --git a/design-activity.md b/design-activity.md new file mode 100644 index 000000000..dc95d007a --- /dev/null +++ b/design-activity.md @@ -0,0 +1,53 @@ +- What classes does each implementation include? Are the lists the same? + +Implementations A and B have the same classes: CartEntry, ShoppingCart, and Order. + +- Write down a sentence to describe each class. + +CartEntry: This object is a specific item, it's price and quantity are stored +ShoppingCart: A list of all items in a cart +Order: A shopping cart of items + +- How do the classes relate to each other? It might be helpful to draw a diagram on a whiteboard or piece of paper. + +The Order class is instantiated with a ShoppingCart class. The ShoppingCart stores an array of entries made up of the CartEntry class. + +Order is one-to-one with ShoppingCart +ShoppingCart is one-to-many with CartEntry + +- What data does each class store? How (if at all) does this differ between the two implementations? + +There is no difference in the data stored, the initialize methods are unchanged between the implementation: +CartEntry stores unit_price and quantity +ShoppingCart stores entries +Order stores cart + +- What methods does each class have? How (if at all) does this differ between the two implementations? + +CartEntry: has a price method in implementation B and no methods in implementation A +Shopping Cart: has a price method in implementation B and no methods in implementation A +Order: has a total_price method - this is the same for both implementations + +- 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 the logic to compute price is retained in Order; however, in implementation B the logic is in ShoppingCart and CartEntry + +- Does total_price directly manipulate the instance variables of other classes? + +Total_price directly manipulates the instance variables in implementation A, but not B + +- If we decide items are cheaper if bought in bulk, how would this change the code? Which implementation is easier to modify? + +This would change either the total_price method in implementation A of the price method of CartEntry is implementation B. Implementation B would be easier to modify. + +- 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 (fewer dependencies) + +Describe in design-activity.md what changes you would need to make to improve this design, and how the resulting design would be an improvement. + diff --git a/lib/block.rb b/lib/block.rb new file mode 100644 index 000000000..c5e035025 --- /dev/null +++ b/lib/block.rb @@ -0,0 +1,11 @@ +require 'date' + +class Block + attr_reader :reservations, :block_id + + def initialize(reservations:, block_id:) + @reservations = reservations + @block_id = block_id + end + +end \ No newline at end of file diff --git a/lib/hotel.rb b/lib/hotel.rb new file mode 100644 index 000000000..f9671c7f8 --- /dev/null +++ b/lib/hotel.rb @@ -0,0 +1,135 @@ +require_relative 'reservation' +require_relative 'block' + +class Hotel + attr_reader :reservations, :rooms, :blocks + + def initialize(reservations: nil, blocks:nil) + @reservations = reservations || [] + @blocks = blocks || [] + @rooms = [*1..20] + end + + + def make_reservation(start_date:, end_date:, hold_block:false, block_id:nil) + if hold_block != false && hold_block.to_s.match(/[1-5]/) == nil + raise ArgumentError.new("If you want hold a block of rooms, enter the number of rooms (maximum of 5) to hold.") + elsif hold_block == false && block_id == nil + id = @reservations.length + 1 + room = list_available_rooms(start_date, end_date).sample + reservation = Reservation.new(start_date:start_date, end_date:end_date, room:room, reservation_id:id) + reservations.push(reservation) + return reservation + elsif hold_block.to_s.match(/[1-5]/) != nil + if block_id != nil + raise ArgumentError.new("Please make hotel block before booking a room ") + end + reservation_block = {} + available_count = list_available_rooms(start_date, end_date).length + if available_count < hold_block + raise ArgumentError.new("Not enough rooms available for this block") + end + hold_block.times do + id = @reservations.length + 1 + room = list_available_rooms(start_date, end_date).sample + reservation = Reservation.new(start_date:start_date, end_date:end_date, room:room, reservation_id:id, block:true) + reservations.push(reservation) + reservation_block[reservation] = "Not booked" + end + id = @blocks.length + 1 + block = Block.new(reservations:reservation_block, block_id:id) + blocks.push(block) + return block + elsif block_id.to_s.match(/[1-5]/) != nil + block = blocks.find{|block| block.block_id == block_id } + if block == nil + raise ArgumentError.new("This block does not exist yet, please reserve the block first") + end + reservations = block.reservations + reservation = reservations.find{|reservation, status| status == "Not booked"} + if reservation == nil + raise ArgumentError.new("This block has been fully booked") + end + reservations[reservation[0]] = "Booked" + return reservations[reservation[0]] + end + end + + def find_reservations_by_date(start_date, end_date) + if start_date.class == String + start_date = Date.strptime(start_date, "%m/%d/%Y") + end + if end_date.class == String + end_date = Date.strptime(end_date, "%m/%d/%Y") + end + found_reservations = [] + if @reservations != nil + @reservations.each do |reservation| + date = start_date.dup + until date == end_date do + if (start_date >= reservation.start_date && start_date < reservation.end_date) || (end_date > reservation.start_date && end_date <= reservation.end_date) + found_reservations.push(reservation) + break + end + date += 1 + end + end + end + return found_reservations + end + + def check_block_availability(block_id:) + if block_id.to_s.match(/[1-5]/) != nil + block = @blocks.find{|block| block.block_id == block_id } + if block == nil + raise ArgumentError.new("This block does not exist yet, please reserve the block first") + end + reservations = block.reservations + available_in_block = reservations.select{|reservation, status| status == "Not booked"} + puts available_in_block + puts available_in_block.class + if available_in_block.length == 0 + raise ArgumentError.new("This block has been fully booked, there are 0 rooms available") + end + available_reservations = [] + available_in_block.each do |reservation, status| + available_reservations.push(reservation) + end + available_rooms = available_reservations.map do |reservation| + reservation.room + end + return available_rooms + end + end + + def list_available_rooms(start_date, end_date) + if start_date.class == String + start_date = Date.strptime(start_date, "%m/%d/%Y") + end + if end_date.class == String + end_date = Date.strptime(end_date, "%m/%d/%Y") + end + occupied_rooms = [] + if @reservations != nil + @reservations.each do |reservation| + date = start_date.dup + until date == end_date do + if (start_date >= reservation.start_date && start_date < reservation.end_date) || (end_date > reservation.start_date && end_date <= reservation.end_date) + occupied_rooms.push(reservation.room) + break + end + date += 1 + end + end + end + rooms = @rooms.dup + occupied_rooms.each do |room| + rooms.delete(room) + end + if rooms.length == 0 + raise ArgumentError.new("Sorry! We are currently all booked for those days, pleae try again.") + end + return rooms + end + +end diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..d4907c4e9 --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,21 @@ +require 'date' + +class Reservation + attr_reader :room, :start_date, :end_date, :block, :reservation_id, :cost + ROOM_COST = 200 + DISCOUNT = 0.20 + + def initialize(start_date:, end_date:, room:, block:false, reservation_id:) + @start_date = Date.strptime(start_date, "%m/%d/%Y") + @end_date = Date.strptime(end_date, "%m/%d/%Y") + @room = room + @block = block + @reservation_id = reservation_id + @cost = @block == false ? ROOM_COST * (@end_date - @start_date).to_i : ROOM_COST * (1-DISCOUNT) * (@end_date - @start_date).to_i + + if end_date <= start_date || start_date == nil || end_date == nil + raise ArgumentError.new("Please enter a valid date range") + end + end + +end \ No newline at end of file diff --git a/refactors.txt b/refactors.txt new file mode 100644 index 000000000..ad5bd1dfe --- /dev/null +++ b/refactors.txt @@ -0,0 +1,3 @@ +List of changes that I would like to make: + - I would like to break apart my make_reservation class in to separate methods that are called my an “overall” make_reservation class. Currently, that class contains a lot of ifs/elses that make it confusing to follow and update when needed + - I would like to combine my block and reservation classes into one because there are limited differences between the two and this sticks to single the single responsibility of making reservations for rooms \ No newline at end of file diff --git a/test/block_test.rb b/test/block_test.rb new file mode 100644 index 000000000..1e0368de3 --- /dev/null +++ b/test/block_test.rb @@ -0,0 +1,24 @@ +require_relative 'test_helper' + +describe "Block class" do + + describe "Block instantiation" do + before do + @hotel = Hotel.new + @block = @hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:4) + end + + it "is an instance of Reservation" do + expect(@block).must_be_kind_of Block + end + + it "has correct attributes" do + [:reservations, :block_id].each do |prop| + expect(@block).must_respond_to prop + end + expect(@block.reservations).must_be_kind_of Hash + expect(@block.block_id).must_be_kind_of Integer + end + + end +end \ No newline at end of file diff --git a/test/hotel_test.rb b/test/hotel_test.rb new file mode 100644 index 000000000..94cecc974 --- /dev/null +++ b/test/hotel_test.rb @@ -0,0 +1,151 @@ +require_relative 'test_helper' + +describe "Hotel class" do + + describe "Hotel instantiation" do + before do + @hotel = Hotel.new() + end + + it "is an instance of Hotel" do + expect(@hotel).must_be_kind_of Hotel + end + + it "has correct attributes" do + [:rooms, :reservations].each do |prop| + expect(@hotel).must_respond_to prop + end + expect(@hotel.rooms).must_be_kind_of Array + expect(@hotel.rooms.length).must_equal 20 + expect(@hotel.reservations).must_be_kind_of Array + end + end + + describe "Makes solo reservations correctly and finds appropriate rooms" do + it "removes booked room from eligible room list" do + hotel = Hotel.new + @reservation = hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019") + room = @reservation.room + available_rooms = hotel.list_available_rooms("5/05/2019", "5/09/2019") + + expect(available_rooms.include?(room)).must_equal false + end + + it "raises argument error when there are no rooms available" do + hotel = Hotel.new + expect { 21.times do + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019") + end }.must_raise ArgumentError + end + + it "instantiates Reservation" do + hotel = Hotel.new + @reservation = hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019") + + expect(@reservation).must_be_kind_of Reservation + end + end + + describe "Find reservation by date" do + it "lists all reservation for a date range" do + hotel = Hotel.new + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019") + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019") + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019") + found_reservations = hotel.find_reservations_by_date("5/05/2019","5/09/2019") + + expect(found_reservations.length).must_equal 3 + end + + it "does not list reservations outside of date range" do + hotel = Hotel.new + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019") + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019") + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019") + hotel.make_reservation(start_date:"5/09/2019", end_date:"5/10/2019") + found_reservations = hotel.find_reservations_by_date("5/05/2019","5/09/2019") + + expect(found_reservations.length).must_equal 3 + end + end + + describe "Makes block reservations correctly" do + it "holds rooms for block" do + hotel = Hotel.new + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:3) + rooms_available = hotel.list_available_rooms("5/05/2019", "5/09/2019") + + expect(hotel.blocks[0].reservations.length).must_equal 3 + expect(hotel.reservations.length).must_equal 3 + expect(rooms_available.length).must_equal 17 + end + + it "raises argument error if block is for more than five rooms" do + hotel = Hotel.new + expect { hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:6) }.must_raise ArgumentError + end + + it "raises argument error if reservation and block are passed in together" do + hotel = Hotel.new + + expect { hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:3, block_id:2) }.must_raise ArgumentError + end + + it "removes blocked room from availability once booked" do + hotel = Hotel.new + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:3) + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:4) + reservation = hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", block_id:1) + + expect(reservation).must_equal "Booked" + + end + + it "does not allow room from block to be blocked if all are gone" do + hotel = Hotel.new + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:3) + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:4) + + expect{ 4.times do + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", block_id:1) + end }.must_raise ArgumentError + + end + + it "does not allow blocks rooms to be booked before they are reserved" do + hotel = Hotel.new + + expect{ hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", block_id:1) }.must_raise ArgumentError + + end + end + + describe "Find availability of block" do + it "lists all rooms available for a block" do + hotel = Hotel.new + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:4) + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:3) + + result1 = hotel.check_block_availability(block_id:1) + result2 = hotel.check_block_availability(block_id:2) + + expect(result1.length).must_equal 4 + expect(result2.length).must_equal 3 + end + + it "raises error if block does not exist" do + hotel = Hotel.new + + expect{ hotel.check_block_availability(block_id:1) }.must_raise ArgumentError + end + + it "raises error if there are no rooms available" do + hotel = Hotel.new + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:2) + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", block_id:1) + hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", block_id:1) + + expect{ hotel.check_block_availability(block_id:1) }.must_raise ArgumentError + end + end +end diff --git a/test/reservation_test.rb b/test/reservation_test.rb new file mode 100644 index 000000000..457dd4af5 --- /dev/null +++ b/test/reservation_test.rb @@ -0,0 +1,53 @@ +require_relative 'test_helper' + +describe "Reservation class" do + + describe "Reservation instantiation" do + before do + @hotel = Hotel.new + @reservation = Reservation.new(start_date:"5/05/2019", end_date:"5/09/2019", room:5, reservation_id:1) + @block = @hotel.make_reservation(start_date:"5/05/2019", end_date:"5/09/2019", hold_block:4) + end + + it "is an instance of Reservation" do + expect(@reservation).must_be_kind_of Reservation + end + + it "has correct attributes" do + [:start_date, :end_date].each do |prop| + expect(@reservation).must_respond_to prop + end + expect(@reservation.room).must_be_kind_of Integer + expect(@reservation.reservation_id).must_be_kind_of Integer + expect(@reservation.start_date).must_be_kind_of Date + expect(@reservation.end_date).must_be_kind_of Date + end + + it "calculates reservation cost correctly for non-block booking" do + expect(@reservation.cost).must_equal 800 + end + + it "calculates reservation cost correctly for block booking" do + expect(@hotel.reservations[2].cost).must_equal 640 + end + + end + + describe "Validates reservations dates" do + it "does not allow end date before start date" do + expect { Reservation.new(start_date:"5/14/2019", end_date:"5/08/2019") }.must_raise ArgumentError + end + + it "does not allow end date and start date to be the same" do + expect { Reservation.new(start_date:"5/08/2019", end_date:"5/08/2019") }.must_raise ArgumentError + end + + it "does not allow invalid dates" do + expect { Reservation.new(start_date:"14/08/2019", end_date:"5/08/2019") }.must_raise ArgumentError + end + + it "confirms reservation is at least one day" do + expect { Reservation.new(start_date:"5/08/2019", end_date:"5/08/2019") }.must_raise ArgumentError + end + end +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index c3a7695cf..655516040 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,8 +1,18 @@ -# Add simplecov -require "minitest" -require "minitest/autorun" -require "minitest/reporters" +require 'simplecov' +SimpleCov.start do + add_filter 'test/' # Tests should not be counted toward coverage. +end + +require 'minitest' +require 'minitest/autorun' +require 'minitest/reporters' +require 'minitest/skip_dsl' +require 'simplecov' +require 'date' + Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new -# require_relative your lib files here! +require_relative '../lib/hotel.rb' +require_relative '../lib/reservation.rb' +require_relative '../lib/block.rb'