diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..b69456a98 Binary files /dev/null and b/.DS_Store differ diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..363c2a26e --- /dev/null +++ b/Rakefile @@ -0,0 +1,11 @@ +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/lib/block.rb b/lib/block.rb new file mode 100644 index 000000000..148a77891 --- /dev/null +++ b/lib/block.rb @@ -0,0 +1,26 @@ +module Property + + class Block < Range + + attr_reader :rooms, :price, :availability, :reservations + + def initialize(rooms, check_in, check_out, price) + + raise ArgumentError.new "Cannot accomodate block reservation + for more than five rooms at a time." if rooms.length > 5 + raise ArgumentError.new "Must specify at least one room." if rooms.empty? + super(check_in, check_out) + @rooms = [] + @available = rooms.clone + @reservations = [] + @price = 150 #discounted + end + + def reserve_block(room) + raise ArgumentError.new "No availability" if @availability.empty? + reservation = @available.pop(room) + @reservations << reservation + return reservation + end + end + end diff --git a/lib/hotel.rb b/lib/hotel.rb new file mode 100644 index 000000000..2aa88bdd5 --- /dev/null +++ b/lib/hotel.rb @@ -0,0 +1,82 @@ +require_relative 'reservation' +require_relative 'block' +require_relative 'range' +require 'date' + +module Property + + class Hotel + + attr_reader :reservations, :rooms, :room_price + + def initialize + @rooms = (1..20).to_a + @room_price = 200 + @reservations = [] + @reserved_blocks = [] + end + + def list_reservations(dates) + @reservations.select { |rez| rez.contains(dates) } + end + + + def available(check_in, check_out) + dates = Range.new(check_in, check_out) + available = @rooms + + overlap_blocks = @reserved_blocks.select do |block| + block.overlap?(dates) + end + blocked_rooms = overlap_blocks.reduce([]) do |memo, block| + memo += block.rooms + end + available -= blocked_rooms + + overlap = @reservations.select do |rez| + rez.overlap?(dates) + end + already_reserved = overlap.map do |rez| + rez.room + end + available -= already_reserved + return available + end + + + def reserve_room(room, check_in, check_out) + raise ArgumentError.new "Room unavailable." unless @rooms.include? room + raise ArgumentError.new "Room #{room}is booked between + # #{check_in} and #{check_out}" unless available(check_in, check_out).include? room + + room_rez = Reservation.new(room, check_in, check_out, @room_price) + @reservations << room_rez + + return room_rez + end + + + def hotel_block(room_qty, check_in, check_out, price) + rooms = available(check_in, check_out) + + raise ArgumentError "Insufficient room availability" if rooms.length < room_qty + + block = Property::Block.new(rooms.first(room_qty), check_in, check_out, price) + rooms = available(check_in, check_out) + + if rooms.length < room_qty + raise ArgumentError("Not enough rooms available") + end + + @reserved_blocks << block + return block + end + + + def reserve_from_block(block) + room = block.reserve_room + room_rez = Reservation.new(room, block.check_in, block.check_out, block.price) + end + + end +end diff --git a/lib/range.rb b/lib/range.rb new file mode 100644 index 000000000..0f390627c --- /dev/null +++ b/lib/range.rb @@ -0,0 +1,41 @@ +require 'date' + +module Property + + class Range + + class InvalidDateRange < StandardError ; end + + attr_reader :check_in, :check_out + + def initialize(check_in, check_out) + + unless check_in < check_out + raise InvalidDateRange.new("Invalid date range") #return true ? + end + + @check_in = check_in + @check_out = check_out + end + + # def valid? + # unless @check_in < @check_out + # raise ArgumentError.new "Invalid date range" #return true ? + # end + # return true + # end + + def overlap?(date) + if @check_in < date.check_in && @check_out > date.check_out || + @check_in < date.check_in && @check_out < date.check_out + return false + else + return true + end + end + + def num_nights + return @check_out - @check_in + end + end +end diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..c2a202b8c --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,21 @@ +require_relative 'range' + +module Property + + class Reservation < Range + + attr_reader :room, :price + + def initialize(room, check_in, check_out, price) + @room = room + @price = 200 + # @dates = Property::Range.new(check_in, check_out) + # @total_price = @dates.num_nights * 200 + super(check_in, check_out) + end + + def total_price + return num_nights * @price + end + end +end diff --git a/specs/.DS_Store b/specs/.DS_Store new file mode 100644 index 000000000..785b8ddd4 Binary files /dev/null and b/specs/.DS_Store differ diff --git a/specs/block_spec.rb b/specs/block_spec.rb new file mode 100644 index 000000000..a9229a1d9 --- /dev/null +++ b/specs/block_spec.rb @@ -0,0 +1,35 @@ +require_relative 'spec_helper' +require_relative '../lib/block' + +describe 'Block' do + + before do + @check_in = Date.new(2017, 5, 5) + @check_out = @check_in + 3 + @price = 150 + end + + it "block is a subclass of Range"do + @rooms = [5, 12, 15] + @reserved_block = Property::Block.new(@rooms, @check_in, @check_out, @price) + @reserved_block.must_be_kind_of Property::Range + end + + it "A block can contain a maximum of 5 rooms" do + @rooms = [1, 2, 3, 4, 5, 6] + proc {@reserved_block = Property::Block.new(@rooms, @check_in, @check_out, @price)}.must_raise ArgumentError + end + + it "block cannot be booked if no rooms available" do + @rooms = [] + proc {@reserved_block = Property::Block.new(@rooms, @check_in, @check_out, @price)}.must_raise ArgumentError + end + + it "block rooms must appear in reservations once booked" do + @rooms = [5, 12, 15] + @reserved_block = Property::Block.new(@rooms, @check_in, @check_out, @price) + @reserved_block.rooms.each do |room| + @reservations.must_include room + end + end +end diff --git a/specs/hotel_spec.rb b/specs/hotel_spec.rb new file mode 100644 index 000000000..e6b2f449b --- /dev/null +++ b/specs/hotel_spec.rb @@ -0,0 +1,114 @@ +require_relative 'spec_helper' +require_relative '../lib/hotel' + +describe 'Hotel' do + + before do + @test_hotel = Property::Hotel.new + end + + it "can be initialized" do + @test_hotel.must_be_instance_of Property::Hotel + end + it "responds to its attr_reader" do + @test_hotel.must_respond_to :reservations + @test_hotel.must_respond_to :rooms + @test_hotel.must_respond_to :room_price + end + + it "can access the list of all of the rooms in the hotel" do + @test_hotel.rooms.must_equal (1..20).to_a + end + + it "there should be 20 total rooms" do + @test_hotel.rooms.count.must_equal 20 + end + + it "room always costs $200/night" do + @test_hotel.room_price.must_equal 200 + end +end + +describe "reserve_room" do + + before do + @test_hotel = Property::Hotel.new + @check_in = Date.new(2017, 05, 05) + @check_out = @check_in + 3 + end + + + it "cannot book invalid room number" do + room = "garbage" + proc {@test_hotel.reserve_room(room, @check_in, @check_out)}.must_raise ArgumentError + end + + it "raises an error if tried to double-book room " do + room = 5 + @test_hotel.reserve_room(room, @check_in, @check_out) + proc {@test_hotel.reserve_room(room, @check_in, @check_out)}.must_raise ArgumentError + end + + it "adds the reservation to @reservations" do + test_rez = @test_hotel.reserve_room(10, @check_in, @check_out) + @test_hotel.reservations.must_include test_rez + end + + it "can access the list of reservations for a specific date" do + rez = @test_hotel.reserve_room(15, @check_in, @check_out) + @test_hotel.reservations.must_include rez + end + + it "I can get the total cost for a given reservation" do + rez = @test_hotel.reserve_room(20, @check_in, @check_out) + rez.total_price.must_equal 600 + end + + it "I can view a list of rooms that are not reserved for a given date range" do + @test_hotel.reserve_room(3, @check_in, @check_out) + @test_hotel.available(@check_in, @check_out).include?(3).must_equal false + end + + it "can book a room that is available" do + excluding_one_room = @test_hotel.reserve_room(2, @check_in, @check_out) + available_room = @test_hotel.available(@check_in, @check_out).first + booking_available_room = @test_hotel.reserve_room(available_room, @check_in, @check_out) + @test_hotel.reservations.must_include booking_available_room + @test_hotel.reservations.length.must_equal 2 + end +end + +describe "block functionality" do + before do + @test_hotel = Property::Hotel.new + @room_qty = 3 + @check_in = Date.new(2017, 5, 5) + @check_out = @check_in + 3 + @price = 150 + # @block = @test_hotel.hotel_block(@room_qty, @check_in, @check_out, @price) + end + + it "cannot book zero rooms " do + proc {@test_hotel.hotel_block(0, @check_in, @check_out, @price)}.must_raise ArgumentError + end +end + # it "correct number of rooms booked in a block " do + # @block = @test_hotel.hotel_block(@room_qty, @check_in, @check_out, @price) + # @block.rooms.length.must_equal @room_qty + # end + + # it "once block booked, reserved_blocks reflects it" do + # @block = @test_hotel.hotel_block(@room_qty, @check_in, @check_out, @price) + # @reserved_blocks.must_include @block + # end + + + # it "reservation cannot be made for room when check in and checkout dates overlap" do + # rez = @test_hotel.reserve_room(3, @check_in, @check_out) + # rez2 = @test_hotel.reserve_room(3, @check_out, (@check_out+ 3)) + # @test_hotel.reservations.include?(rez2).must_equal false + # end + #this test fails. I need to strategize and decide how I'll make sure check in is + #not available on the day of check out for the same room. + #one way I am thinking of doing this is to set time, but that just introduces more + #complexity ... diff --git a/specs/range_spec.rb b/specs/range_spec.rb new file mode 100644 index 000000000..6358fb1bc --- /dev/null +++ b/specs/range_spec.rb @@ -0,0 +1,95 @@ +require_relative 'spec_helper' +require_relative '../lib/range' + + +describe 'Range' do + + it 'Can initialize check in and check out' do + check_in = Date.new(2017, 02, 01) + check_out = check_in + 2 + range = Property::Range.new(check_in, check_out) + range.check_in.must_equal check_in + range.check_out.must_equal check_out + end + + it "cannot have a negative date range" do + check_in = Date.new(2017, 02, 01) + check_out = check_in - 5 + proc {Property::Range.new(check_in, check_out)}.must_raise Property::Range::InvalidDateRange + end + + it "date range must not be 0" do + check_in = Date.new(2017, 02, 01) + check_out = check_in + proc {Property::Range.new(check_in, check_out)}.must_raise Property::Range::InvalidDateRange + end + + # it "returns true for valid date range " do + # check_in = Date.new(2017, 02, 01) + # check_out = check_in + 5 + # @range = Property::Range.new(check_in, check_out) + # @range.valid?must_equal true + # end + +end + +describe 'overlap?' do + + before do + check_in = Date.new(2017, 2, 1) + check_out = check_in + 3 + @range = Property::Range.new(check_in, check_out) + end + + it "returns true for date range that overlaps neatly" do + check_in = Date.new(2017, 2, 1) + check_out = check_in + 3 + other_range = Property::Range.new(check_in, check_out) + @range.overlap?(other_range).must_equal true + end + + it "returns false for date range that doesn't overlap" do + check_in = Date.new(2017, 5, 5) + check_out = check_in + 3 + other_range = Property::Range.new(check_in, check_out) + @range.overlap?(other_range).must_equal false + end + + it "returns true for date range that overlaps for shorter reservations" do + check_in = Date.new(2017, 2, 1) + check_out = check_in + 1 + other_range = Property::Range.new(check_in, check_out) + @range.overlap?(other_range).must_equal true + end + it "returns true for date range that overlaps for reservation with much earlier check in date" do + check_in = Date.new(2017, 1, 1) + check_out = Date.new(2017, 2, 2) + other_range = Property::Range.new(check_in, check_out) + @range.overlap?(other_range).must_equal true + end + + it "returns true for date range that overlaps for longer reservations" do + check_in = Date.new(2017, 2, 1) + check_out = check_in + 27 + other_range = Property::Range.new(check_in, check_out) + @range.overlap?(other_range).must_equal true + end + +end + +describe 'num_nights' do + + it "returns the correct number of nights booked" do + check_in = Date.new(2017, 5, 5) + check_out = Date.new(2017, 5, 8) + range = Property::Range.new(check_in, check_out) + range.num_nights.must_equal 3 + end + + it "error for 0 nights" do + check_in = Date.new(2017, 5, 5) + check_out = check_in + proc {Property::Range.new(check_in, check_out)}.must_raise Property::Range::InvalidDateRange + end + +end diff --git a/specs/reservation_spec.rb b/specs/reservation_spec.rb new file mode 100644 index 000000000..51c360670 --- /dev/null +++ b/specs/reservation_spec.rb @@ -0,0 +1,27 @@ +require_relative 'spec_helper' +require_relative '../lib/reservation' + +describe 'Reservation' do + before do + + @room = 2 + @checkin = Date.new(2017, 5, 5) + @checkout = @checkin + 3 + @price = 200 + @test_rez = Property::Reservation.new(@room, @checkin, @checkout, @price) + end + + it " responds to attr reader" do + @test_rez.must_respond_to :room + @test_rez.must_respond_to :price + end + + it "is a subclass of Range" do + @test_rez.must_be_kind_of Property::Range + end + + it "calculates correct total" do + @test_rez.total_price.must_equal 600 + end + +end diff --git a/specs/spec_helper.rb b/specs/spec_helper.rb new file mode 100644 index 000000000..e8d458517 --- /dev/null +++ b/specs/spec_helper.rb @@ -0,0 +1,14 @@ +require 'simplecov' +SimpleCov.start + +require 'minitest' +require 'minitest/autorun' +require 'minitest/reporters' + +Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new + +require 'date' +require_relative '../lib/reservation' +require_relative '../lib/hotel' +require_relative '../lib/range' +require_relative '../lib/block'