diff --git a/Guardfile b/Guardfile index 6760f9177..fa59fc3ef 100644 --- a/Guardfile +++ b/Guardfile @@ -1,4 +1,4 @@ -guard :minitest, bundler: false, rubygems: false do +guard :minitest, bundler: false, autorun: false, rubygems: false do # with Minitest::Spec watch(%r{^spec/(.*)_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } diff --git a/design-activity.md b/design-activity.md new file mode 100644 index 000000000..a1f16832c --- /dev/null +++ b/design-activity.md @@ -0,0 +1,130 @@ +Implementation A +class CartEntry + attr_accessor :unit_price, :quantity + def initialize(unit_price, quantity) + @unit_price = unit_price + @quantity = quantity + end +end + +class ShoppingCart + attr_accessor :entries + def initialize + @entries = [] + end +end + +class Order + SALES_TAX = 0.07 + def initialize + @cart = ShoppingCart.new + end + + def total_price + sum = 0 + @cart.entries.each do |entry| + sum += entry.unit_price * entry.quantity + end + return sum + sum * SALES_TAX + end +end +Implementation B +class CartEntry + def initialize(unit_price, quantity) + @unit_price = unit_price + @quantity = quantity + end + + def price + return @unit_price * @quantity + end +end + +class ShoppingCart + def initialize + @entries = [] + end + + def price + sum = 0 + @entries.each do |entry| + sum += entry.price + end + return sum + end +end + +class Order + SALES_TAX = 0.07 + def initialize + @cart = ShoppingCart.new + end + + def total_price + subtotal = @cart.price + return subtotal + subtotal * SALES_TAX + end +end + + +What classes does each implementation include? Are the lists the same? +Implementation A classes: +1 - CartEntry +2 - ShoppingCart +3 - Order + +Implementation B classes: +1 - CartEntry +2 - ShoppingCart +3 - Order + +The lists are the same + +Write down a sentence to describe each class. +Implementation A classes: +1 - CartEntry: Initializes a new instance of an item's price and quantity +2 - ShoppingCart: Stores all the entries in an array +3 - Order: Creates a new instance of a ShoppingCart, and calculates the total price of items in the cart + +Implementation B classes: +1 - CartEntry: Same as A +2 - ShoppingCart: Same as A +3 - Order: Same as A + +How do the classes relate to each other? It might be helpful to draw a diagram on a whiteboard or piece of paper. +In A, the CartEntry class instantiates 2 variables, @unit_price and @quantity. These are used in the the Order class to calculate sum. IN the class ShoppingCart, @entries is instantiated. this instance variable is also used in the Order class to also calculate the sum + +In B, CartEntry class instantiates @unit_price and @quantity which are used in a method within the class. This method uses these variables to return a price. In ShoppingCart class, @entries in instantiated and another price method is created, this method uses the price from the CartEntry class to calculate the sum of items in the cart. This price is then used in the Order class to calculate the total price of all items + + +What data does each class store? How (if at all) does this differ between the two implementations? +In A & B, CartEntry stores unit_price and quantity. + ShoppingCart stores entries + Order stores SALES_TAX, cart + + +What methods does each class have? How (if at all) does this differ between the two implementations? +IN A, the only class that has methods is Order. This calculates total_price. + +In B, CartEntry stores price method. + ShoppingCart stores another price method. + Order stores a total_price method like A + +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? +In A, it is retained in Order +In B, it is delegated to lower level classes + +Does total_price directly manipulate the instance variables of other classes? +In A, total_price does manipulate the instance variables of other classes +In B, total_price does not manipulate them + +If we decide items are cheaper if bought in bulk, how would this change the code? Which implementation is easier to modify? +In B, we could create another class that would have bulk prices that could return a bulk price that could be added to the order class. Or just add a bulk_price to CartEntry that would be added to the price method. +In A, a bulk_price could be added to CartEntry but then Order would also have to be changed to add it there too. + +Which implementation better adheres to the single responsibility principle? +B + +Bonus question once you've read Metz ch. 3: Which implementation is more loosely coupled? +B diff --git a/lib/block.rb b/lib/block.rb new file mode 100644 index 000000000..6448d0af0 --- /dev/null +++ b/lib/block.rb @@ -0,0 +1,23 @@ +require 'date' +require_relative "reservation" +require_relative "date_range" +require 'pry' + +module Hotel + class Block + + # As an administrator, I can create a block of rooms + # To create a block you need a date range, collection of rooms and a discounted room rate + # The collection of rooms should only include rooms that are available for the given date range + # If a room is set aside in a block, it is not available for reservation by the general public, nor can it be included in another block + + attr_reader :collection_of_rooms, :date_range, :cost_per_night + + def initialize(collection_of_rooms, cost_per_night, check_in, check_out) + + @collection_of_rooms = collection_of_rooms + @cost_per_night = cost_per_night + @date_range = Hotel::DateRange.new(check_in, check_out) + end + end +end diff --git a/lib/booking_system.rb b/lib/booking_system.rb new file mode 100644 index 000000000..ae3bf1e2b --- /dev/null +++ b/lib/booking_system.rb @@ -0,0 +1,84 @@ +require 'date' +require_relative "reservation" +require_relative "date_range" +require_relative "block" +require 'pry' + + +module Hotel + class BookingSystem + attr_reader :rooms, :reservations + + def initialize + @rooms = + [ + {room_number: 1}, {room_number: 2}, {room_number: 3}, {room_number: 4}, {room_number: 5}, {room_number: 6}, {room_number: 7}, {room_number: 8}, {room_number: 9}, {room_number: 10}, {room_number: 11}, {room_number: 12}, {room_number: 13}, {room_number: 14}, {room_number: 15}, {room_number: 16}, {room_number: 17}, {room_number: 18}, {room_number: 19}, {room_number: 20} + ] + + @reservations = [ ] + + end + + # method to make_reservation + # return if successful return res_id + def make_reservation(cost_per_night, check_in, check_out) + available_rooms = list_available_rooms(Hotel::DateRange.new(check_in, check_out)) + if available_rooms.empty? + raise StandardError, "There's no room in the inn" + end + room_number = available_rooms.sample[:room_number] + reservation = Hotel::Reservation.new(room_number, cost_per_night, check_in, check_out) + @reservations << reservation + return reservation + end + + #make block reservation + # error if not between 3-5 rooms + def make_block_reservation(number_of_rooms, cost_per_night, check_in, check_out) + available_rooms = list_available_rooms(Hotel::DateRange.new(check_in, check_out)) + if available_rooms.length < number_of_rooms + raise StandardError, "There's no room in the inn" + end + block_reservation = [] + number_of_rooms.times do + room_number = available_rooms.sample[:room_number] + reservation = Hotel::Reservation.new(room_number, cost_per_night, check_in, check_out) + @reservations << reservation + block_reservation << res + end + return block_reservation + end + + # list reservations for a specific date + def reservations_by_date(date) + date = Date.parse(date) + res_by_date = @reservations.select do |res| + res.date_range.included_in_date_range(date) + end + return res_by_date + end + + # list reservations for a specific date range + def reservations_by_date_range(date_range) + res_by_date_range = @reservations.select do |res| + res.date_range.overlaps?(date_range) + end + return res_by_date_range + end + + # As an administrator, I can view a list of rooms that are not reserved for a given date range + # reservation date range overlaps with + def list_available_rooms(date_range) + conflicting_reservations = reservations_by_date_range(date_range) + + available_rooms = @rooms.reject do |room| + conflicting_reservations.find do |res| + res.room_number == room[:room_number] + end + end + return available_rooms + end + + + end +end diff --git a/lib/date_range.rb b/lib/date_range.rb new file mode 100644 index 000000000..1151f820c --- /dev/null +++ b/lib/date_range.rb @@ -0,0 +1,38 @@ +require 'date' +require_relative '../lib/reservation' + +module Hotel + class DateRange + + attr_reader :check_in, :check_out + + def initialize(check_in, check_out) + + @check_in = Date.parse(check_in) + @check_out = Date.parse(check_out) + + unless @check_out > @check_in + raise StandardError.new("Check-out can not be before check-in. Chech-out is: #{@check_out} check-in is #{@check_in}") + end + + end + # -Find duration of stay + def duration_of_stay + duration_of_stay = (@check_out - @check_in).to_i + return duration_of_stay + end + + # include? method + def included_in_date_range(date) + return date.between?(check_in, check_out) + end + + # overlap? method + + def overlaps?(date_range) + overlap = @check_in < date_range.check_out && date_range.check_in < @check_out + return overlap + end + + end +end diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..350f3b02e --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,41 @@ +# require_relative '../lib/date_range' +require 'pry' + + +module Hotel + class Reservation + + attr_reader :room_number, :cost_per_night, :date_range, :reservation_id + + def initialize(room_number, cost_per_night, check_in, check_out) + @room_number = room_number + @cost_per_night = cost_per_night.to_f + @date_range = Hotel::DateRange.new(check_in, check_out) + @reservation_id = Reservation.generate_id + end + + # - Generate an reservation ID + def self.generate_id + rand_array = [] + + 3.times do + rand_num = rand(1..9).to_s + rand_array << rand_num + end + + 3.times do + rand_letter = ('A'..'Z').to_a.sample.to_s + rand_array << rand_letter + end + + rand_array.insert(3, "-") + return rand_array * "".to_s + end + # - Find total cost + def total_cost + total_cost = @cost_per_night * @date_range.duration_of_stay + return total_cost + end + + end +end diff --git a/refactors.txt b/refactors.txt new file mode 100644 index 000000000..fb8da5258 --- /dev/null +++ b/refactors.txt @@ -0,0 +1,13 @@ +- Finish the blocks class. +-- The collection of rooms should only include rooms that are available for the given date range +-- If a room is set aside in a block, it is not available for reservation by the general public, nor can it be included in another block + +- Some names could be changed/condensed: +-- list_available_rooms +-- reservations_by_date_range +-- blocks.rb change to room_block? + + +- Tests could be better organized: +-- in date_range, most could fit under "date range" +-- Tests could have more edge chases - If no date is passed in, diff --git a/spec/block_spec.rb b/spec/block_spec.rb new file mode 100644 index 000000000..ea08dcb6e --- /dev/null +++ b/spec/block_spec.rb @@ -0,0 +1,38 @@ +require 'minitest' +require 'minitest/autorun' +require 'minitest/reporters' + +require_relative 'spec_helper' +require_relative '../lib/booking_system.rb' +require_relative '../lib/reservation.rb' +require_relative '../lib/date_range.rb' + +describe "initialize" do + before do + @new_block_res = Hotel::Block.new([{room_number: 1}, {room_number: 2}, {room_number: 3}, {room_number: 4}], 200.00, "2018-02-03", "2018-02-06") + end + + it "is an instance of Blocks" do + expect(@new_block_res).must_be_kind_of Hotel::Block + end + + it "Takes date_range, collection_of_rooms and cost per night" do + + expect(@new_block_res).must_respond_to :date_range + expect(@new_block_res).must_respond_to :collection_of_rooms + expect(@new_block_res).must_respond_to :cost_per_night + + end + + it "is set up for specific attributes and data types" do + [:date_range, :collection_of_rooms, :cost_per_night].each do |initial| + expect(@new_block_res).must_respond_to initial + end + + expect(@new_block_res.date_range).must_be_kind_of Hotel::DateRange + expect(@new_block_res.collection_of_rooms).must_be_kind_of Array + expect(@new_block_res.cost_per_night).must_be_kind_of Float + + + end +end diff --git a/spec/booking_system_spec.rb b/spec/booking_system_spec.rb new file mode 100644 index 000000000..7ec63041a --- /dev/null +++ b/spec/booking_system_spec.rb @@ -0,0 +1,95 @@ +require_relative 'spec_helper' + +describe "BookingSystem class" do + + describe "initialize" do + before do + @booking = Hotel::BookingSystem.new() + end + + it "is an instance of Reservation" do + expect(@booking).must_be_kind_of Hotel::BookingSystem + end + + it "Takes check_in, check_out, cost, and reservation_id" do + expect(@booking).must_respond_to :rooms + expect(@booking).must_respond_to :reservations + # expect(@booking).must_respond_to :availibility + end + + it "is set up for specific attributes and data types" do + [:rooms, :reservations].each do |initial| + expect(@booking).must_respond_to initial + end + expect(@booking.rooms).must_be_kind_of Array + expect(@booking.reservations).must_be_kind_of Array + # expect(@booking_system.availibility).must_be_kind_of String + end + end + + describe "rooms" do + before do + @booking = Hotel::BookingSystem.new() + end + it "returns an array all of the rooms in the hotel" do + expect(@booking.rooms.length).must_equal 20 + # puts @booking.rooms + end + + it "returns a list of available rooms for a given date range"do + + expect(@booking.rooms.length).must_equal 20 + end + + end + + describe "reservations" do + before do + @booking = Hotel::BookingSystem.new() + end + it "takes check-in and check-out, matches to an availible room, ID and pushes it into reservations array" do + + @booking.make_reservation(200, "2018-02-03", "2018-02-06") + # puts @booking.reservations.first.reservation_id + puts @booking.reservations.first.total_cost + puts @booking.reservations + expect(@booking.reservations.length).must_equal 1 + end + # it "Makes a block reservation - takes number of rooms, cost_per_night, check-in, check_out " + + + it "raises an error if no rooms are available" do + 20.times do + @booking.make_reservation(200, "2018-02-03", "2018-02-06") + end + expect { @booking.make_reservation(200, "2018-02-03", "2018-02-06") }.must_raise StandardError + + end + + + it "lists reservations for a specific date" do + res1 = @booking.make_reservation(200, "2018-02-03", "2018-02-06") + res2 = @booking.make_reservation(200, "2018-04-03", "2018-04-06") + puts res1.total_cost + check_day = @booking.reservations_by_date("2018-02-05") + expect(check_day.length).must_equal 1 + # binding.pry + expect(check_day.first).must_equal res1 + end + it "lists reservations for a specific date range" do + res1 = @booking.make_reservation(200, "2018-02-03", "2018-02-06") + res2 = @booking.make_reservation(200, "2018-02-06", "2018-04-10") + puts res1.total_cost + check_day = @booking.reservations_by_date_range(Hotel::DateRange.new("2018-02-05","2018-02-10")) + expect(check_day.length).must_equal 2 + # binding.pry + expect(check_day.first).must_equal res1 + end + + it "lists rooms that are available for a date range" do + res1 = @booking.make_reservation(200, "2018-04-03", "2018-04-06") + + expect(@booking.list_available_rooms(Hotel::DateRange.new("2018-04-03", "2018-04-06")).length).must_equal 19 + end + end +end diff --git a/spec/date_range_spec.rb b/spec/date_range_spec.rb new file mode 100644 index 000000000..bfb786e8b --- /dev/null +++ b/spec/date_range_spec.rb @@ -0,0 +1,120 @@ +require 'minitest' +require 'minitest/autorun' +require 'minitest/reporters' + +require_relative 'spec_helper' +require_relative '../lib/booking_system' +require_relative '../lib/reservation' + + + +describe "initialize" do + before do + @dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + end + + it "is an instance of Reservation" do + expect(@dates).must_be_kind_of Hotel::DateRange + end + + it "Takes check_in, check_out" do + expect(@dates).must_respond_to :check_in + expect(@dates).must_respond_to :check_out + end + + it "raises a StandardError if check-out is before check-in " do + + expect { Hotel::DateRange.new("2018-02-08", "2018-02-06") }.must_raise StandardError + end + + it "is set up for specific attributes and data types" do + [:check_in, :check_out].each do |initial| + expect(@dates).must_respond_to initial + end + expect(@dates.check_in).must_be_kind_of Date + expect(@dates.check_out).must_be_kind_of Date + + end + + describe "date range" do + + it "finds the duration of stay" do + dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + expect(dates.duration_of_stay).must_equal 3 + end + + it "finds if date ranges overlap" do + dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + dates2 = Hotel::DateRange.new("2018-04-03", "2018-04-06") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal false + end + it "finds if ranges overlap if same dates" do + dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + dates2 = Hotel::DateRange.new("2018-02-03", "2018-02-06") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal true + end + it "finds if ranges overlap in the front" do + dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + dates2 = Hotel::DateRange.new("2018-02-03", "2018-02-04") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal true + end + it "finds if ranges overlap in the back" do + dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + dates2 = Hotel::DateRange.new("2018-02-05", "2018-02-07") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal true + end + it "finds if ranges are completely contain" do + dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + dates2 = Hotel::DateRange.new("2018-02-04", "2018-02-05") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal true + end + it "finds if ranges are completely containing" do + dates = Hotel::DateRange.new("2018-02-05", "2018-02-06") + dates2 = Hotel::DateRange.new("2018-02-04", "2018-02-07") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal true + end + it "finds if ranges are not overlpping if completely after eachother" do + dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + dates2 = Hotel::DateRange.new("2018-02-07", "2018-02-11") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal false + end + it "finds if ranges are not overlapping if completely after eachother" do + dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + dates2 = Hotel::DateRange.new("2018-02-01", "2018-02-02") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal false + end + it "finds if ranges are not overlapping if one range ends on checkin date " do + dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + dates2 = Hotel::DateRange.new("2018-02-06", "2018-02-08") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal false + end + it "finds if ranges are not overlapping if one range starts on checkout date " do + dates = Hotel::DateRange.new("2018-02-08", "2018-02-10") + dates2 = Hotel::DateRange.new("2018-02-06", "2018-02-08") + overlaps = dates2.overlaps?(dates) + expect(overlaps).must_equal false + end + + + describe "find if dates are included" do + before do + @dates = Hotel::DateRange.new("2018-02-03", "2018-02-06") + end + it "finds if date is included in date range" do + date_to_find = Date.parse("2018-02-06") + reservations_by_date = @dates.included_in_date_range(date_to_find) + expect(reservations_by_date).must_equal true + end + end + end + +end diff --git a/spec/reservation_spec.rb b/spec/reservation_spec.rb new file mode 100644 index 000000000..ebf6e4e47 --- /dev/null +++ b/spec/reservation_spec.rb @@ -0,0 +1,52 @@ +require 'minitest' +require 'minitest/autorun' +require 'minitest/reporters' + +require_relative 'spec_helper' +require_relative '../lib/booking_system.rb' +require_relative '../lib/reservation.rb' + + +describe "initialize" do + before do + @new_res = Hotel::Reservation.new(1, 200.00, "2018-02-03", "2018-02-06") + end + + it "is an instance of Reservation" do + expect(@new_res).must_be_kind_of Hotel::Reservation + end + + it "Takes cost, and reservation_id" do + + expect(@new_res).must_respond_to :cost_per_night + expect(@new_res).must_respond_to :reservation_id + + end + + it "is set up for specific attributes and data types" do + [:cost_per_night, :reservation_id].each do |initial| + expect(@new_res).must_respond_to initial + end + + expect(@new_res.reservation_id).must_be_kind_of String + expect(@new_res.cost_per_night).must_be_kind_of Float + end +end + +describe "make a reservation" do + before do + @new_res = Hotel::Reservation.new(1, 200.00, "2018-02-03", "2018-02-06") + end + it "generates a reservation ID" do + expect(@new_res.reservation_id.length).must_equal 7 + end + + + it "finds the total cost of reservation" do + expect(@new_res.total_cost).must_equal 600.00 + end + + # it "finds the total cost of each room in a block" do + # expect(@new_res.discounted_rate).must_equal 480 + # end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4d1e3fdc8..487218b67 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,13 @@ +require 'simplecov' +SimpleCov.start + require 'minitest' require 'minitest/autorun' require 'minitest/reporters' -# Add simplecov + Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new -# Require_relative your lib files here! +require_relative '../lib/booking_system.rb' +require_relative '../lib/reservation.rb' +require_relative '../lib/date_range.rb'