diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..f4f704fca --- /dev/null +++ b/Rakefile @@ -0,0 +1,9 @@ +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.libs = ["lib"] + t.warning = false + t.test_files = FileList['specs/*_spec.rb'] +end + +task default: :test diff --git a/design-activity.md b/design-activity.md new file mode 100644 index 000000000..1aeda5e66 --- /dev/null +++ b/design-activity.md @@ -0,0 +1,55 @@ +## Prompts + +1. **What classes does each implementation include? Are the lists the same?** + + Each implementation includes the same classes: + + a CartEntry class + + a ShoppingCart class + + an Order class + +2. **Write down a sentence to describe each class.** + + + `CartEntry`: This is a single kind of item in the online shopping cart, and it tracks the unit price of the item, and the quantity of that item ordered. + + `ShoppingCart`: This is an object representing the actual shopping cart, and it consists of an array of entries (items in the shopping cart). + + `Order`: This is the online order, which contains a shopping cart, and it uses sales tax to calculate the total price of the cart contents. + +3. **How do the classes relate to each other? It might be helpful to draw a diagram on a whiteboard or piece of paper.** + + A `ShoppingCart` has a `CartEntry`, and an `Order` has a `ShoppingCart`--i.e. `Order` depends on `ShoppingCart`, which depends on `CartEntry`. + +4. **What _data_ does each class store? How (if at all) does this differ between the two implementations?** + + The classes in each implementation store the exact same data. `CartEntry` stores `unit_price` and `quantity`, `ShoppingCart` stores `entries` (which are cart entries), and `Order` stores `SALES_TAX` (a constant) and a cart, which is a `ShoppingCart` object. + +5. **What _methods_ does each class have? How (if at all) does this differ between the two implementations?** + + Aside from the accessor methods, Implementation A only has one defined method, which calculates the total price of an order, which means it's more tightly coupled to both the `ShoppingCart` and `CartEntry` classes, because it depends on `ShoppingCart` being an array of CartEntries, and each `CartEntry` having a `unit_price` and `quantity`. + + Implementation B, on the other hand, uses methods in both the `CartEntry` and `ShoppingCart` classes to calculate the price of each `CartEntry` object and the price of each `ShoppingCart` object. This decouples `ShoppingCart` from `CartEntry` (it calls `CartEntry`'s price method rather than manipulating `CartEntry`'s variables directly), as well as decoupling `Order` from both `ShoppingCart` and `CartEntry`. + +6. **Consider the `Order#total_price` method. In each implementation:** + + Is logic to compute the price delegated to "lower level" classes like `ShoppingCart` and `CartEntry`, or is it retained in `Order`? + + Does `total_price` directly manipulate the instance variables of other classes? + + + Implementation A: (see above) No, the logic to compute the price isn't delegated to the lower-level classes. Instead, `total_price` directly manipulates the instance variables of both `ShoppingCart` (it gets `@entries` from `ShoppingCart`) and `CartEntry` (for each `CartEntry`, it gets `@unit_price` and `@quantity`). + + + Implementation B: (see above) Yes, the logic is delegated to the lower-level classes. `CartEntry` is responsible for calculating its own price, and `ShoppingCart` is responsible for calculating the total price of its contents. `Order` then simply calls the price method for `ShoppingCart` and adds sales tax to calculate the total price. + +7. **If we decide items are cheaper if bought in bulk, how would this change the code? Which implementation is easier to modify?** + + This would mean changing the `CartEntry` class so that if quantity exceeds a particular amount, `unit_price` is then set to a discounted rate. Implementation B is easier to modify, because it already has a `price` method, so it simply means modifying that `price` method to account for bulk orders. However, Implementation A has no such method—all the calculations are taken care of in the `Order` class, which means the `total_price` method will become more complex. + +8. **Which implementation better adheres to the single responsibility principle?** + + Implementation B because each class calculates its own price using its own instance variables. + +9. **Bonus question: Which implementation is more loosely coupled?** + + Implementation B + + +## Refactoring Hotel + +My `Hotel` class is pretty tightly coupled to the `Room` class, because it requires the user to pass `Room` objects (as opposed to a room number) to reserve or block rooms. To change this, I would make use of the hash I set up when initializing my `Hotel` class with rooms where the room number is the key and `Room` object is the value. Then, given a room number, I could first retrieve the `Room` object and then reserve it. This would be an improvement because it's much more user friendly (what user is going to want to find a room object before reserving it? No one, that's who). + +Also, while the `Block` class can reserve rooms from within a block, that functionality isn't in the `Hotel` class itself, and it seems like it would make more sense to have all the blocking/reserving functionality of a hotel available in one class (provided that it delegates the appropriate tasks to lower-level classes), especially since you can reserve a room in the `Hotel` class so it doesn't make sense that you can't reserve a blocked room, as well. So I could have a method that calls the appropriate method(s) in the `Block` class to reserve a room in a given block. This is an improvement b/c it's also more user friendly and puts all the user interface methods (i.e. what the CLI would draw from) in one place rather than having them scattered around different classes. diff --git a/lib/block.rb b/lib/block.rb new file mode 100644 index 000000000..b2af4aeb5 --- /dev/null +++ b/lib/block.rb @@ -0,0 +1,77 @@ +require_relative 'reservation' +require_relative 'room' +require_relative 'reservable' + +module Hotel + + class Block + include Reservable + + @@all_blocks = [] + + MAX_ROOMS = 5 + + attr_accessor :check_in, :check_out, :discount, :room_block, :block_id + + def initialize(check_in, check_out, discount, room_block) + # check input + check_dates(check_in, check_out) + check_room_block(room_block, check_in, check_out) + check_discount(discount) + + @block_id = @@all_blocks.length + 1 + @check_in = check_in + @check_out = check_out + + @discount = discount # discount as decimal (e.g. 0.8 for 80% of orig rate) + @room_block = room_block # array of rooms + + # add block to each room + room_block.cycle(1) { |room| room.blocks << self } + + @@all_blocks << self + + end + + def room_available?(room) + raise ArgumentError.new("Room #{room.room_num} isn't in the block") if !room_block.include?(room) + + return !room.booked?(check_in, check_out) + end + + def find_avail_in_block + return room_block.select { |room| room_available?(room) } + end + + def reserve(room) + raise ArgumentError.new("Room #{room.room_num} isn't in the block") if !room_block.include?(room) + + raise ArgumentError.new("Room #{room.room_num} isn't available for the selected dates") if room.booked?(check_in, check_out) + + discounted_rate = discount * room.rate + return room.reserve(check_in, check_out, discounted_rate) + end + + def self.all + return @@all_blocks + end + + def self.clear + @@all_blocks = [] + end + + def self.find(block_id) + blocks = self.all + + blocks.each do |block| + if block.block_id == block_id + return block + end + end + + return nil + end + + end # end of Block class + +end diff --git a/lib/hotel.rb b/lib/hotel.rb new file mode 100644 index 000000000..8ba83b553 --- /dev/null +++ b/lib/hotel.rb @@ -0,0 +1,79 @@ +require_relative 'room' +require_relative 'reservation' +require_relative 'block' + +module Hotel + + class Hotel + include Reservable + + NUM_ROOMS = 20 + + attr_reader :rooms + + def initialize(num_rooms = NUM_ROOMS) + # check input + check_room_num(num_rooms) + + @rooms = {} + + # loop through num_rooms and add rooms to hash + (1..num_rooms).each do |num| + @rooms[num] = ::Hotel::Room.new(num) + end + + end + + def reserve(start_date, end_date, room_num) + check_dates(start_date, end_date) + room = @rooms[room_num] + + raise ArgumentError.new("Room #{room_num} isn't available for the selected dates") if room.booked?(start_date, end_date) || room.blocked?(start_date, end_date) + + return room.reserve(start_date, end_date) + + end + + def block(start_date, end_date, discount, room_nums) + block_rooms = room_nums.map { |room_num| rooms[room_num] } + + raise ArgumentError.new("One or more rooms is unavailable for the selected dates") if block_rooms.any? { |room| room.booked?(start_date, end_date) || room.blocked?(start_date, end_date) } + + return ::Hotel::Block.new(start_date, end_date, discount, block_rooms) + end + + def reserve_blocked_room(block, room_num = nil) + # allows user to specify a room number; else system automatically + # choose a room from the block + avail_rooms = block.find_avail_in_block + + raise ArgumentError.new("No more rooms available in block") if avail_rooms.length < 1 + + # assign room num if not provided + room = room_num == nil ? avail_rooms.pop : rooms[room_num] + + # if room num provided, check if available + if room_num && !block.room_available?(room) + raise ArgumentError.new("Room #{room_num} is already booked") + else + block.reserve(room) + end + end + + def find_reservations_by_date(date) + return ::Hotel::Reservation.find_by_date(date) + end + + def find_avail_rooms(start_date, end_date) + # returns a hash of rooms available in the date range + + # check input + check_dates(start_date, end_date) + + return rooms.reject { |room_num, room| room.booked?(start_date, end_date) || room.blocked?(start_date, end_date) } + + end + + end # end of Hotel class + +end diff --git a/lib/reservable.rb b/lib/reservable.rb new file mode 100644 index 000000000..00b35d235 --- /dev/null +++ b/lib/reservable.rb @@ -0,0 +1,47 @@ +require 'date' +require_relative 'room' + +module Reservable + + def check_dates(start_date, end_date) + + raise TypeError.new("#{start_date} must be of type Date") if start_date.class != Date + raise TypeError.new("#{end_date} must be of type Date") if end_date.class != Date + raise ArgumentError.new("Check out must be later than check in") if start_date >= end_date + raise ArgumentError.new("Can't reserve a room for a date before today") if start_date < Date.today + + end + + def check_room_num(num) + raise TypeError.new("#{num} must of type Integer") if num.class != Integer + raise ArgumentError.new("Invalid number of rooms") if num < 1 + end + + def check_room(room) + raise TypeError.new("#{room} must be of type Hotel::Room") if room.class != Hotel::Room + end + + def check_room_block(room_block, check_in, check_out) + raise TypeError.new("Block of rooms must be an array of Room objects") if room_block.class != Array || room_block[0].class != Hotel::Room + + raise ArgumentError.new("Invalid number of rooms in block") if room_block.length > Hotel::Block::MAX_ROOMS || room_block.length < 1 + + raise ArgumentError.new("Can't have unavailable rooms in the block") if room_block.any? { |room| room.booked?(check_in, check_out) || room.blocked?(check_in, check_out) } + end + + def check_rate(rate) + raise TypeError.new("#{rate} must be of type Integer") if rate.class != Integer + raise ArgumentError.new("Rate must be greater than 0") if rate < 1 + end + + def check_discount(discount) + # must be decimal representing percentage of full cost (e.g. 0.8 for 80% of orig rate) + raise TypeError.new("#{discount} must be of type Float") if discount.class != Float + raise ArgumentError.new("Not a discounted rate") if discount >= 1 || discount <= 0 + end + + def include?(date) + return date >= @check_in && date < @check_out + end + +end diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..b2c6ec1ae --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,83 @@ +require 'date' +require_relative 'room' +require_relative 'reservable' + +module Hotel + + class Reservation + include Comparable + include Reservable + + @@all_reservations = [] + + attr_accessor :check_in, :check_out, :room_num, :rate, :reservation_id + + def initialize(check_in, check_out, room, rate = room.rate) + + check_dates(check_in, check_out) + check_room(room) + + @reservation_id = @@all_reservations.length + 1 + @check_in = check_in + @check_out = check_out + @room_num = room.room_num + @rate = rate + + @@all_reservations << self + end + + def ==(other_reservation) + return check_in == other_reservation.check_in && check_out == other_reservation.check_out && room_num == other_reservation.room_num + end + + # def include?(date) + # return date >= check_in && date < check_out + # end + + def total_cost + num_nights = (check_out - check_in).to_i + return num_nights * rate + end + + def self.all + return @@all_reservations + end + + def self.find(reservation_id) + reservations = self.all + + reservations.each do |reservation| + if reservation.reservation_id == reservation_id + return reservation + end + end + + return nil + end + + def self.clear + # clears all reservations (for testing) + @@all_reservations = [] + end + + def self.find_by_date(date) + raise TypeError.new("#{date} must be of type Date") if date.class != Date + + return @@all_reservations.select { |reservation| reservation.include?(date)} + + end + + def to_s + # return human readable representation + s = "Reservation id: #{reservation_id}\n" + s += "Room number: #{room_num}\n" + s += "Check-in: #{check_in}\n" + s += "Check-out: #{check_out}\n" + s += "Total cost: #{total_cost}\n" + + return s + + end + + end # end of Reservation class +end diff --git a/lib/room.rb b/lib/room.rb new file mode 100644 index 000000000..0e549d6e2 --- /dev/null +++ b/lib/room.rb @@ -0,0 +1,117 @@ +require_relative 'reservation' +require_relative 'reservable' + +module Hotel + + class Room + include Reservable + include Comparable + + @@all_rooms = [] + + DEFAULT_RATE = 200 + + attr_reader :room_num, :reservations, :blocks + attr_accessor :rate + + def initialize(room_num, rate = DEFAULT_RATE) + + check_room_num(room_num) + check_rate(rate) + + @room_num = room_num + @reservations = [] + @blocks = [] + @rate = rate + + @@all_rooms << self + + end + + def <=>(other_room) + room_num <=> other_room.room_num + end + + def reserve(start_date, end_date, rate_to_charge = rate) + + raise ArgumentError.new("Room #{room_num} isn't available for the given dates") if booked?(start_date, end_date) + + new_reservation = ::Hotel::Reservation.new(start_date, end_date, self, rate_to_charge) + reservations << new_reservation#.reservation_id + + return new_reservation + + end + + def booked?(start_date, end_date = start_date.next_day) + + return includes_date?(reservations, start_date, end_date) + + end + + def blocked?(start_date, end_date = start_date.next_day) + + return includes_date?(blocks, start_date, end_date) + + end + + def self.all + # @all_rooms.sort! + return @@all_rooms + end + + def self.find(room_num) + rooms = self.all + + rooms.each do |room| + if room.room_num == room_num + return room + end + end + + return nil + end + + def self.clear + @@all_rooms = [] + end + + def to_s + # return human readable representation + s = "Room number: #{room_num}\n" + s += "Rate per night: $#{rate}\n" + s += "Reservations:\n" + + reservations.each do |reservation| + s += reservation.to_s + end + + # s += "Blocks:\n" + # blocks.each do |block| + # s += block.to_s + # end + + return (s += "\n") + + end + + private + + def includes_date?(blocks_or_res_array, start_date, end_date) + # don't include final date since check-out doesn't conflict with check-in of a new reservation + date_range = (start_date...end_date).to_a + + blocks_or_res_array.each do |item| + date_range.each do |date| + if item.include?(date) + return true + end + end + end + + return false + end + + end # end of Room class + +end diff --git a/specs/block_spec.rb b/specs/block_spec.rb new file mode 100644 index 000000000..f89c85d43 --- /dev/null +++ b/specs/block_spec.rb @@ -0,0 +1,166 @@ +require_relative 'spec_helper' + +describe "Testing Block class" do + + let(:rooms) { (1..5).to_a.map { |num| Hotel::Room.new(num) } } + let(:today) { Date.today } + let(:two_days_later) { Date.today + 2 } + let(:block) { Hotel::Block.new(Date.today, Date.today + 2, 0.8, rooms)} + let(:room_to_reserve) { rooms[0] } + + describe "#initialize" do + + it "Creates a Block with a check_in and check_out date, an array of rooms, and a discounted rate" do + room1 = block.room_block[0] + + block.must_be_instance_of Hotel::Block + block.check_in.must_equal today + block.check_out.must_equal two_days_later + block.room_block.must_equal rooms + (block.discount * room1.rate).must_be :<, room1.rate + end + + it "Creates a block id based that auto-increments based on length of @@all_blocks" do + Hotel::Block.clear + + block1 = Hotel::Block.new(today, two_days_later, 0.8, rooms) + block1.block_id.must_equal 1 + + block2 = Hotel::Block.new(two_days_later, two_days_later + 1, 0.8, rooms) + block2.block_id.must_equal 2 + end + + it "Creates a block with no more than MAX_ROOMS num of rooms" do + rooms << Hotel::Room.new(6) + + proc {Hotel::Block.new(today, two_days_later, 0.8, rooms) }.must_raise ArgumentError + end + + it "Raises an error if it tries to include an unavailable room for the given dates in the block" do + unavail_room = rooms[0] + unavail_room.reserve(today, two_days_later) + + proc { Hotel::Block.new(today, two_days_later, 0.8, rooms) }.must_raise ArgumentError + end + + it "Adds itself to each room's list of blocks" do + block1 = Hotel::Block.new(today, two_days_later, 0.8, rooms) + + rooms.each do |room| + room.blocks.length.must_equal 1 + room.blocks.must_include block1 + end + + block2 = Hotel::Block.new(two_days_later, two_days_later + 3, 0.8, rooms) + + rooms.each do |room| + room.blocks.length.must_equal 2 + room.blocks.must_include block2 + end + + end + end + + describe "#self.clear" do + it "Clears all Block instances from the @@all_blocks array" do + Hotel::Block.clear + Hotel::Block.all.must_equal [] + + new_block = Hotel::Block.new(today, two_days_later, 0.8, rooms) + Hotel::Block.all.must_equal [new_block] + + Hotel::Block.clear + Hotel::Block.all.must_equal [] + + end + end + + describe "#self.all" do + it "Returns an array of all Block instances" do + Hotel::Block.clear + + block1 = Hotel::Block.new(today, two_days_later, 0.8, rooms) + Hotel::Block.all.must_equal [block1] + + block2 = Hotel::Block.new(two_days_later, two_days_later + 1, 0.8, rooms) + Hotel::Block.all.must_equal [block1, block2] + end + end + + describe "#self.find" do + + before do + Hotel::Block.clear + + @block_to_find = Hotel::Block.new(today, two_days_later, 0.8, rooms) + Hotel::Block.new(two_days_later, two_days_later + 1, 0.8, rooms) + end + + it "Returns a block matching the given block_id" do + Hotel::Block.find(1).must_equal @block_to_find + end + + it "Returns nil if no such block found" do + Hotel::Block.find(5).must_be_nil + end + end + + describe "#is_available" do + + it "Returns true if a room in the block is available" do + block.room_available?(room_to_reserve).must_equal true + + room_to_reserve.reserve(today, two_days_later) + block.room_available?(room_to_reserve).must_equal false + end + + it "Raises an argument error if room isn't in the block" do + wrong_room = Hotel::Room.new(6) + proc { block.room_available?(wrong_room) }.must_raise ArgumentError + end + end + + describe "#find_avail_in_block" do + + it "Returns a list of rooms that are available in the block" do + block.find_avail_in_block.must_equal rooms + + rooms[0].reserve(today, two_days_later) + rooms[1].reserve(today, two_days_later) + + block.find_avail_in_block.must_equal rooms[2..-1] + end + + it "Returns an empty array if no rooms available" do + block.find_avail_in_block.must_equal rooms + + 5.times do |num| + rooms[num].reserve(today, two_days_later) + end + + block.find_avail_in_block.must_equal [] + end + end + + describe "#reserve" do + + it "Reserves a room for the dates of the block" do + block_res = block.reserve(room_to_reserve) + + block_res.check_in.must_equal today + block_res.check_out.must_equal two_days_later + end + + it "Raises an error if it tries to reserve a room not in the block" do + room_outside_block = Hotel::Room.new(20) + proc { block.reserve(room_outside_block) }.must_raise ArgumentError + end + + it "Raises an error if it tries to reserve an room that's already reserved" do + block.reserve(room_to_reserve) + proc { block.reserve(room_to_reserve) }.must_raise ArgumentError + proc { room_to_reserve.reserve(today, two_days_later) }.must_raise ArgumentError + end + end + +end diff --git a/specs/hotel_spec.rb b/specs/hotel_spec.rb new file mode 100644 index 000000000..179286181 --- /dev/null +++ b/specs/hotel_spec.rb @@ -0,0 +1,167 @@ +require_relative 'spec_helper' + +describe "Testing Hotel class" do + let(:hotel) { Hotel::Hotel.new } + let(:today) { Date.today } + let(:three_days_later) { Date.today + 3} + + describe "#initialize" do + + it "Creates a hotel class with a hash of rooms" do + hotel.must_be_instance_of Hotel::Hotel + hotel.rooms.must_be_kind_of Hash + + hotel.rooms.each do |room_num, room| + room.must_be_instance_of Hotel::Room + end + end + + it "Creates a hotel with NUM_ROOMS num of rooms as the default" do + hotel.rooms.length.must_equal Hotel::Hotel::NUM_ROOMS + end + + it "Creates a hotel with the specified number of rooms" do + num_rooms = 17 + new_hotel = Hotel::Hotel.new(num_rooms) + new_hotel.rooms.length.must_equal num_rooms + end + + it "Raises an error if not passed a valid number for num of rooms" do + proc { Hotel::Hotel.new(-1) }.must_raise ArgumentError + proc { Hotel::Hotel.new(0) }.must_raise ArgumentError + proc { Hotel::Hotel.new(nil) }.must_raise TypeError + proc { Hotel::Hotel.new("cat") }.must_raise TypeError + proc { Hotel::Hotel.new(3.14) }.must_raise TypeError + + end + + it "Creates rooms with room nums between 1 & specified number of rooms" do + num_rooms = 25 + big_hotel = Hotel::Hotel.new(num_rooms) + num_big_hotel_rooms = big_hotel.rooms.length + + big_hotel.rooms.each do |room_num, room| + room.room_num.must_be :>=, 1 + room.room_num.must_be :<=, num_big_hotel_rooms + end + end + + end + + describe "#reserve" do + before do + Hotel::Reservation.clear + @room1 = hotel.rooms[1] + end + + it "Reserves the given room for the given dates" do + @room1.reservations.must_equal [] + + new_res = hotel.reserve(today, three_days_later, 1) + @room1.reservations.must_include new_res + end + + it "Raises error when it tries to reserve a room that's already reserved" do + + hotel.reserve(today, three_days_later, 1) + + proc { hotel.reserve(today + 1, three_days_later, 1) }.must_raise ArgumentError + + end + + it "Raises error when it tries to reserve a room that's in a block" do + + Hotel::Block.new(today, three_days_later, 0.85, [@room1]) + + proc { hotel.reserve(today, today + 2, 1) }.must_raise ArgumentError + + end + + end + + describe "#find_reservations_by_date" do + before do + Hotel::Reservation.clear + end + + it "Returns a list of reservations for that date" do + hotel.find_reservations_by_date(today).must_equal [] + + res1 = hotel.reserve(today, three_days_later, 1) + hotel.find_reservations_by_date(today).must_equal [res1] + hotel.find_reservations_by_date(three_days_later).must_equal [] + + res2 = hotel.reserve(today + 1, three_days_later, 2) + hotel.find_reservations_by_date(today).must_equal [res1] + hotel.find_reservations_by_date(today + 1).must_equal [res1, res2] + end + + end + + describe "#find_avail_rooms" do + before do + Hotel::Reservation.clear + end + + it "Returns hash of rooms available for date range (doesn't count check-out date as unavail)" do + hotel.find_avail_rooms(today, three_days_later).must_equal hotel.rooms + + hotel.reserve(today, three_days_later, 1) + hotel.reserve(today, three_days_later + 1, 2) + + hotel.find_avail_rooms(today, three_days_later).wont_include hotel.rooms[2] + hotel.find_avail_rooms(today, three_days_later - 1).wont_include hotel.rooms[1] + hotel.find_avail_rooms(today, three_days_later - 1).length.must_equal 18 + + end + + it "Raises ArgumentError if start date is later than end date" do + room1 = hotel.rooms[1] + proc { hotel.find_avail_rooms(today, three_days_later, room1) }.must_raise ArgumentError + + proc { hotel.find_avail_rooms(hotel, three_days_later, room1) }.must_raise ArgumentError + end + end + + describe "#reserve_blocked_room" do + before do + Hotel::Reservation.clear + @block = hotel.block(today, three_days_later, 0.8, [1, 2, 3]) + @room1 = hotel.rooms[1] + end + + it "Raises ArgumentError if all rooms already booked" do + @block.reserve(@room1) + @block.reserve(hotel.rooms[2]) + @block.reserve(hotel.rooms[3]) + + proc { hotel.reserve_blocked_room(@block) }.must_raise ArgumentError + end + + it "Raises ArgumentError if specified room is already booked" do + @block.reserve(@room1) + proc { hotel.reserve_blocked_room(@block, 1) }.must_raise ArgumentError + end + + it "Reserves the specified room in the block" do + hotel.reserve_blocked_room(@block, 1) + + res = Hotel::Reservation.new(today, three_days_later, @room1) + @room1.reservations.must_include res + hotel.find_avail_rooms(today, three_days_later).wont_include(1) + end + + it "Reserves any available room in block if room num isn't specified" do + @block.reserve(@room1) + + hotel.reserve_blocked_room(@block) + hotel.find_avail_rooms(today, three_days_later).wont_include(2 || 3) + + hotel.reserve_blocked_room(@block) + hotel.find_avail_rooms(today, three_days_later).wont_include(2 && 3) + end + + end + + +end diff --git a/specs/reservable_spec.rb b/specs/reservable_spec.rb new file mode 100644 index 000000000..1cb72df61 --- /dev/null +++ b/specs/reservable_spec.rb @@ -0,0 +1,5 @@ +require_relative 'spec_helper' + +describe "Testing Reservable module" do + +end diff --git a/specs/reservation_spec.rb b/specs/reservation_spec.rb new file mode 100644 index 000000000..781a3abf2 --- /dev/null +++ b/specs/reservation_spec.rb @@ -0,0 +1,153 @@ +require_relative 'spec_helper' + +describe "Testing Reservation class" do + + let(:room) { Hotel::Room.new(4) } + let(:res) { Hotel::Reservation.new(Date.today, Date.today + 2, Hotel::Room.new(5)) } + let(:today) { Date.today } + let(:two_days_later) { Date.today + 2 } + + describe "#initialize" do + + it "Creates a Reservation with a date range and room number" do + + res.check_in.must_equal today + res.check_out.must_equal two_days_later + res.room_num.must_equal 5 + + end + + it "Creates a reservation_id that auto-increments based on length of @@all_reservations" do + Hotel::Reservation.clear + + res1 = Hotel::Reservation.new(today, two_days_later, room) + res1.reservation_id.must_equal 1 + + res2 = Hotel::Reservation.new(today, two_days_later, Hotel::Room.new(1)) + res2.reservation_id.must_equal 2 + end + + describe "Testing Reservable mixin" do + + it "Raises an error if check in or check out aren't dates" do + + not_dates = ["cat", nil, 0, -3.14] + + not_dates.each do |invalid_input| + proc { Hotel::Reservation.new(invalid_input, two_days_later, room) }.must_raise TypeError + end + + not_dates.each do |invalid_input| + proc { Hotel::Reservation.new(today, invalid_input, room) }.must_raise TypeError + end + + end + + it "Raises an error if check in is before today's date" do + proc { Hotel::Reservation.new(today.prev_day, today, room) }.must_raise ArgumentError + end + + it "Raises an error if check out isn't later than check in " do + proc { Hotel::Reservation.new(two_days_later, today) }.must_raise ArgumentError + + proc { Hotel::Reservation.new(today, today) }.must_raise ArgumentError + end + + end + + end + + describe "#self.clear" do + it "Clears all Reservation instances from the all_reservations array" do + Hotel::Reservation.clear + Hotel::Reservation.all.must_equal [] + + new_res = Hotel::Reservation.new(today, two_days_later, room) + Hotel::Reservation.all.must_equal [new_res] + + Hotel::Reservation.clear + Hotel::Reservation.all.must_equal [] + end + end + + describe "#self.all" do + it "Returns a list of all Reservation instances" do + Hotel::Reservation.clear + + res1 = Hotel::Reservation.new(today, two_days_later, room) + Hotel::Reservation.all.must_equal [res1] + + res2 = Hotel::Reservation.new(today, two_days_later, Hotel::Room.new(1)) + Hotel::Reservation.all.must_equal [res1, res2] + end + end + + describe "#self.find(id)" do + before do + Hotel::Reservation.clear + + @res_to_find = Hotel::Reservation.new(today, two_days_later, room) + Hotel::Reservation.new(today, two_days_later, Hotel::Room.new(1)) + + end + + it "Returns a reservation matching the given reservation id" do + Hotel::Reservation.find(1).must_equal @res_to_find + end + + it "Returns nil if no reservation found with given id" do + Hotel::Reservation.find(3).must_be_nil + end + end + + describe "#self.find_by_date" do + it "Returns a list of reservations for a specific date (doesn't include checkout date)" do + Hotel::Reservation.clear + + res1 = Hotel::Reservation.new(today, two_days_later, room) + res2 = Hotel::Reservation.new(today, two_days_later + 1, Hotel::Room.new(1)) + + Hotel::Reservation.find_by_date(today).must_equal [res1, res2] + Hotel::Reservation.find_by_date(two_days_later).must_equal [res2] + Hotel::Reservation.find_by_date(today + 4).must_equal [] + end + + it "Raises an error if date isn't a Date object" do + invalid_dates = ["cat", [], nil, 4] + + invalid_dates.each do |invalid_date| + proc { Hotel::Reservation.find_by_date(invalid_date) }.must_raise TypeError + end + end + end + + describe "#total_cost" do + + it "Returns the total cost of the reservation" do + + res = Hotel::Reservation.new(today, two_days_later, room) + expected_cost = 400 # 2 nights @ 200/night + res.total_cost.must_equal expected_cost + + room.rate = 300 + more_expensive_res = Hotel::Reservation.new(two_days_later, two_days_later + 2, room) + higher_cost = 600 # 2 nights @ 300/night + more_expensive_res.total_cost.must_equal higher_cost + + end + + it "Returns the total discounted cost of the reservation if the room is in a block" do + block = Hotel::Block.new(today, two_days_later, 0.8, [room]) + block_res = block.reserve(room) + expected_cost = 320 # 0.8 * 200/night * 2 nights + block_res.total_cost.must_equal expected_cost + + new_block = Hotel::Block.new(two_days_later, two_days_later + 1, 0.7, [room]) + new_block_res = new_block.reserve(room) + new_cost = 140 #0.7 * 200/night * 1 night + new_block_res.total_cost.must_equal new_cost + end + + end + +end diff --git a/specs/room_spec.rb b/specs/room_spec.rb new file mode 100644 index 000000000..5b09fbc7d --- /dev/null +++ b/specs/room_spec.rb @@ -0,0 +1,190 @@ +require_relative 'spec_helper' + +describe "Testing Room class" do + + let(:room) { Hotel::Room.new(4) } + let(:today) { Date.today } + let(:three_days_later) { Date.today + 3 } + + describe "#initialize" do + + it "Creates a Room object with the given room number" do + room.must_be_instance_of Hotel::Room + room.room_num.must_equal 4 + end + + it "Sets a default rate if no rate provided" do + room.rate.must_equal Hotel::Room::DEFAULT_RATE + + expected_rate = 300 + Hotel::Room.new(4, 300).rate.must_equal expected_rate + end + + it "Creates an empty array of Reservation ids" do + room.reservations.must_equal [] + end + + it "Creates an empty array of Block ids" do + room.blocks.must_equal [] + end + + describe "Testing Reservable mixin" do + + it "Raises an error if room number isn't a valid num" do + # doesn't test for room num > 20 b/c optional param in Hotel allows for more rooms + proc { Hotel::Room.new(-1) }.must_raise ArgumentError + proc { Hotel::Room.new(0) }.must_raise ArgumentError + proc { Hotel::Room.new("cat") }.must_raise TypeError + proc { Hotel::Room.new(4.23) }.must_raise TypeError + proc { Hotel::Room.new(nil) }.must_raise TypeError + + end + + it "Raises an error if rate isn't a valid num" do + proc { Hotel::Room.new(4, -1) }.must_raise ArgumentError + proc { Hotel::Room.new(4, 0) }.must_raise ArgumentError + proc { Hotel::Room.new(4, "cat") }.must_raise TypeError + proc { Hotel::Room.new(4, 4.23) }.must_raise TypeError + proc { Hotel::Room.new(4, nil) }.must_raise TypeError + + end + end + + end + + describe "#self.clear" do + it "Clears all room instances from class array of all_rooms" do + Hotel::Room.clear + Hotel::Room.all.must_equal [] + + Hotel::Room.new(2) + Hotel::Room.all.length.must_equal 1 + + Hotel::Room.clear + Hotel::Room.all.must_equal [] + end + + end + + describe "#self.all" do + it "Returns a list of all Room instances" do + Hotel::Room.clear + + room1 = Hotel::Room.new(1) + Hotel::Room.all.must_equal [room1] + + room2 = Hotel::Room.new(2) + Hotel::Room.all.must_equal [room1, room2] + end + end + + describe "#self.find" do + before do + Hotel::Room.clear + + 5.times do |num| + Hotel::Room.new(num + 1) + end + end + + it "Returns a room object matching the given room number" do + room = Hotel::Room.new(1) + + Hotel::Room.find(1).must_equal room + end + + it "Returns nil if no such room is found" do + Hotel::Room.find(6).must_be_nil + end + end + + describe "#reserve" do + + before do + room.reserve(today, three_days_later) + end + + it "Adds the reservation to the room's reservations array" do + + room.reservations.length.must_equal 1 + room.reservations[0].must_be_instance_of Hotel::Reservation + + end + + it "Reserves the room for the specified start and end dates" do + + res = room.reservations[0] + + res.check_in.must_equal today + res.check_out.must_equal three_days_later + + end + + it "Raises an error if the room isn't available" do + proc { room.reserve(today, three_days_later) }.must_raise ArgumentError + end + + it "Returns the new reservation if the reservation is created" do + + new_res = Hotel::Reservation.new(three_days_later, three_days_later.next_day, room) + + room.reserve(three_days_later, three_days_later.next_day).must_equal new_res + + end + + end + + describe "#booked?" do + before do + room.reserve(today, three_days_later) + end + + it "Returns true if the room is booked for a given date (one param + optional param)" do + + room.booked?(today).must_equal true + room.booked?(today.next_day).must_equal true + room.booked?(three_days_later).must_equal false + + room.reserve(three_days_later, three_days_later + 2) + room.booked?(three_days_later).must_equal true + + end + + it "Returns true if room is booked for given date range (two params)" do + + room.booked?(today - 2, today).must_equal false + room.booked?(today + 1, three_days_later).must_equal true + + end + end + + describe "#blocked?" do + + let(:rooms) { [2, 4, 5, 9, 20].map { |num| Hotel::Room.new(num) } } + + it "Returns true if a room is in a block for the given date range (no reservation)" do + + Hotel::Block.new(today, three_days_later, 0.8, rooms) + blocked_room = rooms[0] + blocked_room.blocked?(today - 1, today + 1).must_equal true + + unblocked_room = Hotel::Room.new(15) + unblocked_room.blocked?(today, three_days_later).must_equal false + + end + + it "Returns true if a room is in a block for the given date range (with reservation)" do + + new_block = Hotel::Block.new(today, three_days_later, 0.8, rooms) + reserved_room = rooms[0] + new_block.reserve(reserved_room) + reserved_room.blocked?(today, three_days_later).must_equal true + + unblocked_room_with_res = Hotel::Room.new(15) + unblocked_room_with_res.reserve(today, three_days_later) + unblocked_room_with_res.blocked?(today, three_days_later).must_equal false + + end + end + +end diff --git a/specs/spec_helper.rb b/specs/spec_helper.rb new file mode 100644 index 000000000..2900bb250 --- /dev/null +++ b/specs/spec_helper.rb @@ -0,0 +1,19 @@ +require 'simplecov' +SimpleCov.start + +require 'minitest/autorun' +require 'minitest/reporters' +require 'minitest/skip_dsl' +require 'pry' +require 'date' + +require_relative '../lib/room' +require_relative '../lib/reservation' +require_relative '../lib/hotel' +require_relative '../lib/block' +require_relative '../lib/reservable' + +reporter_options = { color:true} +Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new(reporter_options) + +# Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new