diff --git a/.gitignore b/.gitignore index 5e1422c9c..c0ac3dc53 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ build-iPhoneSimulator/ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc +coverage diff --git a/design-scaffolding-notes.md b/design-scaffolding-notes.md new file mode 100644 index 000000000..8efb3d487 --- /dev/null +++ b/design-scaffolding-notes.md @@ -0,0 +1,49 @@ +# Hotel Design Scaffolding + +## How to Read This Scaffolding to Create a Second Draft + +This scaffolding is one solution that answers some of the initial questions about how project files can be laid out. + +Use [this view of our branch on GitHub.com](https://github.com/AdaGold/hotel/tree/design-scaffolding) to explore the files that exist on this repo. + - What classes exist? + - Why? What are they named, what do they represent, and what state and behavior do they have? + - What tests exist? + - What parts of this design inspires you, and you want to steal? + - What parts of this design are you unsure about, and need to consider again later? + - What parts of this design do you think you can do without? + +Spend **no more than 1 hour** answering those questions and adjusting your project's first draft design. After one hour, get started; don't forget that a useful skill for the programmer is the ability to get started, and adjust in small ways often. + +### What it includes + +- Three class stubs, `HotelController`, `Reservation` and `DateRange` +- Stubs for public methods of each class from waves 1 and 2, as described in the user stories +- "Interface" tests for each class method that invoke it with the right parameters and verify the return type +- Full test stubs for the `DateRange` class + +### What it does not include + +- Opinions about how classes should interact or data should be stored +- Opinions about whether there should be a `Room` class, or whether it should know about `Reservation`s +- Private helper methods to keep code organized + +Students should feel free to modify any code as they see fit, including changing method signatures, adding new classes and methods, reordering things, not looking at the `DateRange` tests because they want to give it a shot on their own, etc. + +## How to use this code + +Design scaffolding code lives on the `design-scaffolding` branch. + +You can use this code either as inspiration, or as a starting point. If using it as an inspiration, it follows our standard project layout, with product code under `lib/` and tests under `test/`. + +If you choose to use the code on this branch as a starting point, you can either: + +1. Copy and paste each file from this project into your existing `hotel` folder +2. Or start your project anew with the following steps: + + ``` + $ git clone + $ cd hotel + $ git merge origin/design-scaffolding + ``` + + - Note: You can try to merge in the design scaffolding after you've started, but you'll probably end up with merge conflicts. See an instructor if you're not able to resolve them yourself. diff --git a/lib/block.rb b/lib/block.rb new file mode 100644 index 000000000..19c74cebd --- /dev/null +++ b/lib/block.rb @@ -0,0 +1,25 @@ +module Hotel + + class Block + # Generator + attr_reader :range, :rooms, :rate + + # Constructor + def initialize(start_date, end_date, rooms, rate) + @range = Hotel::DateRange.new(start_date, end_date) + raise ArgumentError, "Rooms must be an array" unless rooms.instance_of?(Array) + raise ArgumentError, "No more than 5 rooms per block" if rooms.size > 5 + raise ArgumentError, "At least one room per block" if rooms.size == 0 + @rooms = rooms + raise ArgumentError, "Rate must be a number" unless rate.is_a?(Numeric) + raise ArgumentError, "Rate must be a positive number" if rate < 0 + @rate = rate + end + + # Calculate cost + def cost + return @rate * @range.nights * @rooms.size + end + end + +end \ No newline at end of file diff --git a/lib/date_range.rb b/lib/date_range.rb new file mode 100644 index 000000000..8340ea82e --- /dev/null +++ b/lib/date_range.rb @@ -0,0 +1,46 @@ +module Hotel + + class DateRange + # Generator + attr_accessor :start_date, :end_date + + # Constructor + def initialize(start_date, end_date) + # Raise an error when an invalid date range is provided + unless start_date.instance_of?(Date) && end_date.instance_of?(Date) + raise ArgumentError, "Parameters must be of class Date" + end + if start_date >= end_date + raise ArgumentError, "Start date must be before end date" + end + @start_date = start_date + @end_date = end_date + end + + # Check range overlap + def overlap?(other) + unless other.instance_of?(Hotel::DateRange) + raise ArgumentError, "Parameters must be of class Hotel::DateRange" + end + + return other.start_date < @end_date && other.end_date > @start_date + end + + # Check if a date is included in the date range + def include?(date) + unless date.instance_of?(Date) + raise ArgumentError, "Parameters must be of class Date" + end + + return date >= @start_date && date < @end_date + end + + # Calculate number of nights + # Checkout day not to be charged + def nights + return (@end_date - @start_date).to_i + end + + end + +end diff --git a/lib/hotel_controller.rb b/lib/hotel_controller.rb new file mode 100644 index 000000000..a4aead81b --- /dev/null +++ b/lib/hotel_controller.rb @@ -0,0 +1,123 @@ +module Hotel + + class HotelController + # Generator + # Access list of all rooms in hotel + attr_reader :rooms, :reservations, :blocks + + # Constructor + # Hotel has 20 rooms, 1 through 20 + def initialize + @rooms = (1..20).to_a + @reservations = [] + @blocks = [] + end + + # Reserve a room for a given date range + # Determine which room to reserve given start and end dates + def reserve_room(start_date, end_date) + room = available_rooms(start_date, end_date).first + raise ArgumentError, "No Vacancy" if room == nil + reservation = Hotel::Reservation.new(start_date, end_date, room) + @reservations << reservation + return reservation + end + + # Access list of reservations for a specific date to track reservations by date + def reservations_by_date(date) + return @reservations.select do |reservation| + reservation.range.include?(date) + end + end + + # Returns list of reservations for specific room and given date range + def reservations_by_room(start_date, end_date, room) + range = Hotel::DateRange.new(start_date, end_date) + return @reservations.select do |reservation| # which reservations have an overlap with range passed + reservation.range.overlap?(range) && reservation.room == room + end + end + + # Returns specific room available for date range + def is_room_available?(start_date, end_date, room) + return reservations_by_room(start_date, end_date, room).size == 0 + end + + # Returns list of rooms available for date range + # Checks if the room is in a block. Respects room blocks as well as individual reservations + def available_rooms(start_date, end_date) + no_reservation = @rooms.select { |room| is_room_available?(start_date, end_date, room) } + # Cannot reserve specific room, for specific date, if room already in a block for that specific date + no_blocks = no_reservation.select { |room| is_room_unblocked?(start_date, end_date, room) } + return no_blocks + end + + # Wave 3 + + # Create a block for given date range, set of rooms and rate + def create_block(start_date, end_date, rooms, rate) + raise ArgumentError, "Invalid room" unless rooms_valid?(rooms) + # Exception raised if try to create a block and at least one of the rooms is unavailable for given date range + raise ArgumentError, "Rooms not available" unless block_rooms_available?(start_date, end_date, rooms) + block = Hotel::Block.new(start_date, end_date, rooms, rate) + @blocks << block + return block + end + + # Create a reservation from a block + # Reserve a specific room from a hotel block + def reserve_from_block(block, room) + raise ArgumentError, "Not a block" unless block.instance_of?(Hotel::Block) + raise ArgumentError, "Room not in block" unless block.rooms.include?(room) + # Only reserves a room from block for the full duration of the block + # When a room is reserved from a block of rooms, the reservation dates will always match the date range of the block + start_date = block.range.start_date + end_date = block.range.end_date + raise ArgumentError, "No Vacancy" unless is_room_available?(start_date, end_date, room) + reservation = Hotel::Reservation.new(start_date, end_date, room) + # Reservation made from a hotel block adds to the list of reservations for that date + @reservations << reservation + return reservation + end + + # Check if rooms are valid + def rooms_valid?(rooms) + return false unless rooms.instance_of?(Array) + return false if rooms.size == 0 + rooms.each do |room| + return false unless @rooms.include?(room) + end + return true + end + + # Check if rooms for a block are available in date range + # Cannot create another hotel block for specific date and room if room already in another block + def block_rooms_available?(start_date, end_date, rooms) + rooms.each do |room| + return false unless is_room_available?(start_date, end_date, room) + return false unless is_room_unblocked?(start_date, end_date, room) + end + return true + end + + # Check if a room is not blocked in a specific date range + def is_room_unblocked?(start_date, end_date, room) + return blocks_by_room(start_date, end_date, room).size == 0 + end + + # Returns a list of blocks for that room in the date range + def blocks_by_room(start_date, end_date, room) + range = Hotel::DateRange.new(start_date, end_date) + return @blocks.select do |block| # which blocks have an overlap with the range passed + block.range.overlap?(range) && block.rooms.include?(room) + end + end + + # Checks whether a given block has any rooms available + def available_rooms_in(block) + return block.rooms.select { |room| is_room_available?(block.range.start_date, block.range.end_date, room)} + end + + end # class HotelController + +end # module Hotel \ No newline at end of file diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..219903bf9 --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,21 @@ +module Hotel + + class Reservation + # Generator + attr_reader :range, :room + ROOM_COST = 200 + + # Constructor + def initialize(start_date, end_date, room) + @range = Hotel::DateRange.new(start_date, end_date) + @room = room + end + + # Get the total cost for a given reservation + # Rooms Identical and Cost $200/night + def cost + return ROOM_COST * @range.nights + end + end + +end diff --git a/main.rb b/main.rb new file mode 100755 index 000000000..3ffc38bb8 --- /dev/null +++ b/main.rb @@ -0,0 +1,179 @@ +#!/usr/bin/ruby +# +# Title : Hotel Main - Ada Cohort 13 - Space +# Author : Suely Barreto +# Date : March 2020 +# +require "Date" +require_relative "lib/date_range" +require_relative "lib/reservation" +require_relative "lib/block" +require_relative "lib/hotel_controller" + +DISCOUNT_RATE = 150 + +# Function to prompt for a date +def get_date(message) + valid_date = false + while !valid_date + print message + begin + date = Date.parse(gets.chomp) + valid_date = true + rescue => exception + puts "Error: #{exception.message}" + end + end + return date +end + +# Function to prompt for a positive integer with min, max +def get_integer(prompt, min, max) + expression = /[0-9]+/ + # Repeat until number within range + number = 0 + input = "" + while number < min || number > max || input == "" + # Repeat until valid number + input = "" + while input.match(expression).to_s != input || input == "" + print prompt + input = gets.chomp + puts "This is not a valid number!" if input.match(expression).to_s != input || input == "" + end + number = input.to_i + puts "Number is too large. Maximum is #{max}." if number > max + puts "Number is too small. Minimum is #{min}." if number < min + end + return number +end + +# Reserve a room +def make_reservation(hotel) + puts "Enter the data for the new reservation" + start_date = get_date("Start date: ") + end_date = get_date("End date: ") + print "\nDo you confirm you want to add this reservation? (Y/N) " + confirm = gets.chomp.upcase + if confirm == "Y" || confirm == "YES" + begin + reservation = hotel.reserve_room(start_date, end_date) + puts "New reservation was made for room #{reservation.room}." + rescue => exception + puts "Error: #{exception.message}" + end + else + puts "Ok, new reservation was NOT made." + end +end + +# Make a block +def make_block(hotel) + puts "Enter the data for the new block" + start_date = get_date("Start date: ") + end_date = get_date("End date: ") + start_room = get_integer("Start room: ", 1, 20) + end_room = get_integer("End room: ", start_room, 20) + print "\nDo you confirm you want to add this block? (Y/N) " + confirm = gets.chomp.upcase + if confirm == "Y" || confirm == "YES" + begin + block = hotel.create_block(start_date, end_date, (start_room..end_room).to_a, DISCOUNT_RATE) + puts "New block was made." + rescue => exception + puts "Error: #{exception.message}" + end + else + puts "Ok, new block was NOT made." + end +end + +# Make a new reservation from the block +def make_block_reservation(hotel) + show_blocks(hotel) + return if hotel.blocks.size == 0 + puts "\nEnter the data for the block reservation." + start_date = get_date("Start date: ") + end_date = get_date("End date: ") + room = get_integer("Room to reserve: ", 1, 20) + begin + block = hotel.blocks_by_room(start_date, end_date, room).first + rescue => exception + puts "Error: #{exception.message}" + block = nil + end + if block == nil + puts "\nCould not find that block." + else + begin + hotel.reserve_from_block(block, room) + rescue => exception + puts "Error: #{exception.message}" + end + end +end + +# Show reservations +def show_reservations(hotel) + puts "List of reservations\n\n" + puts "No reservations!" if hotel.reservations.size == 0 + hotel.reservations.each do |reservation| + puts "Start date: #{reservation.range.start_date}, end date: #{reservation.range.end_date}, room: #{reservation.room}" + end +end + +# Show blocks +def show_blocks(hotel) + puts "List of blocks\n\n" + puts "No blocks!" if hotel.blocks.size == 0 + hotel.blocks.each do |block| + print "Start date: #{block.range.start_date}, end date: #{block.range.end_date}, rooms:" + block.rooms.each do |room| + print " #{room}" + end + print ", available rooms:" + available = hotel.available_rooms_in(block) + available.each do |room| + print " #{room}" + end + print "None" if available.size == 0 + puts + end +end + +# Main method to show CLI options and call other methods +def main + hotel = Hotel::HotelController.new + + puts "\nWelcome to Hotel Ada Lovelace!" + choice = "" + while choice != "exit" + puts "\nHotel Main Menu:" + puts "1 - Make Reservation (mr, r)" + puts "2 - Make Block (mb, b)" + puts "3 - Make Block Reservation (mbr, br)" + puts "4 - Show Reservations (sr, s)" + puts "5 - Show Blocks (sb)" + puts "6 - Exit (x, q)" + print "\nWhat would you like to do? " + choice = gets.chomp.downcase + case choice + when "make reservation", "reservation", "mr", "r", "1" + make_reservation(hotel) + when "make block", "block", "mb", "b", "2" + make_block(hotel) + when "make block reservation", "mbr", "br", "3" + make_block_reservation(hotel) + when "show reservations", "sr", "s", "4" + show_reservations(hotel) + when "show blocks", "sb", "5" + show_blocks(hotel) + when "exit", "x", "quit", "q", "e", "6" + choice = "exit" + else + puts "Invalid choice!" + end + end +end + +main \ No newline at end of file diff --git a/test/block_test.rb b/test/block_test.rb new file mode 100644 index 000000000..09efe886b --- /dev/null +++ b/test/block_test.rb @@ -0,0 +1,65 @@ +require_relative "test_helper" + +describe "Hotel::Block" do + + before do + @start_date = Date.new(2017, 01, 01) + @end_date = @start_date + 3 + @rooms = [3,4,5] + @rate = 150 + @block = Hotel::Block.new(@start_date, @end_date, @rooms, @rate) + end + + describe "constructor" do + it "initializes with two dates, array of rooms and rate" do + expect(@block).must_be_kind_of Hotel::Block + expect(@block.range.start_date).must_equal @start_date + expect(@block.range.end_date).must_equal @end_date + expect(@block.rooms).must_be_kind_of Array + expect(@block.rooms).must_equal @rooms + expect(@block.rate).must_equal @rate + end + + it "does not accept a number as rooms" do + # create a block with rooms = 3. Must raise argumenterror + expect{ Hotel::Block.new(@start_date, @end_date, 3, @rate) }.must_raise ArgumentError + end + + it "does not accept zero rooms or more than 5 rooms" do + expect{ Hotel::Block.new(@start_date, @end_date, [], @rate) }.must_raise ArgumentError + expect{ Hotel::Block.new(@start_date, @end_date, [1,2,3,4,5,6], @rate) }.must_raise ArgumentError + end + + it "does not accept invalid rates" do + # create a block with rate = -1. Must raise argumenterror + expect{ Hotel::Block.new(@start_date, @end_date, @rooms, -1) }.must_raise ArgumentError + # create a block with rate = "100". Must raise argumenterror + expect{ Hotel::Block.new(@start_date, @end_date, @rooms, "100") }.must_raise ArgumentError + # create a block with rate = nil. Must raise argumenterror + expect{ Hotel::Block.new(@start_date, @end_date, @rooms, nil) }.must_raise ArgumentError + end + + it "raises an ArgumentError if invalid dates provided" do + end_date1 = @start_date - 1 + end_date2 = "2017/01/04" + # start date can not be after end date + expect{ Hotel::Block.new(@start_date, end_date1, @rooms, @rate) }.must_raise ArgumentError + # date can not be a string + expect{ Hotel::Block.new(@start_date, end_date2, @rooms, @rate) }.must_raise ArgumentError + # start date can not be the same as end date + expect{ Hotel::Block.new(@start_date, @start_date, @rooms, @rate) }.must_raise ArgumentError + end + end # describe "constructor" + + describe "cost" do + it "returns a number" do + expect(@block.cost).must_be_kind_of Numeric + end + + it "calculates the cost" do + # block cost for 3 nights, 3 rooms, at $150 room rate + expect(@block.cost).must_equal 1350 + end + end # describe "cost" + +end # describe "Hotel::Block" \ No newline at end of file diff --git a/test/date_range_test.rb b/test/date_range_test.rb new file mode 100644 index 000000000..04d857bf4 --- /dev/null +++ b/test/date_range_test.rb @@ -0,0 +1,151 @@ +require_relative "test_helper" + +describe "Hotel::DateRange" do + + describe "constructor" do + it "can be initialized with two dates" do + start_date = Date.new(2017, 01, 01) + end_date = start_date + 3 + range = Hotel::DateRange.new(start_date, end_date) + + expect(range.start_date).must_equal start_date + expect(range.end_date).must_equal end_date + expect(range).must_be_kind_of Hotel::DateRange + end + + it "is an error if parameters are not dates" do + start_date = "2017/01/01" + end_date = "2017/01/04" + + expect{ range = Hotel::DateRange.new(start_date, end_date) }.must_raise ArgumentError + end + + it "is an error for negative-length ranges" do + start_date = Date.new(2017, 01, 01) + end_date = start_date - 3 + + expect{ range = Hotel::DateRange.new(start_date, end_date) }.must_raise ArgumentError + end + + it "is an error to create a 0-length range" do + start_date = Date.new(2017, 01, 01) + end_date = start_date + + expect{ range = Hotel::DateRange.new(start_date, end_date) }.must_raise ArgumentError + end + + end + + describe "overlap?" do + before do + def simple_range(start_day, end_day) + return Hotel::DateRange.new(Date.new(2017, 01, start_day), Date.new(2017, 01, end_day)) + end + end + + it "raises an ArgumentError if parameter not a range" do + range1 = simple_range(01, 05) + range2 = Date.new(2017, 01, 01) + expect{ range1.overlap?(range2) }.must_raise ArgumentError + end + + it "returns true for the same range" do + range1 = simple_range(01, 05) + range2 = simple_range(01, 05) + expect(range1.overlap?(range2)).must_equal true + end + + it "returns true for a contained range" do + range1 = simple_range(05, 10) + range2 = simple_range(01, 15) + expect(range1.overlap?(range2)).must_equal true + end + + it "returns true for a range that overlaps in front" do + range1 = simple_range(05, 15) + range2 = simple_range(01, 10) + expect(range1.overlap?(range2)).must_equal true + end + + it "returns true for a range that overlaps in the back" do + range1 = simple_range(01, 10) + range2 = simple_range(05, 15) + expect(range1.overlap?(range2)).must_equal true + end + + it "returns true for a containing range" do + range1 = simple_range(01, 15) + range2 = simple_range(05, 10) + expect(range1.overlap?(range2)).must_equal true + end + + it "returns false if range starts on end_date date" do + range1 = simple_range(01, 05) + range2 = simple_range(05, 10) + expect(range1.overlap?(range2)).must_equal false + end + + it "returns false if range starts on start_date date" do + range1 = simple_range(05, 10) + range2 = simple_range(01, 05) + expect(range1.overlap?(range2)).must_equal false + end + + it "returns false for a range completely before" do + range1 = simple_range(01, 05) + range2 = simple_range(10, 15) + expect(range1.overlap?(range2)).must_equal false + end + + it "returns false for a date completely after" do + range1 = simple_range(10, 15) + range2 = simple_range(01, 05) + expect(range1.overlap?(range2)).must_equal false + end + end + + describe "include?" do + before do + start_date = Date.new(2018, 01, 01) + end_date = Date.new(2018, 01, 10) + + @range = Hotel::DateRange.new(start_date, end_date) + end + + it "raises an ArgumentError if parameter not a date" do + expect{ @range.include?("2018/01/01") }.must_raise ArgumentError + end + + it "returns false if the date is clearly before" do + expect(@range.include?(Date.new(2017, 01, 01))).must_equal false + end + + it "returns false if the date is clearly after" do + expect(@range.include?(Date.new(2019, 01, 01))).must_equal false + end + + it "returns true for dates in the range" do + expect(@range.include?(Date.new(2018, 01, 05))).must_equal true + end + + it "returns true for the start_date date" do + expect(@range.include?(Date.new(2018, 01, 01))).must_equal true + end + + it "returns false for the end_date date" do + expect(@range.include?(Date.new(2018, 01, 10))).must_equal false + end + end + + describe "nights" do + it "returns the correct number of nights" do + start_date = Date.new(2017, 01, 01) + end_date = start_date + 5 + range = Hotel::DateRange.new(start_date, end_date) + + expect(range.nights).must_be_kind_of Integer + expect(range.nights).must_equal 5 + end + end + +end \ No newline at end of file diff --git a/test/hotel_controller_test.rb b/test/hotel_controller_test.rb new file mode 100644 index 000000000..34ae498e8 --- /dev/null +++ b/test/hotel_controller_test.rb @@ -0,0 +1,360 @@ +require_relative "test_helper" +require "awesome_print" + +describe "Hotel::HotelController" do + + before do + @hotel = Hotel::HotelController.new + def dt(day) + return Date.new(2020, 01, day) + end + end + + describe "wave 1" do + + describe "rooms" do + it "returns a list" do + expect(@hotel.rooms).must_be_kind_of Array + end + + it "return 20 rooms" do + expect(@hotel.rooms.size).must_equal 20 + end + + it "return 20 rooms numbered 1 through 20" do + (1..20).each do |room| + expect(@hotel.rooms.include?(room)).must_equal true + end + end + end # describe "rooms" + + describe "reserve_room" do + it "takes two Date objects and returns a Reservation" do + reservation = @hotel.reserve_room(dt(1), dt(4)) + expect(reservation).must_be_kind_of Hotel::Reservation + end + + it "adds to the list of reservations" do + @hotel.reserve_room(dt(1), dt(4)) + expect(@hotel.reservations.size).must_equal 1 + @hotel.reserve_room(dt(1), dt(4)) + expect(@hotel.reservations.size).must_equal 2 + end + + it "selects the first available room" do + # Reserve a room, than check if the reservation room equals to 1 + reservation1 = @hotel.reserve_room(dt(1), dt(4)) + expect(reservation1.room).must_equal 1 + expect(@hotel.reservations.last.room).must_equal 1 + + # Reserve another room, than check if the reservation room equals 2 + reservation2 = @hotel.reserve_room(dt(1), dt(4)) + expect(reservation2.room).must_equal 2 + expect(@hotel.reservations.last.room).must_equal 2 + end + + it "returns error if no rooms are available" do + # reserve 20 rooms (20.times). The 21st should return an error. + (1..20).each do + @hotel.reserve_room(dt(1), dt(4)) + end + expect(@hotel.reservations.size).must_equal 20 + expect{ @hotel.reserve_room(dt(1), dt(4)) }.must_raise ArgumentError + # it's ok to create more reservations in other date ranges + @hotel.reserve_room(dt(15), dt(20)) + expect(@hotel.reservations.size).must_equal 21 + end + + it "does not accept the same date for start and end" do + # create a reservation from 2020/01/05 to 2020/01/05. It should raise an exception + expect{ @hotel.reserve_room(dt(5), dt(5)) }.must_raise ArgumentError + end + + it "does not accept a start date after the end date" do + # create a reservation from 2020/01/05 to 2020/01/01. It should raise an exception + expect{ @hotel.reserve_room(dt(5), dt(1)) }.must_raise ArgumentError + end + end # describe "reserve_room" + + # Access the list of reservations for a specific date to track reservations by date + describe "reservations_by_date" do + it "takes a date and returns a list of reservations" do + @hotel.reserve_room(dt(1), dt(4)) + @hotel.reserve_room(dt(1), dt(4)) + reservations_ondate = @hotel.reservations_by_date(dt(1)) + + expect(reservations_ondate).must_be_kind_of Array + expect(reservations_ondate.size).must_equal 2 + + reservations_ondate.each do |reservation| + expect(reservation).must_be_kind_of Hotel::Reservation + end + end + + it "returns reservation when asked for start date" do + # create a reservation from 2020/01/01 to 2020/01/04, than get a list of reservations for 2020/01/01, should return 1 + @hotel.reserve_room(dt(1), dt(4)) + reservations_ondate = @hotel.reservations_by_date(dt(1)) + expect(reservations_ondate.size).must_equal 1 + # create another reservation from 2020/01/01 to 2020/01/05, than get a list of reservations for 2020/01/01, should return 2 + @hotel.reserve_room(dt(1), dt(5)) + reservations_ondate = @hotel.reservations_by_date(dt(1)) + expect(reservations_ondate.size).must_equal 2 + # create another reservation from 2020/01/05 to 2020/01/10, than get a list of reservations for 2020/01/05, should return 1 + @hotel.reserve_room(dt(5), dt(10)) + reservations_ondate = @hotel.reservations_by_date(dt(5)) + expect(reservations_ondate.size).must_equal 1 + end + + it "returns reservation when ask for date in the middle" do + # create a reservation from 2020/01/01 to 2020/01/10, than get a list of reservations for 2020/01/05. It should return 1 + @hotel.reserve_room(dt(1), dt(10)) + expect(@hotel.reservations_by_date(dt(5)).size).must_equal 1 + # create a reservation from 2020/01/02 to 2020/01/15, than get a list of reservations for 2020/01/05. It should return 2 + @hotel.reserve_room(dt(2), dt(15)) + expect(@hotel.reservations_by_date(dt(5)).size).must_equal 2 + # create a reservation from 2020/01/10 to 2020/01/15, than get a list of reservations for 2020/01/05. It should return 2 + @hotel.reserve_room(dt(10), dt(15)) + expect(@hotel.reservations_by_date(dt(5)).size).must_equal 2 + end + + it "does not return reservation when ask for end date" do + # create a reservation from 2020/01/01 to 2020/01/10, than get a list of reservations for 2020/01/10. It should return 0 + @hotel.reserve_room(dt(1), dt(10)) + expect(@hotel.reservations_by_date(dt(10)).size).must_equal 0 + # create a reservation from 2020/01/05 to 2020/01/10, than get a list of reservations for 2020/01/10. It should return 0 + @hotel.reserve_room(dt(5), dt(10)) + expect(@hotel.reservations_by_date(dt(10)).size).must_equal 0 + end + end # describe "reservations_by_date" + + end # describe "wave 1" + + # check whether reservations conflict with each other + describe "wave 2" do + + describe "available_rooms" do + it "takes two dates and returns a list" do + room_list = @hotel.available_rooms(dt(1), dt(4)) + expect(room_list).must_be_kind_of Array + end + + it "removes a room after it is reserved" do + @hotel.reserve_room(dt(1), dt(5)) + expect(@hotel.available_rooms(dt(1), dt(5)).size).must_equal 19 + @hotel.reserve_room(dt(1), dt(5)) + expect(@hotel.available_rooms(dt(1), dt(5)).size).must_equal 18 + end + + it "room not available if overlaping reservation" do + # create a reservation from 2020/01/05 to 2020/01/15. + @hotel.reserve_room(dt(5), dt(15)) + # Check overlap with same range + expect(@hotel.available_rooms(dt(5), dt(15)).size).must_equal 19 + # Check overlap in the back + expect(@hotel.available_rooms(dt(10), dt(20)).size).must_equal 19 + # Check overlap at the front + expect(@hotel.available_rooms(dt(1), dt(10)).size).must_equal 19 + # Check overlap fully contained + expect(@hotel.available_rooms(dt(7), dt(12)).size).must_equal 19 + # Check overlap fully containing + expect(@hotel.available_rooms(dt(1), dt(20)).size).must_equal 19 + # Check no overlap fully before + expect(@hotel.available_rooms(dt(1), dt(3)).size).must_equal 20 + # Check no overlap fully after + expect(@hotel.available_rooms(dt(17), dt(20)).size).must_equal 20 + # Check no overlap on checkout date after + expect(@hotel.available_rooms(dt(15), dt(20)).size).must_equal 20 + # Check no overlap on checkout date before + expect(@hotel.available_rooms(dt(1), dt(5)).size).must_equal 20 + end + + end # describe "available_rooms" + + end # describe "wave 2" + + describe "wave 3" do + + # Create a Block for a Given Date Range, a Set of Rooms and a Rate + describe "create_block" do + it "takes start/end/rooms/rate and returns Block" do + block = @hotel.create_block(dt(1), dt(4), [3, 4, 5], 150) + expect(block).must_be_kind_of Hotel::Block + end + + it "raises and ArgumentError if the rooms are invalid " do + expect{ @hotel.create_block(dt(1), dt(4), [1, 10, 18, 20, 21], 150) }.must_raise ArgumentError + end + + it "raises and ArgumentError if more than 5 rooms" do + expect{ @hotel.create_block(dt(1), dt(4), [1, 2, 3, 4, 5, 6], 150) }.must_raise ArgumentError + end + + it "raises and ArgumentError if rooms is an empty array" do + expect{ @hotel.create_block(dt(1), dt(4), [], 150) }.must_raise ArgumentError + end + + it "raises and ArgumentError if room is unavailable " do + # Create a reservation and try to block a room that is already reserved + @hotel.reserve_room(dt(1), dt(4)) # this will reserve room number 1 + expect{ @hotel.create_block(dt(1), dt(4), [1, 2, 3], 150) }.must_raise ArgumentError + # Create a block and try to block a room that is already blocked (in another block) + @hotel.create_block(dt(1), dt(4), [2, 3, 4], 150) + expect{ @hotel.create_block(dt(1), dt(4), [4, 5, 6], 150) }.must_raise ArgumentError + expect{ @hotel.create_block(dt(2), dt(3), [4, 5, 6], 150) }.must_raise ArgumentError + end + + it "adds to the block list" do + @hotel.create_block(dt(1), dt(4), [3, 4, 5], 150) + expect(@hotel.blocks.size).must_equal 1 + @hotel.create_block(dt(1), dt(4), [6, 7, 8], 150) + @hotel.create_block(dt(4), dt(8), [6, 7, 8], 150) + expect(@hotel.blocks.size).must_equal 3 + end + + it "blocks reservations that conflict with a block" do # improve... + @hotel.create_block(dt(1), dt(4), [1, 2, 3], 150) + reservation = @hotel.reserve_room(dt(1), dt(4)) # this should reserve room number 4 instead of 1 + expect(reservation.room).must_equal 4 + end + end # describe "create_block" + + describe "reserve_from_block" do + # Create a Reservation from a Block + # Reserve a specific room from a hotel block + it "takes a block and a room. Returns a reservation" do + block = @hotel.create_block(dt(1), dt(4), [1, 2, 3], 150) + reservation = @hotel.reserve_from_block(block, 2) + expect(reservation).must_be_kind_of Hotel::Reservation + end + + # trying to reserve from block without having a block + it "raises an ArgumentError if the block is invalid" do + expect{ @hotel.reserve_from_block(nil, 2) }.must_raise ArgumentError + expect{ @hotel.reserve_from_block(1, 2) }.must_raise ArgumentError + expect{ @hotel.reserve_from_block(dt(1), 2) }.must_raise ArgumentError + end + + it "raises an ArgumentError if room is not in block" do + block = @hotel.create_block(dt(1), dt(4), [1, 2, 3], 150) + expect{ @hotel.reserve_from_block(block, 4) }.must_raise ArgumentError + end + + it "reserves room for the full duration of the block" do + block = @hotel.create_block(dt(1), dt(4), [1, 2, 3], 150) + reservation = @hotel.reserve_from_block(block, 2) + expect(reservation.range.start_date).must_equal block.range.start_date + expect(reservation.range.end_date).must_equal block.range.end_date + end + + it "raises an ArgumentError if the room is not available" do + block = @hotel.create_block(dt(1), dt(4), [1, 2, 3], 150) + @hotel.reserve_from_block(block, 2) + expect{ @hotel.reserve_from_block(block, 2) }.must_raise ArgumentError + end + + it "adds to the list of reservations" do + block = @hotel.create_block(dt(1), dt(4), [1, 2, 3], 150) + @hotel.reserve_from_block(block, 2) + expect(@hotel.reservations.size).must_equal 1 + reservation = @hotel.reserve_room(dt(1), dt(4)) + expect(@hotel.reservations.size).must_equal 2 + @hotel.reserve_from_block(block, 1) + expect(@hotel.reservations.size).must_equal 3 + @hotel.reserve_from_block(block, 3) + expect(@hotel.reservations.size).must_equal 4 + end + end # describe "reserve_from_block" + + # Check if Rooms for a Block are Valid + describe "rooms_valid?" do + it "returns true if the rooms are valid" do + expect(@hotel.rooms_valid?([1, 2, 3])).must_equal true + expect(@hotel.rooms_valid?([18, 19, 20])).must_equal true + expect(@hotel.rooms_valid?([1])).must_equal true + expect(@hotel.rooms_valid?([1, 2, 3, 4, 5, 20])).must_equal true + expect(@hotel.rooms_valid?((1..20).to_a)).must_equal true + end + + it "returns false if the rooms are invalid" do + expect(@hotel.rooms_valid?([])).must_equal false + expect(@hotel.rooms_valid?(nil)).must_equal false + expect(@hotel.rooms_valid?(["1, 2, 3"])).must_equal false + expect(@hotel.rooms_valid?([18, 19, 20, 21])).must_equal false + expect(@hotel.rooms_valid?(1..20)).must_equal false + end + end # describe "rooms_valid?" + + # Check if the Rooms for a Block are Available in the Date Range + # Given a specific date, and that a room is set aside in a hotel block for that specific date, I cannot create another hotel block that includes that specific room for that specific date, because it is unavailable + describe "block_rooms_available?" do + it "returns true if rooms are available in date range" do + @hotel.reserve_room(dt(1), dt(4)) # this will reserve room number 1 + expect(@hotel.block_rooms_available?(dt(1), dt(4), [2, 3, 4])).must_equal true # different rooms + expect(@hotel.block_rooms_available?(dt(4), dt(8), [1, 2, 3])).must_equal true # different dates + @hotel.create_block(dt(1), dt(4), [2, 3, 4], 150) + expect(@hotel.block_rooms_available?(dt(1), dt(4), [5, 6, 7])).must_equal true # different rooms + expect(@hotel.block_rooms_available?(dt(4), dt(8), [1, 2, 3, 4])).must_equal true # different dates + end + + it "returns false if rooms not available in date range" do + @hotel.reserve_room(dt(1), dt(4)) # this will reserve room number 1 + expect(@hotel.block_rooms_available?(dt(1), dt(4), [1, 2, 3])).must_equal false # room 1 in both + expect(@hotel.block_rooms_available?(dt(3), dt(6), [1, 2, 3])).must_equal false # overlapping dates + @hotel.create_block(dt(1), dt(4), [2, 3, 4], 150) + expect(@hotel.block_rooms_available?(dt(1), dt(4), [4, 5, 6])).must_equal false # room 4 in both + expect(@hotel.block_rooms_available?(dt(3), dt(6), [2, 3, 4])).must_equal false # overlapping dates + end + end # describe "block_rooms_available?" + + # Check if a Room is Not blocked in a Specific Date Range + describe "is_room_unblocked?" do + it "returns true if room is not blocked in date range" do + @hotel.create_block(dt(1), dt(4), [1, 2, 3, 4], 150) + expect(@hotel.is_room_unblocked?(dt(1), dt(4), 5)).must_equal true # different room + expect(@hotel.is_room_unblocked?(dt(4), dt(8), 1)).must_equal true # different dates + end + + it "returns false if room is blocked in date range" do + @hotel.create_block(dt(1), dt(4), [1, 2, 3, 4], 150) + expect(@hotel.is_room_unblocked?(dt(1), dt(4), 3)).must_equal false # room 3 is both + expect(@hotel.is_room_unblocked?(dt(3), dt(8), 3)).must_equal false # overlapping dates + end + end # describe "is_room_unblocked?" + + # Checks whether a given block has any rooms available # test to fix this. + describe "available_rooms_in(block)" do + it "returns a list of rooms" do + block = @hotel.create_block(dt(1), dt(4), [1, 2, 3, 4], 150) + available = @hotel.available_rooms_in(block) + expect(available).must_be_kind_of Array + expect(@hotel.rooms_valid?(available)).must_equal true + end + + it "returns all rooms if there are no reservations" do + block = @hotel.create_block(dt(1), dt(4), [1, 2, 3, 4], 150) + available = @hotel.available_rooms_in(block) + expect(available).must_equal [1, 2, 3, 4] + end + + it "returns fewer rooms after a reservation is made" do + block = @hotel.create_block(dt(1), dt(4), [1, 2, 3, 4], 150) + @hotel.reserve_from_block(block, 1) + @hotel.reserve_from_block(block, 3) + available = @hotel.available_rooms_in(block) + expect(available).must_equal [2, 4] + end + + it "returns an empty array if no rooms are left" do + block = @hotel.create_block(dt(1), dt(4), [1, 2, 3, 4], 150) + (1..4).each do |room| + @hotel.reserve_from_block(block, room) + end + available = @hotel.available_rooms_in(block) + expect(available.size).must_equal 0 + end + end # describe "available_rooms_in(block)" + + end # escribe "wave 3" + +end # describe "Hotel::HotelController" \ No newline at end of file diff --git a/test/reservation_test.rb b/test/reservation_test.rb new file mode 100644 index 000000000..eac2fc561 --- /dev/null +++ b/test/reservation_test.rb @@ -0,0 +1,51 @@ +require_relative "test_helper" + +describe "Hotel::Reservation" do + + describe "constructor" do + it "can be initialized with two dates and a room" do + start_date = Date.new(2017, 01, 01) + end_date = start_date + 3 + room = 10 + reservation = Hotel::Reservation.new(start_date, end_date, room) + + expect(reservation).must_be_kind_of Hotel::Reservation + expect(reservation.range.start_date).must_equal start_date + expect(reservation.range.end_date).must_equal end_date + expect(reservation.room).must_equal room + end + + it "raises an ArgumentError if invalid dates provided" do + start_date = Date.new(2017, 01, 01) + end_date1 = start_date - 1 + end_date2 = "2017/01/04" + room = 10 + + # start date can not be after end date + expect{ reservation = Hotel::Reservation.new(start_date, end_date1, room) }.must_raise ArgumentError + # date can not be a string + expect{ reservation = Hotel::Reservation.new(start_date, end_date2, room) }.must_raise ArgumentError + # start date can not be same as end date + expect{ reservation = Hotel::Reservation.new(start_date, start_date, room) }.must_raise ArgumentError + end + end # describe "constructor" + + describe "cost" do + before do + start_date = Date.new(2017, 01, 01) + end_date = start_date + 3 + room = 10 + @reservation = Hotel::Reservation.new(start_date, end_date, room) + end + + it "returns a number" do + expect(@reservation.cost).must_be_kind_of Numeric + end + + it "calculates the cost" do + # Rooms identical and cost $200/night + expect(@reservation.cost).must_equal 600 + end + end # describe "cost" + +end # describe "Hotel::Reservation" \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index c3a7695cf..e6518d098 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,8 +1,17 @@ -# Add simplecov +require "simplecov" + require "minitest" require "minitest/autorun" require "minitest/reporters" +require "minitest/skip_dsl" + +require "date" +SimpleCov.start Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new -# require_relative your lib files here! +# Require_relative your lib files here! +require_relative "../lib/hotel_controller.rb" +require_relative "../lib/reservation.rb" +require_relative "../lib/date_range.rb" +require_relative "../lib/block.rb" \ No newline at end of file