diff --git a/.gitignore b/.gitignore index 5e1422c9c..df88feab6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ + + *.gem *.rbc /.config @@ -48,3 +50,4 @@ build-iPhoneSimulator/ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc +coverage diff --git a/lib/date.rb b/lib/date.rb new file mode 100644 index 000000000..1e3ea67ea --- /dev/null +++ b/lib/date.rb @@ -0,0 +1,22 @@ +require 'date' + +require_relative 'frontdesk.rb' +require_relative 'reservation.rb' + +module Hotel + class DateRange + + def contains(date) + (date >= @start_date && date < @end_date) ? true : false + end + + def no_conflict?(new_start, new_end) + if @end_date == new_start || new_start > @end_date || new_end < @start_date || new_end == @start_date + return true + elsif new_start >= @start_date && new_start < @end_date || new_end > @start_date + return false + end + end + + end +end \ No newline at end of file diff --git a/lib/frontdesk.rb b/lib/frontdesk.rb new file mode 100644 index 000000000..3fc246735 --- /dev/null +++ b/lib/frontdesk.rb @@ -0,0 +1,85 @@ +require 'date' + +require_relative 'date.rb' +require_relative 'reservation.rb' + +module Hotel + class FrontDesk + + attr_reader :all_reservations, :rooms + + def initialize + @all_reservations = all_reservations || [] + @rooms = [*1..20] + end + + def assign_room(new_reservation) + taken_rooms = [] + all_rooms = @rooms.dup + @all_reservations.each do |reservation| + reservation.no_conflict?(new_reservation.start_date, new_reservation.end_date) ? taken_rooms << reservation.assigned_room : next + end + room_to_assign = (all_rooms - taken_rooms.flatten) + assigning = room_to_assign.sample(new_reservation.num_rooms) + taken_rooms.include?(assigning) + assigning = room_to_assign.sample(new_reservation.num_rooms) + end + + def add_reservation(start_date, end_date, num_rooms) + new_reservation = Hotel::Reservation.new(start_date: start_date, end_date: end_date, num_rooms: num_rooms) + new_reservation.assigned_room = assign_room(new_reservation) + if new_reservation.num_rooms > 1 + raise ArgumentError.new("Invalid room reservation request: if you would still like #{num_rooms} rooms please reserve as a block.") + end + @all_reservations << new_reservation + end + + def add_block_reservation(start_date, end_date, num_rooms, discount, block_key) + new_reservation = Hotel::Reservation.new(start_date: start_date, end_date: end_date, num_rooms: num_rooms, discount: discount, block_key: block_key) + new_reservation.assigned_room = assign_room(new_reservation) + if new_reservation.num_rooms > 1 + new_reservation.block = :BLOCK + elsif new_reservation.num_rooms == 1 + raise ArgumentError.new("Invalid room reservation request: if you would still like #{num_rooms} room please reserve as a single.") + end + @all_reservations << new_reservation + end + + def find_reservation_by_date(date) + @all_reservations.select {|reservation| reservation.contains(date)} + end + + def find_available_room_by_date(date) + taken = [] + all_rooms = @rooms.dup + booked = find_reservation_by_date(date) + booked.map {|reservation| taken << reservation.assigned_room} + available_rooms = (all_rooms - taken.flatten) + end + + def find_block_by_block_key(date, block_key) + reservations_on_day = find_reservation_by_date(date) + in_block = reservations_on_day.select {|reservation| reservation.block_key == block_key} + return in_block + end + + def update_block(date, block_key) + found_block = find_block_by_block_key(date, block_key) + if found_block[0].num_rooms == 0 + raise ArgumentError.new("Invalid room reservation request: all rooms in this block have been reserved.") + else + found_block[0].num_rooms -= 1 + end + return found_block[0] + end + + # * I can reserve a specific room from a hotel block + # * I can only reserve that room from a hotel block for the full duration of the block + def reserve_from_block(date, block_key) + book_room = update_block(date, block_key) + book_room.assigned_room = book_room.assigned_room[0] + book_room.num_rooms = 1 + return book_room + end + end +end \ No newline at end of file diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..f55138a8b --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,53 @@ +require 'date' + + +require_relative 'frontdesk.rb' +require_relative 'date.rb' + +module Hotel + class Reservation < DateRange + attr_accessor :start_date, :end_date, :num_rooms, :assigned_room, :discount, :block, :block_key + + COST = 200.00 + + def initialize(start_date:, end_date:, num_rooms:, discount: nil, block: :SINGLE, block_key: nil) + @start_date = start_date + @end_date = end_date + @num_rooms = num_rooms + @assigned_room = [] + @discount = discount + @block = block + @block_key = block_key + + raise ArgumentError.new("Invalid times: #{@start_date} comes after #{@end_date}") if (@start_date > @end_date || @start_date == @end_date) + + raise ArgumentError.new("Invalid room request: provided #{@num_rooms} instead of an Integer") if @num_rooms.class != Integer + + raise ArgumentError.new("Invalid room request: requested #{@num_rooms} rooms, #{@num_rooms-5} too many.") if @num_rooms > 5 + + raise ArgumentError.new("Invalid block key. Not valid for single room reservations") if @num_rooms == 1 && block_key != nil + + raise ArgumentError.new("Status and room argument: #{@num_rooms} room, should not be noted as a block") if (@num_rooms == 1 && @block == :BLOCK) + + raise ArgumentError.new("Invalid status. Please choose: single or block.") unless [:SINGLE, :BLOCK].include? block + end + + def total_cost + @num_rooms == 1 ? final_bill = (@end_date - @start_date)*@num_rooms*COST : final_bill = (@end_date - @start_date)*@num_rooms*COST*(1 - @discount) + return final_bill + end + + #DateRange class originally lived here as methods + # def contains(date) + # (date >= @start_date && date < @end_date) ? true : false + # end + + # def conflict?(new_start, new_end) + # if new_start >= @start_date && new_start < @end_date || new_end > @start_date + # return false + # elsif @end_date = new_start || new_start > @end_date || new_end < @start_date || new_end == @start_date + # return true + # end + # end + end +end \ No newline at end of file diff --git a/refactors.txt b/refactors.txt new file mode 100644 index 000000000..58ebe3227 --- /dev/null +++ b/refactors.txt @@ -0,0 +1,17 @@ +A few things to change moving forward: + +Tighter code + 1. Refactor code with Enumerable methods where possible + 2. Consider where ternary format could be used + 3. Reconsider where other helper methods could be created + +Block class + 1. Consider pulling out a separate Block class and using Reservation as it's super class -- that way, if you used super in intitialize, you'd only have to add on block: and block_key: + +DateRange class + 1. Reconsider DateRange as it's own class (has no independent behaviors) + +Tighten up variable naming conventions + 1. Make sure method naming conventions follow a pattern + 2. Make sure method naming conventions logically build on one another IE reserve_from_block relies on update_block + 3. Review and update variable names for clarity and replication \ No newline at end of file diff --git a/test/date_test.rb b/test/date_test.rb new file mode 100644 index 000000000..9ca6f274b --- /dev/null +++ b/test/date_test.rb @@ -0,0 +1,40 @@ +require_relative "test_helper" + +describe "DateRange" do + + describe "contains" do + before do + @reservation = Hotel::Reservation.new(start_date: Date.new(2020, 5, 1), end_date: Date.new(2020, 5, 4), num_rooms: 1) + end + + it "returns true if requested date is contained in reservation dates" do + expect(@reservation.contains(Date.new(2020, 5, 2))).must_equal true + end + + it "returns true if requested date is first day in reservation dates" do + expect(@reservation.contains(Date.new(2020, 5, 1))).must_equal true + end + + it "returns false if requested date is last day in reservation dates" do + expect(@reservation.contains(Date.new(2020, 5, 4))).must_equal false + end + end + + describe "conflict?" do + before do + @reservation = Hotel::Reservation.new(start_date: Date.new(2020, 5, 1), end_date: Date.new(2020, 5, 4), num_rooms: 1) + end + + it "returns true if requested dates are contained in current reservation dates" do + expect(@reservation.no_conflict?(Date.new(2020, 5, 2), Date.new(2020, 5, 4))).must_equal false + end + + it "returns true if requested start date is the same as current end date" do + expect(@reservation.no_conflict?(Date.new(2020, 5, 4), Date.new(2020, 5, 5))).must_equal true + end + + it "returns false if requested start date is earlier than current end date" do + expect(@reservation.no_conflict?(Date.new(2020, 5, 2), Date.new(2020, 5, 5))).must_equal false + end + end +end \ No newline at end of file diff --git a/test/frontdesk_test.rb b/test/frontdesk_test.rb new file mode 100644 index 000000000..65a3d2246 --- /dev/null +++ b/test/frontdesk_test.rb @@ -0,0 +1,267 @@ +require_relative 'test_helper' + +describe "Front Desk" do + + describe "Initialize" do + + before do + @front_desk = Hotel::FrontDesk.new + end + + it "can create an instance of FrontDesk" do + expect(@front_desk).must_be_kind_of Hotel::FrontDesk + end + + it "can return a list of 20 rooms" do + expect(@front_desk.rooms).must_equal [*1..20] + end + + it "can return a list of 20 rooms represented by Integers" do + expect(@front_desk.rooms[0]).must_be_kind_of Integer + end + + it "can return an empty reservation collection when initialized (before reservations have been added)" do + expect(@front_desk.all_reservations.empty?).must_equal true + end + + it "can return the reservation collection as an Array" do + expect(@front_desk.all_reservations).must_be_kind_of Array + end + end + + describe "add_reservation" do + + before do + @start_date = Date.new(2019, 1, 4) + @end_date = Date.new(2019, 1, 7) + @front_desk = Hotel::FrontDesk.new() + end + + it "can add a new reservation to the reservation collection" do + @front_desk.add_reservation(@start_date, @end_date, 1) + + expect(@front_desk.all_reservations.length).must_equal 1 + end + + it "can return a list of 20 rooms" do + expect(@front_desk.rooms).must_equal [*1..20] + end + end + + describe "find_reservation_by_date" do + + before do + @front_desk = Hotel::FrontDesk.new() + @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 1) + @front_desk.add_reservation(Date.new(2019, 1, 1),Date.new(2019, 1, 5), 1) + @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 5), 1) + @front_desk.add_reservation(Date.new(2019, 1, 3),Date.new(2019, 1, 6), 1) + end + + it "can return an array of reservations by date" do + expect(@front_desk.find_reservation_by_date(Date.new(2019, 1, 4))).must_be_kind_of Array + end + + it "can return an accurate list of reservations by date" do + reservation_by_date = @front_desk.find_reservation_by_date(Date.new(2019, 1, 4)) + + expect(reservation_by_date.length).must_equal 4 + end + + it "can return an accurate first reservation by date" do + reservation_by_date = @front_desk.find_reservation_by_date(Date.new(2019, 1, 4)) + + expect(reservation_by_date.first.start_date).must_equal Date.new(2019, 1, 4) + end + + it "can return an accurate last reservation by date" do + reservation_by_date = @front_desk.find_reservation_by_date(Date.new(2019, 1, 4)) + + expect(reservation_by_date.last.start_date).must_equal Date.new(2019, 1, 3) + end + + it "can return an array of reservations and those reservations are Reservation class" do + reservation = @front_desk.find_reservation_by_date(Date.new(2019, 1, 4))[0] + + expect(reservation).must_be_kind_of Hotel::Reservation + end + + it "returns a single reservation for hotel blocks based on date" do + @front_desk = Hotel::FrontDesk.new() + block_to_find = @front_desk.add_block_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 3, 0.10, "banana") + + reservation = @front_desk.find_reservation_by_date(Date.new(2019, 1, 4)) + + expect(reservation).must_equal block_to_find + end + + end + + describe "assign room" do + + it "assigns a room to a new reservation" do + @front_desk = Hotel::FrontDesk.new() + reservation = @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 1) + + expect(reservation[0].assigned_room.empty?).must_equal false + end + + it "returns an error if no rooms are available" do + @front_desk = Hotel::FrontDesk.new() + 20.times do @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 1) + end + + expect{ @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 1, 1) }.must_raise ArgumentError + end + + it "returns an error if more than one room is requested" do + @front_desk = Hotel::FrontDesk.new() + + expect{ @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 4) }.must_raise ArgumentError + end + + it "can start a reservation on the same day that another in the same room ends" do + @front_desk = Hotel::FrontDesk.new() + 20.times do @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 1) + end + @front_desk.add_reservation(Date.new(2019, 1, 7),Date.new(2019, 1, 9), 1) + reservation = @front_desk.find_reservation_by_date(Date.new(2019, 1, 8)) + + expect(reservation[0].assigned_room.empty?).must_equal false + end + + end + + describe "add_block_reservation" do + it "returns a block reservation when a block is added" do + @front_desk = Hotel::FrontDesk.new() + @front_desk.add_block_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 3, 0.10, "banana") + + expect(@front_desk.all_reservations[0].block).must_equal :BLOCK + end + + it "raises an error if only one room is requested" do + @front_desk = Hotel::FrontDesk.new() + + expect{ @front_desk.add_block_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 1, 0.10, "bananas") }.must_raise ArgumentError + end + + #I want an exception raised if I try to create a Hotel Block and at least one of the rooms is unavailable for the given date range + #{TODO}MOVE THIS TO BLOCK SECTION + it "returns an error if not enough rooms are available for a block" do + @front_desk = Hotel::FrontDesk.new() + 15.times do @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 1) + end + + expect{ @front_desk.add_block_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 5, 1) }.must_raise ArgumentError + end + end + + describe "find_available_room_by_date" do + before do + @front_desk = Hotel::FrontDesk.new() + @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 1) + @front_desk.add_reservation(Date.new(2019, 1, 1),Date.new(2019, 1, 5), 1) + @front_desk.add_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 5), 1) + @front_desk.add_reservation(Date.new(2019, 1, 3),Date.new(2019, 1, 6), 1) + end + + it "can return an array of available rooms by date" do + expect(@front_desk.find_available_room_by_date(Date.new(2019, 1, 4))).must_be_kind_of Array + end + + it "can return an array of Integers (room numbers)" do + available_rooms = @front_desk.find_available_room_by_date(Date.new(2019, 1, 4)) + expect(available_rooms[0]).must_be_kind_of Integer + end + + it "can return an accurate list of available rooms by date" do + available_rooms = @front_desk.find_available_room_by_date(Date.new(2019, 1, 4)) + expect(available_rooms.length).must_equal 16 + end + + it "can return an accurate list of available rooms by date when blocks are involved" do + @front_desk.add_block_reservation(Date.new(2019, 1, 3),Date.new(2019, 1, 6), 3, 0.10, "banana") + available_rooms = @front_desk.find_available_room_by_date(Date.new(2019, 1, 4)) + expect(available_rooms.length).must_equal 14 + end + + it "can return an accurate list of available rooms and those rooms are unique" do + available_rooms = @front_desk.find_available_room_by_date(Date.new(2019, 1, 4)) + expect(available_rooms.length).must_equal available_rooms.uniq.length + end + + it "can return an accurate first available room by date" do + room_by_date = @front_desk.find_available_room_by_date((Date.new(2019, 1, 4))) + expect(room_by_date.first).must_equal room_by_date[0] + end + + it "can return an accurate last available room by date" do + room_by_date = @front_desk.find_available_room_by_date((Date.new(2019, 1, 4))) + expect(room_by_date.last).must_equal room_by_date[-1] + end + end + + describe "find_block_by_block_key" do + before do + @front_desk = Hotel::FrontDesk.new() + block = @front_desk.add_block_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 5, 0.20, "bananas") + end + + it "can return a block using a block_key" do + found_block = @front_desk.find_block_by_block_key(Date.new(2019, 1, 4),"bananas") + expect(found_block[0].block_key).must_equal "bananas" + end + end + + describe "update_block" do + before do + @front_desk = Hotel::FrontDesk.new() + block = @front_desk.add_block_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 5, 0.20, "bananas") + end + + it "can return an updated block (num_rooms - 1) using a block_key" do + found_block = @front_desk.update_block(Date.new(2019, 1, 4),"bananas") + expect(found_block.num_rooms).must_equal 4 + end + + it "raises an ArgumentError if no rooms are left in the block" do + @front_desk.update_block(Date.new(2019, 1, 4),"bananas") + @front_desk.update_block(Date.new(2019, 1, 4),"bananas") + @front_desk.update_block(Date.new(2019, 1, 4),"bananas") + @front_desk.update_block(Date.new(2019, 1, 4),"bananas") + @front_desk.update_block(Date.new(2019, 1, 4),"bananas") + + expect{ @front_desk.update_block(Date.new(2019, 1, 4),"bananas") }.must_raise ArgumentError + end + end + + describe "reserve_from_block" do + before do + @front_desk = Hotel::FrontDesk.new() + @front_desk.add_block_reservation(Date.new(2019, 1, 4),Date.new(2019, 1, 7), 5, 0.20, "bananas") + end + + it "can create a new single reservation with block_key" do + expect(@front_desk.reserve_from_block(Date.new(2019, 1, 4), "bananas")).must_be_kind_of Hotel::Reservation + end + + end + + + # it "can return an accurate list of available rooms and those rooms are unique" do + # available_rooms = @front_desk.find_available_room_by_date(Date.new(2019, 1, 4)) + # expect(available_rooms.length).must_equal available_rooms.uniq.length + # end + + # it "can return an accurate first available room by date" do + # room_by_date = @front_desk.find_available_room_by_date(Date.parse(Date.new(2019, 1, 4))) + # expect(room_by_date.first).must_equal room_by_date[0] + # end + + # it "can return an accurate last available room by date" do + # room_by_date = @front_desk.find_available_room_by_date(Date.parse(Date.new(2019, 1, 4))) + # expect(room_by_date.last).must_equal 20 + # end + #end +end \ No newline at end of file diff --git a/test/reservation_test.rb b/test/reservation_test.rb new file mode 100644 index 000000000..d4d9f823e --- /dev/null +++ b/test/reservation_test.rb @@ -0,0 +1,84 @@ +require_relative "test_helper" + +describe "Reservation" do + + describe "Initialize" do + + before do + @reservation = Hotel::Reservation.new(start_date: Date.new(2020, 5, 1), end_date: Date.new(2020, 5, 4), num_rooms: 1) + end + + it "is an instance of Reservation" do + expect(@reservation).must_be_kind_of Hotel::Reservation + end + + it "each start and stop time in array is a Time instance" do + expect(@reservation.start_date).must_be_kind_of Date + expect(@reservation.end_date).must_be_kind_of Date + end + + it "the number of rooms provided is an Integer" do + expect(@reservation.num_rooms).must_be_kind_of Integer + end + + it "returns an ArgumentError if given a bad start/end date combination" do + expect { Hotel::Reservation.new(start_date: Date.new(2020, 5, 5), end_date: Date.new(2020, 5, 4), num_rooms: 1) }.must_raise ArgumentError + end + + it "returns an ArgumentError if the start/end date provided are the same" do + expect { Hotel::Reservation.new(start_date: Date.new(2020, 5, 5), end_date: Date.new(2020, 5, 5), num_rooms: 1) }.must_raise ArgumentError + end + + it "raises an ArgumentError rooms given is not an Integer" do + expect { Hotel::Reservation.new(start_date: Date.new(2020, 5, 5), end_date: Date.new(2020, 5, 6), num_rooms: "B") }.must_raise ArgumentError + end + + it "raises an ArgumentError room assigned is not nil when reservation is created" do + expect { Hotel::Reservation.new(start_date: Date.new(2020, 5, 5), end_date: Date.new(2020, 5, 6), num_rooms: 1, assigned_room: 4) }.must_raise ArgumentError + end + + it "raises an ArgumentError if more than 5 rooms are requested" do + expect { Hotel::Reservation.new(start_date: Date.new(2020, 5, 5), end_date: Date.new(2020, 5, 6), num_rooms: 6) }.must_raise ArgumentError + end + + it "raises an ArgumentError if an invalid reservation status is given for block" do + expect { Hotel::Reservation.new(start_date: Date.new(2020, 5, 5), end_date: Date.new(2020, 5, 6), num_rooms: 5, block: :BANANA) }.must_raise ArgumentError + end + + it "raises an ArgumentError if block is noted but only one room is requested" do + expect { Hotel::Reservation.new(start_date: Date.new(2020, 5, 5), end_date: Date.new(2020, 5, 6), num_rooms: 1, block: :BLOCK) }.must_raise ArgumentError + end + + it "raises an ArgumentError if a block_key is given for a single room" do + expect { Hotel::Reservation.new(start_date: Date.new(2020, 5, 5), end_date: Date.new(2020, 5, 6), num_rooms: 1, block_key: 7) }.must_raise ArgumentError + end + end + + describe "total_cost" do + before do + @reservation = Hotel::Reservation.new(start_date: Date.new(2020, 5, 1), end_date: Date.new(2020, 5, 4), num_rooms: 1) + end + + it "returns the total cost accurately for a single room" do + expect(@reservation.total_cost).must_equal 600.00 + end + + it "returns the total cost as a float" do + expect(@reservation.total_cost).must_be_kind_of Float + end + end + + describe "total_cost block" do + before do + @reservation = Hotel::Reservation.new(start_date: Date.new(2020, 5, 1), end_date: Date.new(2020, 5, 4), num_rooms: 3, discount: 0.10, block: :BLOCK) + end + + it "returns the total cost as a float" do + expect(@reservation.total_cost).must_be_kind_of Float + end + + it "returns the total cost accurately for room blocks (includes discount)" do + expect(@reservation.total_cost).must_equal 1620.00 + end + end +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index c3a7695cf..f4addf318 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,8 +1,16 @@ # Add simplecov +require 'simplecov' +SimpleCov.start + require "minitest" require "minitest/autorun" require "minitest/reporters" +require "minitest/skip_dsl" Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new # require_relative your lib files here! + +require_relative '../lib/reservation.rb' +require_relative '../lib/frontdesk.rb' +require_relative '../lib/date.rb'