diff --git a/.gitignore b/.gitignore index 5e1422c9c..58b5cdce3 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ build-iPhoneSimulator/ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc +coverage +Guardfile 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..32e737a10 --- /dev/null +++ b/design-activity.md @@ -0,0 +1,41 @@ +1. Both implementations contains the same three classes--CartEntry, ShoppingCart, and Order. + +2. CartEntry - Responsible for a single entry in a ShoppingCart, tracks unit_price of an item and quantity of that item being purchased. + + ShoppingCart - Responsible for multiple CartEntries. + + Order - Responsible for calculating total price of a particular ShoppingCart + +3. CartEntry (one or many) + v + ShoppingCart (only one) + v + Order + +4. CartEntry - price of a single unit of an item, quantity of that item being purchased + ShoppingCart - holds multiple individual CartEntries + Order - sales tax constant, a single ShoppingCart + + The data stored by each class does not change between implementations + +5. CartEntry - #price returns the price of a quantity of some item + ShoppingCart - #price returns the price of all entries in the cart + Order - #total_price returns the cart price + sales tax + + Implementation A uses attr_accessor to make the data from CartEntry and ShoppingCart available to Order so it can calculate the total price, but Implementation B provides instance methods for each that return their respective prices, so that Order can simply call @cart.price then add sales tax. + +6. In Implementation B, the price logic is delegated to lower level classes, but in Implementation A it is not. + + total_price does directly manipulate the instance variables of other classes in Implementation A, but not in B. + +7. Ideally we would be working with Implementation B and could just add conditional functionality in CartEntry#price that affected unit price based on quantity. In Implementation A we would need to modify the code in Order#total_price, which is both unintuitive and messy. + +8. Implementation B, absolutely. I've made my case above... lol + +9. Once again, B. + + +----------- + + +In RoomBooker#new_block_reservation, the local `room` variable is used to modify the state of the Room struct, which belongs to the RoomBlock instance. If I were to handle this functionality instead within the RoomBlock class itself in an instance method, my classes would be even more loosely coupled and resilient to future change/easier to change in the future. diff --git a/lib/date_logic.rb b/lib/date_logic.rb new file mode 100644 index 000000000..485d3b9d1 --- /dev/null +++ b/lib/date_logic.rb @@ -0,0 +1,33 @@ +module DateLogic + def DateLogic.date_range_include?(reservation, date) + if (reservation.check_in...reservation.check_out).cover?(date) + return true + else + return false + end + end + + def DateLogic.date_ranges_exclusive?( + old_check_in, + old_check_out, + new_check_in, + new_check_out) + + existing_range_array = date_range_array(old_check_in, old_check_out) + + new_range_array = date_range_array(new_check_in, new_check_out) + + intersecting_dates = existing_range_array & new_range_array + + if intersecting_dates.empty? + return true + else + return false + end + end + + def DateLogic.date_range_array(check_in, check_out) + date_range = check_in...check_out + return date_range.to_a + end +end diff --git a/lib/no_rooms_in_block.rb b/lib/no_rooms_in_block.rb new file mode 100644 index 000000000..fb61ec8cb --- /dev/null +++ b/lib/no_rooms_in_block.rb @@ -0,0 +1,2 @@ +class NoRoomsInBlock < StandardError +end diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..05b40d7ad --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,23 @@ +module BookingLogic + class Reservation + attr_reader :room, :check_in, :check_out + + def initialize(room, check_in, check_out) + check_date_range(check_in, check_out) + @room = room + @check_in = check_in + @check_out = check_out + end + + def check_date_range(check_in, check_out) + if check_out <= check_in + raise StandardError, "Invalid date range provided" + end + end + + def reservation_cost + days_reserved = check_out - check_in + return days_reserved.to_i * room.cost + end + end +end diff --git a/lib/room.rb b/lib/room.rb new file mode 100644 index 000000000..4cdd90409 --- /dev/null +++ b/lib/room.rb @@ -0,0 +1,4 @@ +module BookingLogic + class Room + end +end diff --git a/lib/room_block.rb b/lib/room_block.rb new file mode 100644 index 000000000..1bb7ed538 --- /dev/null +++ b/lib/room_block.rb @@ -0,0 +1,44 @@ +require_relative 'no_rooms_in_block' + +module BookingLogic + class RoomBlock + attr_reader :name, :check_in, :check_out, :rooms, :rate + + def initialize(name, check_in, check_out, room_block, rate) + check_number_of_rooms(room_block) + @name = name + @check_in = check_in + @check_out = check_out + @rooms = room_block + @rate = rate + end + + def check_number_of_rooms(room_block) + if room_block.length > 5 + raise StandardError, "Maximum of 5 rooms per room block" + end + end + + def available + available_rooms = rooms.find_all { |room| room.block_reserved == nil } + + if available_rooms.empty? + raise NoRoomsInBlock, "No rooms currently available in this block" + else + return available_rooms + end + end + + def set_blocked_room_rate + rooms.each do |room| + room.cost = rate + end + end + + def reserve_room + room = self.available.first + room.block_reserved = true + return room + end + end +end diff --git a/lib/room_booker.rb b/lib/room_booker.rb new file mode 100644 index 000000000..6ec6421fc --- /dev/null +++ b/lib/room_booker.rb @@ -0,0 +1,169 @@ +require 'date' +require_relative 'date_logic' +require_relative 'reservation' +require_relative 'room_block' +require_relative 'room_not_available' + +module BookingLogic + class RoomBooker + Room = Struct.new(:id, :cost, :block_reserved) + + attr_reader :rooms + attr_accessor :reservations, :blocks + + def initialize + @rooms = populate_rooms + @reservations = [] + @blocks = [] + end + + def populate_rooms + rooms_array = [] + + 20.times do |id| + room = Room.new((id + 1), 200) + rooms_array << room + end + + return rooms_array + end + + def list_rooms + return rooms + end + + def find_room_by_id(room_id) + return rooms.find { |room| room.id == room_id } + end + + def list_reservations(date) + list_of_reservations = [] + + reservations.each do |reservation| + if DateLogic.date_range_include?(reservation, date) + list_of_reservations << reservation + end + end + + return list_of_reservations + end + + def list_available_rooms(check_in, check_out) + reserved_rooms = [] + + reservations.each do |reservation| + unless DateLogic.date_ranges_exclusive?( + reservation.check_in, + reservation.check_out, + check_in, + check_out + ) + + reserved_rooms << reservation.room + end + end + + unavailable_rooms = reserved_rooms + find_blocked_rooms(check_in, check_out) + + available_rooms = rooms.dup + + unavailable_rooms.each do |unavailable_room| + available_rooms.delete_if { |room| room.id == unavailable_room.id } + end + + return available_rooms + end + + def new_reservation(room_id, check_in, check_out) + room = find_room_by_id(room_id) + room_unavailable?(room, check_in, check_out) + new_reservation = BookingLogic::Reservation.new(room, check_in, check_out) + reservations << new_reservation + return new_reservation + end + + def room_unavailable?(room, check_in, check_out) + unless list_available_rooms(check_in, check_out).include?(room) + raise RoomNotAvailable, "Room not available for the given dates" + end + end + + def find_block(name) + found_block = blocks.find { |block| block.name == name } + return found_block + end + + def find_blocked_rooms(check_in, check_out) + blocked_rooms = [] + + blocks.each do |block| + unless DateLogic::date_ranges_exclusive?( + block.check_in, + block.check_out, + check_in, + check_out + ) + + block.rooms.each do |room| + blocked_rooms << room + end + end + end + + return blocked_rooms + end + + def block_available_rooms(check_in, check_out, number_of_rooms, rate) + + available_rooms = list_available_rooms(check_in, check_out) + + room_block = [] + + number_of_rooms.times do |i| + room_block << available_rooms[i].dup + end + + if room_block.length < number_of_rooms + raise StandardError, "There are not enough available rooms for the given dates to create this room block" + end + + return room_block + end + + def new_room_block(name, check_in, check_out, number_of_rooms, rate) + + block_of_rooms = block_available_rooms( + check_in, + check_out, + number_of_rooms, + rate + ) + + new_room_block = BookingLogic::RoomBlock.new( + name, + check_in, + check_out, + block_of_rooms, + rate + ) + + new_room_block.set_blocked_room_rate + + @blocks << new_room_block + return new_room_block + end + + def new_block_reservation(name) + block = find_block(name) + room = block.reserve_room + new_reservation = BookingLogic::Reservation.new(room, block.check_in, block.check_out) + reservations << new_reservation + return new_reservation + end + + def set_room_rate(room_id, custom_rate) + room = rooms.find { |room| room.id == room_id } + room.cost = custom_rate + end + end +end diff --git a/lib/room_not_available.rb b/lib/room_not_available.rb new file mode 100644 index 000000000..0d5b57552 --- /dev/null +++ b/lib/room_not_available.rb @@ -0,0 +1,2 @@ +class RoomNotAvailable < StandardError +end diff --git a/spec/date_logic_spec.rb b/spec/date_logic_spec.rb new file mode 100644 index 000000000..dba14d6d7 --- /dev/null +++ b/spec/date_logic_spec.rb @@ -0,0 +1,123 @@ +require_relative 'spec_helper.rb' + +describe 'Wave 1' do + describe 'DateLogic.date_range_include?' do + before do + @hotel = BookingLogic::RoomBooker.new + + room_id1 = 14 + check_in1 = Date.new(2018, 4, 1) + check_out1 = Date.new(2018, 4, 2) + @reservation1 = @hotel.new_reservation(room_id1, check_in1, check_out1) + + room_id2 = 15 + check_in2 = Date.new(2018, 4, 3) + check_out2 = Date.new(2018, 4, 6) + @reservation2 = @hotel.new_reservation(room_id2, check_in2, check_out2) + end + + it 'returns true when reservation date range includes given date' do + result = DateLogic.date_range_include?(@reservation1, Date.new(2018, 4, 1)) + expect(result).must_equal true + + result2 = DateLogic.date_range_include?(@reservation2, Date.new(2018, 4, 4)) + expect(result2).must_equal true + end + + it 'returns false when reservation date range does not include given date' do + result = DateLogic.date_range_include?(@reservation1, Date.new(2018, 4, 2)) + expect(result).must_equal false + + result2 = DateLogic.date_range_include?(@reservation2, Date.new(1992, 9, 11)) + expect(result2).must_equal false + + result3 = DateLogic.date_range_include?(@reservation2, Date.new(2018, 4, 6)) + expect(result3).must_equal false + end + end +end + +describe 'Wave 2' do + describe 'DateLogic.date_range_exclusive?' do + it 'returns true for completely non-intersecting date ranges' do + non_intersecting = DateLogic.date_ranges_exclusive?( + Date.new(2018, 4, 1), + Date.new(2018, 4, 3), + Date.new(2018, 4, 4), + Date.new(2018, 4, 18)) + + non_intersecting2 = DateLogic.date_ranges_exclusive?( + Date.new(2018, 3, 1), + Date.new(2018, 3, 16), + Date.new(2018, 2, 3), + Date.new(2018, 2, 28)) + + expect(non_intersecting).must_equal true + expect(non_intersecting2).must_equal true + end + + it 'returns false for back intersecting dates' do + intersecting = DateLogic.date_ranges_exclusive?( + Date.new(2018, 4, 1), + Date.new(2018, 4, 3), + Date.new(2018, 4, 2), + Date.new(2018, 4, 18)) + + expect(intersecting).must_equal false + end + + it 'returns false for front intersecting dates' do + intersecting = DateLogic.date_ranges_exclusive?( + Date.new(2018, 4, 1), + Date.new(2018, 4, 5), + Date.new(2018, 4, 4), + Date.new(2018, 4, 18)) + + expect(intersecting).must_equal false + end + + it 'returns false for encapsulated dates' do + encapsulated1 = DateLogic.date_ranges_exclusive?( + Date.new(2018, 4, 1), + Date.new(2018, 4, 21), + Date.new(2018, 4, 4), + Date.new(2018, 4, 18)) + + encapsulated2 = DateLogic.date_ranges_exclusive?( + Date.new(2018, 4, 1), + Date.new(2018, 4, 3), + Date.new(2018, 3, 2), + Date.new(2018, 4, 18)) + + expect(encapsulated1).must_equal false + expect(encapsulated2).must_equal false + end + + it 'returns false for same dates' do + same_dates = DateLogic.date_ranges_exclusive?( + Date.new(2018, 4, 1), + Date.new(2018, 4, 3), + Date.new(2018, 4, 1), + Date.new(2018, 4, 3)) + + expect(same_dates).must_equal false + end + + it 'returns true when reservations end/begin on checkout date' do + first_checkout = DateLogic.date_ranges_exclusive?( + Date.new(2018, 4, 1), + Date.new(2018, 4, 3), + Date.new(2018, 4, 3), + Date.new(2018, 4, 18)) + + second_checkout = DateLogic.date_ranges_exclusive?( + Date.new(2018, 4, 1), + Date.new(2018, 4, 3), + Date.new(2018, 3, 27), + Date.new(2018, 4, 1)) + + expect(first_checkout).must_equal true + expect(second_checkout).must_equal true + end + end +end diff --git a/spec/reservation_spec.rb b/spec/reservation_spec.rb new file mode 100644 index 000000000..1931d2061 --- /dev/null +++ b/spec/reservation_spec.rb @@ -0,0 +1,27 @@ +require_relative 'spec_helper.rb' + +describe 'Wave 1' do + before do + @hotel = BookingLogic::RoomBooker.new + + room_id1 = 14 + check_in1 = Date.new(2018, 4, 1) + check_out1 = Date.new(2018, 4, 2) + @reservation1 = @hotel.new_reservation(room_id1, check_in1, check_out1) + + room_id2 = 15 + check_in2 = Date.new(2018, 4, 3) + check_out2 = Date.new(2018, 4, 6) + @reservation2 = @hotel.new_reservation(room_id2, check_in2, check_out2) + end + + describe 'reservation_cost method' do + it 'finds the cost of a given reservation' do + reservation1 = @reservation1.reservation_cost + reservation2 = @reservation2.reservation_cost + + expect(reservation1).must_equal 200 + expect(reservation2).must_equal 600 + end + end +end diff --git a/spec/room_booker_spec.rb b/spec/room_booker_spec.rb new file mode 100644 index 000000000..6e85714d5 --- /dev/null +++ b/spec/room_booker_spec.rb @@ -0,0 +1,211 @@ +require_relative 'spec_helper.rb' +require 'pry' + +describe 'Wave 1' do + before do + @hotel = BookingLogic::RoomBooker.new + + room_id1 = 14 + check_in1 = Date.new(2018, 4, 1) + check_out1 = Date.new(2018, 4, 2) + @reservation1 = @hotel.new_reservation(room_id1, check_in1, check_out1) + + room_id2 = 15 + check_in2 = Date.new(2018, 4, 3) + check_out2 = Date.new(2018, 4, 6) + @reservation2 = @hotel.new_reservation(room_id2, check_in2, check_out2) + end + + describe 'list_rooms method' do + it 'lists all rooms in the hotel' do + expect(@hotel.list_rooms).must_be_kind_of Array + expect(@hotel.list_rooms.length).must_equal 20 + end + end + + describe 'new_reservation method' do + it 'adds a new reservation to master collection' do + expect(@hotel.reservations.first).must_be_instance_of BookingLogic::Reservation + end + end + + describe 'list_reservations method' do + it 'lists the reservations for a given date' do + given_date = Date.new(2018, 4, 1) + listed_reservations = @hotel.list_reservations(given_date) + + expect(listed_reservations).must_be_kind_of Array + # According to the before block there should only be one reservation on 4/1/2018 + expect(listed_reservations.length).must_equal 1 + listed_reservations.each do |reservation| + expect(reservation).must_be_instance_of BookingLogic::Reservation + end + end + end +end + +describe 'Wave 2' do + before do + @hotel = BookingLogic::RoomBooker.new + + room_id1 = 14 + check_in1 = Date.new(2018, 4, 1) + check_out1 = Date.new(2018, 4, 2) + @reservation1 = @hotel.new_reservation(room_id1, check_in1, check_out1) + + room_id2 = 15 + check_in2 = Date.new(2018, 4, 3) + check_out2 = Date.new(2018, 4, 6) + @reservation2 = @hotel.new_reservation(room_id2, check_in2, check_out2) + + room_id3 = 2 + check_in3 = Date.new(2018, 3, 21) + check_out3 = Date.new(2018, 4, 3) + @reservation3 = @hotel.new_reservation(room_id3, check_in3, check_out3) + end + + describe 'list_available_rooms method' do + before do + @array_of_rooms = @hotel.list_available_rooms( + Date.new(2018, 4, 1), Date.new(2018, 4, 4) + ) + end + + it 'returns an array of rooms' do + expect(@array_of_rooms).must_be_kind_of Array + @array_of_rooms.each do |room| + expect(room).must_be_kind_of Struct + expect(1..20).must_include room.id + expect(room.cost).must_equal 200 + end + end + + it 'only returns available rooms' do + room2 = @hotel.rooms.find { |room| room.id == 2 } + room14 = @hotel.rooms.find { |room| room.id == 14 } + room15 = @hotel.rooms.find { |room| room.id == 15 } + remaining_rooms = @hotel.rooms.find_all { |room| room.id != 14 && room.id != 2 && room.id != 15 } + + expect(@array_of_rooms).wont_include room2 + expect(@array_of_rooms).wont_include room14 + expect(@array_of_rooms).wont_include room15 + + remaining_rooms.each do |room| + expect(@array_of_rooms).must_include room + end + end + + it 'does not include blocked rooms' do + block = @hotel.new_room_block("fdsaj", Date.new(2018, 4, 1), Date.new(2018, 4, 6), 5, 150) + + block.rooms.each do |room| + expect(@array_of_rooms).wont_include room + end + end + end + + describe 'new_reservation method' do + before do + @hotel = BookingLogic::RoomBooker.new + + room_id1 = 1 + check_in1 = Date.new(2018, 4, 1) + check_out1 = Date.new(2018, 4, 2) + @reservation1 = @hotel.new_reservation(room_id1, check_in1, check_out1) + + room_id2 = 2 + check_in2 = Date.new(2018, 4, 3) + check_out2 = Date.new(2018, 4, 6) + @reservation2 = @hotel.new_reservation(room_id2, check_in2, check_out2) + + room_id3 = 3 + check_in3 = Date.new(2018, 3, 21) + check_out3 = Date.new(2018, 4, 3) + @reservation3 = @hotel.new_reservation(room_id3, check_in3, check_out3) + end + + it 'instantiates a new Reservation and adds it to master collection' do + expect(@reservation1).must_be_instance_of BookingLogic::Reservation + expect(@hotel.reservations).must_include @reservation1 + end + + it 'throws a StandardError when provided with invalid date range' do + expect{ @hotel.new_reservation(4, Date.new(2018, 4, 1), Date.new(2015, 5, 1)) }.must_raise StandardError + end + + it 'throws a RoomNotAvailable error when trying to reserve a room with a pre-existing reservation conflicting with given dates' do + expect{ @hotel.new_reservation(3, Date.new(2018, 4, 1), Date.new(2018, 4, 4)) }.must_raise RoomNotAvailable + end + + it 'throws a RoomNotAvailable error when all rooms in the hotel are booked' do + 20.times do |i| + @hotel.new_reservation((i + 1), Date.new(2019, 5, 1), Date.new(2019, 5, 4)) + end + + expect{ @hotel.new_reservation(14, Date.new(2019, 5, 1), Date.new(2019, 5, 4)) }.must_raise RoomNotAvailable + end + end +end + +describe 'Wave 3' do + before do + @hotel = BookingLogic::RoomBooker.new + + room_id1 = 14 + @check_in1 = Date.new(2018, 4, 1) + check_out1 = Date.new(2018, 4, 2) + @reservation1 = @hotel.new_reservation(room_id1, @check_in1, check_out1) + + room_id2 = 15 + check_in2 = Date.new(2018, 4, 3) + @check_out2 = Date.new(2018, 4, 6) + @reservation2 = @hotel.new_reservation(room_id2, check_in2, @check_out2) + + @name = "Gay Convention" + @room_rate = 160 + @number_of_rooms = 5 + @room_block = @hotel.new_room_block(@name, @check_in1, @check_out2, @number_of_rooms, @room_rate) + end + + describe 'new_room_block method' do + it 'instantiates a new RoomBlock' do + expect(@room_block).must_be_instance_of BookingLogic::RoomBlock + + # The following assertions based off of provided arguments in before block + expect(@room_block.name).must_equal @name + expect(@room_block.check_in).must_equal @check_in1 + expect(@room_block.check_out).must_equal @check_out2 + expect(@room_block.rooms.length).must_equal @number_of_rooms + expect(@room_block.rate).must_equal @room_rate + end + + it 'throws an error when creating a RoomBlock with more than 5 rooms' do + expect{ @hotel.new_room_block(@name, @check_in1, @check_out2, 6, @room_rate) }.must_raise StandardError + end + + it 'throws an error when there are not enough rooms left to create new RoomBlock' do + 2.times do |i| + @hotel.new_room_block("#{i}", @check_in1, @check_out2, 5, 150) + end + + expect{ @hotel.new_room_block("asldkfj", @check_in1, @check_out2, 5, 150) }.must_raise StandardError + end + end + + describe 'new_block_reservation method' do + it 'instantiates a new Reservation and adds it to master collection' do + reservation = @hotel.new_block_reservation(@name) + + expect(reservation).must_be_instance_of BookingLogic::Reservation + expect(@hotel.reservations).must_include reservation + end + + it 'throws a NoRoomsInBlock error when all rooms in that block have already been reserved' do + 5.times do + @hotel.new_block_reservation(@name) + end + + expect{ @hotel.new_block_reservation(@name) }.must_raise NoRoomsInBlock + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4d1e3fdc8..11cc09650 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,14 @@ +# Add simplecov +require 'simplecov' +SimpleCov.start + require 'minitest' require 'minitest/autorun' require 'minitest/reporters' -# Add simplecov + +require_relative '../lib/room_booker' +require_relative '../lib/reservation.rb' +require_relative '../lib/room_block.rb' Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new