diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..2e875f0d2 Binary files /dev/null and b/.DS_Store differ diff --git a/RakeFile b/RakeFile new file mode 100644 index 000000000..082edecca --- /dev/null +++ b/RakeFile @@ -0,0 +1,9 @@ +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.libs = ["lib"] + t.warning = true + t.test_files = FileList['specs/spec_*.rb'] +end + +task default: :test diff --git a/design-activity.md b/design-activity.md new file mode 100644 index 000000000..2d8fc7481 --- /dev/null +++ b/design-activity.md @@ -0,0 +1,55 @@ +What classes does each implementation include? Are the lists the same? + Both A & B have classes: CartEntry, ShoppingCart, and Order. Yes, the list of classes are the same for both implementations. + +Write down a sentence to describe each class. + CartEntry: Tracks the quantity of each item ordered and their individual item costs. B has a method to calculate cost of items. + + ShoppingCart: Tracks the items ordered, can calculate the subtotal or order. B has a method to calculate running subtotal. + + Order: Calculates the total for each order, B does relies on its other classes and their methods to do the logic required. + +How do the classes relate to each other? It might be helpful to draw a diagram on a whiteboard or piece of paper. + Order class creates a shopping cart that has an instance variable entries that can store instances of Cart entries. This is then used to calculate total price with the order class' total_price instance method. + + +What data does each class store? How (if at all) does this differ between the two implementations? + CartEntry: stores number quantity of an item and item unit price. Implementation B also has a method to return the price accounting only for item quantity. + + ShoppingCart: Stores instance of CartEntry. Implementation B calculates subtotal of all items in a cart. + + Order: creates instances of ShoppingCart and calculates the total_price of all items. ShoppingCart is an array that can hold instances of CartEntry. + + +What methods does each class have? How (if at all) does this differ between the two implementations? + CartEntry & ShoppingCart Implementation A: no instance methods, except for initialize. + + CartEntry & ShoppingCart Implementation B: has 2 helper methods to calculate subtotal of instance variables values. + + Order Implementation A & B: Stores instance of CartEntry. Implementation B calculates subtotal of all items in a cart. + +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? + Implementation A: It is computed in Order. + + Implementation B: It is completed in all the classes. Total base price per item in CartEntry. Subtotal for all entries in ShoppingCart is called in ShoppingCart. Order does the final calculates the total or the order. + +Does total_price directly manipulate the instance variables of other classes? +If we decide items are cheaper if bought in bulk, how would this change the code? Which implementation is easier to modify? + Implementation A: Yes it acts on the CartEntry class using instance variables unit_price and quantity. + + Implementation B: Yes it acts directly on the ShoppingCart instance using the price instance method of the ShoppingCart class. + +Which implementation better adheres to the single responsibility principle? + Implementation B because it only calculates the total price based on the subtotal returned by the ShoppingCart price instance method and shares the logic required for calculating the total across all of the classes. + +Bonus question once you've read Metz ch. 3: Which implementation is more loosely coupled? + + Implementation B because it separates uses a local variable subtotal and instance variable #@cart to shield the ShoppingCart instance from the calculation done by total_price method of Order. + + +Revisiting Hotel Activity + There are a couple of places where I violate the single responsibility rule for classes. + + I chose to remove the instance variable price in admin and use a local variable to store the block room prices. This did not require any changes to my testing. + + I also decided to change the block_add_reservation instance method in admin and move the logic and making of new reservation into block_room class. The tests pass still. diff --git a/lib/admin.rb b/lib/admin.rb new file mode 100644 index 000000000..cc9047358 --- /dev/null +++ b/lib/admin.rb @@ -0,0 +1,94 @@ +require 'date' +require_relative 'reservation' +require_relative 'block_room' + +class Admin + attr_reader :roomlist, :reservations, :blocks + + def initialize + @roomlist = [] + @reservations = [] + + #array of block instances + @blocks = [] + + 20.times do |i| + @roomlist << (i + 1) + end + end + + def add_reservation(check_in, check_out, room_id) + available_rooms = available_rooms(check_in, check_out) + + if !(available_rooms.include?(room_id)) + raise ArgumentError.new("#{room_id} is booked for these dates") + end + + if available_rooms.count == 0 + raise ArgumentError.new("No available roooms") + end + + @reservations << Reservation.new(check_in, check_out, room_id) + end + + + def available_rooms(check_in, check_out) + + check_in = Date.parse(check_in) + check_out = Date.parse(check_out) + + rooms = @roomlist.dup + + @reservations.each do |reservation| + valid_check_in = check_in.between?(reservation.start_date, (reservation.end_date - 1)) + + + valid_check_out = check_out.between?((reservation.start_date + 1), reservation.end_date) + + if valid_check_in && valid_check_out + rooms.delete(reservation.room_id) + end + end + return rooms + end + + def new_block(check_in, check_out, number_of_rooms, rate) + block_rooms_ids = [] + + rooms_available = available_rooms(check_in, check_out) + + if number_of_rooms <= 5 && rooms_available.count >= number_of_rooms + number_of_rooms.times do + block_rooms_ids << rooms_available[0] + end + + block = BlockRoom.new(check_in, check_out, rate, block_rooms_ids) + + @blocks << block + else + raise ArgumentError.new("Can only block 5 rooms at a time. Can only block available rooms. There are #{available_rooms.count} rooms.") + end + end + + # Modified this method + def block_add_reservation(block) + new_rez = block.reserve_block + + @reservations << new_rez + end + + def bookings_by_date(date) + bookings_by_day = [] + + date = Date.parse(date) + + @reservations.each do |booking| + if date.between?(booking.start_date, booking.end_date) + bookings_by_day << booking + end + end + + return bookings_by_day + end + +end diff --git a/lib/block_room.rb b/lib/block_room.rb new file mode 100644 index 000000000..85226cc6a --- /dev/null +++ b/lib/block_room.rb @@ -0,0 +1,58 @@ +require 'date' +require_relative 'reservation' + +class BlockRoom + + attr_reader :reservations, :blocked_rooms_ids, :check_in, :check_out, :rate + + def initialize(check_in, check_out, rate, blocked_rooms_ids) + if blocked_rooms_ids.count > 5 || blocked_rooms_ids.count < 1 || rate >= 200 + raise ArgumentError.new("Room Blocks must range 1 to 5 rooms, rate must be <200") + end + + @check_in = Date.parse(check_in) + @check_out = Date.parse(check_out) + @rate = rate + @reservations = [] + + # an array of blocked room's ids + @blocked_rooms_ids = blocked_rooms_ids + end + + def add_reservation(reservation) + if @reservations.count > 4 || !@blocked_rooms_ids.include?(reservation.room_id) + raise ArgumentError.new("Room Block") + else + @reservations << reservation + end + end + + # Valid to make this block? + def new_block(check_in, check_out, number_of_rooms, rate) + block_rooms_ids = [] + + rooms_available = available_rooms(check_in, check_out) + + if number_of_rooms <= 5 && rooms_available.count >= number_of_rooms + number_of_rooms.times do + block_rooms_ids << rooms_available[0] + end + + block = BlockRoom.new(check_in, check_out, rate, block_rooms_ids) + + @blocks << block + else + raise ArgumentError.new("Can only block 5 rooms at a time. Can only block available rooms. There are #{available_rooms.count} rooms.") + end + end + + # Modified this method + def reserve_block + new_rez = Reservation.new(@block.check_in.to_s, @block.check_out.to_s, @block.blocked_rooms_ids[0]) + + @block.add_reservation(new_rez) + + return new_rez + end + +end diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..b8a34e40e --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,47 @@ +require 'date' + +class Reservation + + attr_reader :end_date, :start_date, :stay, :cost, :room_id + + def initialize(start_date, end_date, room_id) + valid = true + if valid_dates?(start_date, end_date) + @start_date = Date.parse(start_date) + @end_date = Date.parse(end_date) + else + valid = false + end + stay_length if valid + + if !@stay || !valid + raise ArgumentError.new("Invalid date(s)") + end + + @room_id = room_id + end + + def valid_dates?(start_date, end_date) + valid = true + begin + Date.parse(start_date) + Date.parse(end_date) + rescue + valid = false + end + return valid + end + + def stay_cost + @cost = @stay * 200 + end + + def stay_length + @stay = false + date_order = @start_date <=> @end_date + if date_order < 0 + @stay = (@end_date.mjd - @start_date.mjd) + stay_cost + end + end +end diff --git a/specs/spec_admin.rb b/specs/spec_admin.rb new file mode 100644 index 000000000..d7bbb96e4 --- /dev/null +++ b/specs/spec_admin.rb @@ -0,0 +1,293 @@ +require_relative 'spec_helper' + +describe 'admin initialize' do + before do + @admin_test = Admin.new + end + + it "admin initializes and returns a single instance of admin" do + @admin_test.must_be_instance_of Admin + end + + it "roomlist instance variable returns list of 20 room ids" do + @admin_test.roomlist.must_be_instance_of Array + @admin_test.roomlist.count.must_equal 20 + @admin_test.roomlist[8].must_be_instance_of Integer + end +end + +describe 'add_reservation' do + before do + @date1 = '2018-1-10' + @date2 = '2018-3-5' + + @admin_test = Admin.new + + @admin_test.add_reservation(@date1, @date2, 1) + + @bookings = @admin_test.reservations + + end + + it "reservations returns list of reservation instances" do + @bookings.must_be_instance_of Array + @bookings[0].must_be_instance_of Reservation + end + + it "can access room id from reservation instance." do + @bookings[0].room_id.must_equal 1 + end + + it "calculates the length of stay" do + @admin_test.reservations[0].stay.must_equal(Date.parse(@date2).mjd - Date.parse(@date1).mjd) + end + + it "every new reserservation increases reservations list by one" do + admin_test_2 = Admin.new + + bookings_2 = admin_test_2.reservations.count + + admin_test_2.add_reservation(@date1, @date2, 1) + + admin_test_2.reservations.count.must_equal(bookings_2 + 1) + end + + it "gives the price of new reservation." do + admin_test_2 = Admin.new + + bookings_2 = admin_test_2.reservations + + admin_test_2.add_reservation(@date1, @date2, 1) + + admin_test_2.reservations[0].cost.must_equal((Date.parse(@date2).mjd - Date.parse(@date1).mjd) * 200) + end + + it "gives the room_id of new reservation from reservations array" do + admin_test_2 = Admin.new + + bookings_2 = admin_test_2.reservations + + admin_test_2.add_reservation(@date1, @date2, 1) + + admin_test_2.add_reservation(@date1, @date2, 2) + + admin_test_2.add_reservation(@date1, @date2, 3) + + admin_test_2.reservations[1].room_id.must_equal 2 + end + + it "takes reservation check in and check out date, assigns a room and makes a reservation" do + admin_test_2 = Admin.new + + admin_test_2.bookings_by_date(@date1).include?(1).must_equal false + + admin_test_2.add_reservation(@date1, @date2, 1) + + admin_test_2.bookings_by_date(@date1)[0].room_id.must_equal 1 + end + + it "assigns available room" do + admin_test_2 = Admin.new + + admin_test_2.available_rooms(@date1, @date2).include?(1).must_equal true + + admin_test_2.add_reservation(@date1, @date2, 1) + + admin_test_2.available_rooms(@date1, @date2).include?(1).must_equal false + + end + + it "no reservations allowed in fully booked hotel" do + admin_test_2 = Admin.new + + 20.times do |index| + admin_test_2.add_reservation(@date1, @date2, (index + 1)) + end + + proc { + admin_test_2.add_reservation(@date1 << 5, @date2 << 5, 1)}.must_raise ArgumentError + end + + it "raises an error when room is already booked for given date range" do + admin_test_2 = Admin.new + + rez1 = admin_test_2.add_reservation(@date1, @date2, 11) + + proc { + rez2 = admin_test_2.add_reservation(@date1, @date2, 11) + }.must_raise ArgumentError + + end + + describe "bookings_by_date" do + + before do + booking_dates = [ + ['2018-1-10', '2018-3-5'], ['2018-1-19', '2018-5-2'], ['2018-1-10', '2018-5-5'] + ] + + @admin_test = Admin.new + + booking_dates.each do |dates| + @admin_test.add_reservation(dates[0], dates[1], 1) + end + + @date1 = '2018-1-15' + end + + it "returns an array" do + bookings_on_test_day = @admin_test.bookings_by_date(@date1) + + bookings_on_test_day.must_be_instance_of Array + bookings_on_test_day[0].must_be_instance_of Reservation + end + + it "returns array of reservation instances" do + bookings_on_test_day = @admin_test.bookings_by_date(@date1) + + bookings_on_test_day.must_be_instance_of Array + bookings_on_test_day[0].must_be_instance_of Reservation + end + + it "reservation check_in/check_out date range includes date argument input" do + bookings_on_test_day = @admin_test.bookings_by_date(@date1) + + Date.parse(@date1).between?(bookings_on_test_day[1].start_date, bookings_on_test_day[1].end_date).must_equal true + end + + it "returns an empty array if no reservation dates intersect with booking date argument given" do + bookings_on_test_day = @admin_test.bookings_by_date('2019-1-10') + + bookings_on_test_day.must_be_instance_of Array + + bookings_on_test_day.count.must_equal 0 + end + end + + describe "available_rooms" do + before do + booking_dates = [ + ['2018-1-10', '2018-3-5'], ['2018-1-19', '2018-5-2'], ['2018-1-10', '2018-5-5'] + ] + + @admin_test = Admin.new + + room_id = 1 + + booking_dates.each do |dates| + @admin_test.add_reservation(dates[0], dates[1], room_id) + room_id += 1 + end + end + + it "takes valid checkin/checkout dates returns an array of room ids available for the date range" do + rooms = @admin_test.available_rooms('2018-1-5', '2018-1-9') + rooms.count.must_equal 20 + end + + it "rooms are available for new reservation check in on last day of existing reservation" do + rooms = @admin_test.available_rooms('2018-1-5', '2018-1-10') + rooms.count.must_equal 20 + end + + it "rooms are available for new reservation checkout on first day of existing reservation" do + rooms = @admin_test.available_rooms('2018-5-5', '2018-5-10') + rooms.count.must_equal 20 + end + + it "returns list of available rooms without reservations dates that coincide with new reservation date range." do + rooms = @admin_test.available_rooms('2018-1-21', '2018-2-15') + rooms.count.must_equal 17 + end + + it "returns empty array of available rooms if hotel is fully booked during new reservation date range." do + + date1 = '2018-1-10' + date2 = '2018-3-5' + + full_hotel_test = Admin.new + + 20.times do |index| + full_hotel_test.add_reservation(date1, date2, index + 1) + end + + rooms = full_hotel_test.available_rooms('2018-1-21', '2018-2-15') + + rooms.count.must_equal 0 + + rooms.must_be_instance_of Array + end + end + + # describe "new_block" do + # before do + # @date1 = '2018-1-10' + # @date2 = '2018-3-5' + # + # @room_block_ids = [1, 2, 3, 4, 5] + # + # @rate = 150 + # + # @new_admin = Admin.new + # + # @new_admin.new_block(@date1, @date2, 5, @rate) + # + # @test_block = @new_admin.blocks[0] + # end + # + # it "makes a new block instance" do + # @test_block.must_be_instance_of BlockRoom + # end + # + # it "stores the new block instance in a hash and assigns it an id (key)" do + # @new_admin.blocks[0].must_be_instance_of BlockRoom + # end + # + # it "rooms assigned to block are not available during block date range" do + # @new_admin.available_rooms(@date1, @date2).include?(@room_block_ids[0]).must_equal true + # + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # @new_admin.available_rooms(@date1, @date2).include?(@room_block_ids[0]).must_equal false + # end + # + # it "each reservation within the block has the same check_in date" do + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # check_in_1 = @new_admin.blocks[0].reservations[0].start_date + # + # check_in_2 = @new_admin.blocks[0].reservations[2].start_date + # check_in_1.must_equal(check_in_2) + # end + # + # it "each reservation within the block has the same check_out date" do + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # check_out_1 = @new_admin.blocks[0].reservations[0].end_date + # + # check_out_2 = @new_admin.blocks[0].reservations[2].end_date + # check_out_1.must_equal(check_out_2) + # end + # + # it "each reservation within the block has the same cost" do + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # @new_admin.block_add_reservation(@new_admin.blocks[0]) + # + # cost_1 = @new_admin.blocks[0].reservations[0].cost + # + # cost_2 = @new_admin.blocks[0].reservations[2].cost + # cost_1.must_equal(cost_2) + # end + # end +end diff --git a/specs/spec_block.rb b/specs/spec_block.rb new file mode 100644 index 000000000..3609ffd7a --- /dev/null +++ b/specs/spec_block.rb @@ -0,0 +1,109 @@ +require_relative 'spec_helper' + +describe 'blockroom initialize' do + before do + @date1 = '2018-1-10' + @date2 = '2018-3-5' + + @room_block_ids = [1, 2, 3, 4, 5] + + @rate = 150 + + @blockroom_test = BlockRoom.new(@date1, @date2, @rate, @room_block_ids.dup) + end + + it "initializes and returns a single instance of BlockRoom" do + @blockroom_test.must_be_instance_of BlockRoom + end + + it "check_in/check_out instance variables are of date class" do + @blockroom_test.check_in.must_be_instance_of Date + @blockroom_test.check_out.must_be_instance_of Date + end + + it "add reservation adds reservation to reservations list" do + before_rezs = @blockroom_test.reservations.dup + + 5.times do |index| + @blockroom_test.add_reservation(Reservation.new(@date1, @date2, @room_block_ids[index])) + end + + count_compare = @blockroom_test.reservations.count > before_rezs.count + + count_compare.must_equal true + end + + it "raises argument if block is full and add_reservation is called." do + test_block = BlockRoom.new(@date1, @date2, @rate, @room_block_ids.dup) + + 5.times do |index| + test_block.add_reservation(Reservation.new(@date1, @date2, @room_block_ids[index])) + end + + proc { + test_block.add_reservation(Reservation.new(@date1, @date2, @room_block_ids[3])) + }.must_raise ArgumentError + end + + describe "reservation in blocked room" do + before do + @date1 = '2018-1-10' + @date2 = '2018-3-5' + + @rate = 150 + end + + it "makes a new reservation instance" do + room_block_ids = [1, 2, 3, 4, 5] + + test_block = BlockRoom.new(@date1, @date2, @rate, room_block_ids.dup) + + 5.times do |index| + test_block.add_reservation(Reservation.new(@date1, @date2, room_block_ids[index])) + end + + test_block.reservations[0].must_be_instance_of Reservation + end + + it "raises error if user tries to make a new reservation instance using invalid room id" do + room_block_ids = [1, 2, 3, 4, 5] + + test_block = BlockRoom.new(@date1, @date2, @rate, room_block_ids) + + proc { + test_block.add_reservation(Reservation.new(@date1, @date2, 19)) + }.must_raise ArgumentError + end + + it "increases the number of reservations in admin" do + room_block_ids = [1, 2, 3, 4, 5] + + test_block = BlockRoom.new(@date1, @date2, @rate, room_block_ids.dup) + + rezs_before = test_block.reservations.dup + + 5.times do |index| + test_block.add_reservation(Reservation.new(@date1, @date2, room_block_ids[index])) + end + + count_compare = test_block.reservations.count > rezs_before.count + + count_compare.must_equal true + end + + it "makes a reservation with the rooom set aside for blocked room" do + room_block_ids = [1, 2, 3, 4, 5] + + blocked_room_ids = room_block_ids.dup + + test_block = BlockRoom.new(@date1, @date2, @rate, room_block_ids.dup) + + 5.times do |index| + test_block.add_reservation(Reservation.new(@date1, @date2, room_block_ids[index])) + end + + blocked_room_ids.include?(test_block.reservations[2].room_id).must_equal true + end + end + +end diff --git a/specs/spec_helper.rb b/specs/spec_helper.rb new file mode 100644 index 000000000..70cf680ce --- /dev/null +++ b/specs/spec_helper.rb @@ -0,0 +1,17 @@ +require 'simplecov' +SimpleCov.start + +require 'time' +require 'date' +require 'pry' +require 'minitest' +require 'minitest/autorun' +require 'minitest/reporters' +require 'minitest/pride' + +Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new + +# Require_relative your lib files here! +require_relative '../lib/reservation' +require_relative '../lib/admin' +require_relative '../lib/block_room' diff --git a/specs/spec_reservation.rb b/specs/spec_reservation.rb new file mode 100644 index 000000000..3a7487674 --- /dev/null +++ b/specs/spec_reservation.rb @@ -0,0 +1,75 @@ +require_relative 'spec_helper' + +describe 'reservation' do + before do + @date1 = '2018-1-10' + @date2 = '2018-3-5' + end + + # NOMINAL + it "initializes and returns a single instance of reservation" do + test = Reservation.new(@date1, @date2, 1) + test.must_be_instance_of Reservation + end + + it "takes in two dates and calculates length of stay" do + test = Reservation.new(@date1, @date2, 1) + + test_dates = (Date.parse(@date2)).mjd - (Date.parse(@date1)).mjd + + test.stay.must_equal(test_dates) + end + + # EDGE TESTING + it "raises error if end_date & start_date are the same" do + proc { + Reservation.new("2018-1-1", "2018-1-1", 1)}.must_raise ArgumentError + end + + it "stay_length is 1 if end_date & start_date are one day apart" do + test_3 = Reservation.new("2018-1-1", "2018-1-2", 1) + test_3.stay.must_equal 1 + end + + it "stay_length is 365 if end_date & start_date are one year apart" do + test_3 = Reservation.new("2018-1-1", "2019-1-1", 1) + test_3.stay.must_equal 365 + end + + it "raises error if end date preceeds start date" do + proc { + Reservation.new(@date2, @date1, 1)}.must_raise ArgumentError + end + + it "raises error if either start or end start date objects are nil" do + proc { + Reservation.new(nil, @date2, 1)}.must_raise ArgumentError + + proc { + Reservation.new(@date2, nil, 1)}.must_raise ArgumentError + end + + it "raises error if both start or end start date objects are nil" do + proc { + Reservation.new(nil, nil, 1)}.must_raise ArgumentError + end + + it "raises error if either start or end start date objects are non-string values" do + proc { + Reservation.new(0, @date2, 1)}.must_raise ArgumentError + + proc { + Reservation.new(@date2, 100, 1)}.must_raise ArgumentError + + proc { + Reservation.new(@date2, :test_date, 1)}.must_raise ArgumentError + end + + it "raises error if either start or end start date strings arguments can't be made into Date objects" do + proc { + Reservation.new("test_string", @date2, 1)}.must_raise ArgumentError + + proc { + Reservation.new(@date2, "test_string", 1)}.must_raise ArgumentError + end +end