diff --git a/.gitignore b/.gitignore index 5e1422c9c..703a023e5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ /test/tmp/ /test/version_tmp/ /tmp/ +.DS_Store +/lib/junk_files/ # Used by dotenv library to load environment variables. # .env diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..deb52f2cd --- /dev/null +++ b/Rakefile @@ -0,0 +1,9 @@ +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.libs = ["lib"] + t.warning = true + t.test_files = FileList['specs/*_spec.rb'] +end + +task default: :test diff --git a/design-activity.md b/design-activity.md new file mode 100644 index 000000000..dec35eb88 --- /dev/null +++ b/design-activity.md @@ -0,0 +1,56 @@ +> What classes does each implementation include? Are the lists the same? + +CartEntry, ShoppingCart & Order - yes, the lists are the same. + +> Write down a sentence to describe each class. + +* CartEntry stores information about individual items, like price and quantity. + +* ShoppingCart holds a list of CartEntries. + +* Order applies sales tax to the subtotal derived from ShoppingCart. + +> How do the classes relate to each other? *It might be helpful to draw a diagram on a whiteboard or piece of paper*. + +CartEntry is an element in ShoppingCart (though that logic is invisible) - and Order takes information from ShoppingCart and adds the sales tax. + +> What data does each class store? How (if at all) does this differ between the two implementations? + +* In the first implementation CartEntry exists to hold the instance variables for @unit_price and @quanitity, in the second implementation it has a method for price that returns the @unit_price multiplied by the @quantity. + +* In the first implementation ShoppingCart holds the instance variable @entries which is assigned to an empty array, in the second implementation ShoppingCart has a method for price that returns the sum of elements in the @entries array. + +* In implementation A, Order holds a constant for SALES_TAX, an instance of ShoppingCart and a method for total price that adds the unit_price * quantity for each entry in the ShoppingCart @entries array, but in the second implementation the logic is much simpler for the total price method. + +> What methods does each class have? How (if at all) does this differ between the two implementations? + +See previous answer. + +> 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 Implementation A, it is retained in Order in Implementation B it is delegated to "lower level" classes. + +> Does total_price directly manipulate the instance variables of other classes? + +It doesn't change the values of other variables, but it uses them to calculate a new value. + +> If we decide items are cheaper if bought in bulk, how would this change the code? Which implementation is easier to modify? + +It would probably affect the price for CartEntry. The change for Implementation B requires less complicated logic because it's not as far removed from the source of the change. + +> Which implementation better adheres to the single responsibility principle? + +Implementation B seems to adhere to this better. + +> **Bonus question once you've read Metz ch. 3: Which implementation is more loosely coupled?** + +Implementation B is more loosely coupled, because it knows less information about other classes. In B, total_price only knows its own variable @cart has a price, in A, total_price knows that each entry has a unit_price and a quantity, if those values are changed, then it will no longer work. Whereas with A, if they were changed, as long as price remained as method that could be called there would be no difference for the program as a whole. + +## Activity +> Based on the answers to the above questions, identify one place in your Hotel project where a class takes on multiple roles, or directly modifies the attributes of another class. + +> Describe in design-activity.md what changes you would need to make to improve this design, and how the resulting design would be an improvement. + +My BookingManager class takes on multiple responsibilities when it should delegate them. I need to change all of the places where I chain to an instance method so that those classes handle the logic themselves. diff --git a/lib/block.rb b/lib/block.rb new file mode 100644 index 000000000..c3419b82b --- /dev/null +++ b/lib/block.rb @@ -0,0 +1,31 @@ +require 'awesome_print' +require 'pry' +require 'date' +require 'simplecov' + +module Hotel + + class Block + @@block_count = 0 + + attr_reader :id, :rooms, :period + def initialize(date_range, available_rooms) + @id = @@block_count += 1 + @rooms = reserve_block(date_range, available_rooms) + @period = date_range + end + + def reserve_block(date_range, open_rooms, price: 150) + reserved = Hash[date_range.collect { |date| [date, :BLOCK]}] + rooms = [] + open_rooms.each do |open_room| + open_room.check_block_dates(date_range) + open_room.block_dates.merge!(reserved) + open_room.block_price(price) + rooms << open_room + end + return rooms + end + + end +end diff --git a/lib/booking.rb b/lib/booking.rb new file mode 100644 index 000000000..16e05fcb1 --- /dev/null +++ b/lib/booking.rb @@ -0,0 +1,32 @@ +require 'awesome_print' +require 'pry' +require 'date' +require 'simplecov' + +module Hotel + + + class Booking + @@id_count = 0 + attr_reader :cost_estimate, :room, :period, :id, :id_count + def initialize(open_room, date_range) + @id = @@id_count += 1 + @room = reserve_room(open_room, date_range) + @period = date_range + @cost_estimate = get_cost_estimate(date_range) + end + + def get_cost_estimate(period) + length = period.size + subtotal = room.cost_per_night * length + return subtotal + end + + def reserve_room(open_room, date_range) + reserved = Hash[date_range.collect { |date| [date, :RESERVED]}] + open_room.reserved_dates.merge!(reserved) + return open_room + end + + end +end diff --git a/lib/booking_manager.rb b/lib/booking_manager.rb new file mode 100644 index 000000000..591d5e07a --- /dev/null +++ b/lib/booking_manager.rb @@ -0,0 +1,108 @@ +require 'awesome_print' +require 'pry' +require 'time' + +require_relative 'booking' +require_relative 'room' +require_relative 'block' +require_relative 'duration' + +module Hotel + class BookingManager + # should handle the business logic for bookings + attr_reader :rooms, :reservations, :occupied_rooms + def initialize + @rooms = load_rooms + @reservations = [] + @occupied_rooms = [] + @block_reservations = [] + @available_rooms = nil + end + + def load_rooms + rooms = [] + numbers = (1..20).to_a + numbers.each do |num| + rooms << Room.new(num) + end + return rooms + end + + def set_booking(start_date, end_date) + dates = Duration.new(start_date, end_date) + date_range = dates.period + + if @reservations.length == 0 + open_room = rooms.first + else + available_rooms = all_available(date_range) + open_room = available_rooms.pop + end + + @reservations << Booking.new(open_room, date_range) + end + + def reserve_block(start_date, end_date, number_of_rooms: 5) + dates = Duration.new(start_date, end_date) + date_range = dates.period + + open_rooms = rooms.take(number_of_rooms) + block = Block.new(date_range, open_rooms) + + @block_reservations << block + return block + end + + + + def all_available(date_range) #actively using this + available_rooms = [] + date_range.each do |date| + rooms_check = get_availability_by_date(date) + + if available_rooms.length == 0 + available_rooms = rooms_check + else + temp_rooms = available_rooms & rooms_check + available_rooms = temp_rooms + end + end + + available_rooms.each do |room| + room.check_reserved_dates(date_range) + end + + return available_rooms + end + + def get_bookings_by_date(date) + bookings = @reservations.find_all do |booking| + if booking.period.include? date + @occupied_rooms << booking + end + end + @available_rooms = @rooms - @occupied_rooms + + return bookings + end + + def get_booking_by_id(id_to_find) + find_booking = @reservations.find { |booking| booking.id == id_to_find} + return find_booking + end + + def get_availability_by_date(date) + get_bookings_by_date(date) + open_rooms = [] + + @available_rooms.each do |room| + unless room.free?(date) + open_rooms << room + end + end + + return open_rooms + end + + end +end diff --git a/lib/duration.rb b/lib/duration.rb new file mode 100644 index 000000000..c0815a307 --- /dev/null +++ b/lib/duration.rb @@ -0,0 +1,36 @@ +require 'awesome_print' +require 'pry' +require 'date' +require 'simplecov' + +module Hotel + class Duration + attr_reader :period + def initialize(start_date, end_date) + @date_range = date_range(start_date, end_date) + @period = list + end + + def date_range(start_date, end_date) + new_range = (check_date(start_date)...check_date(end_date)) + return new_range + end + + def list + dates = [] + @date_range.each {|date| dates << date } + #add in call to check availability of the room. + return dates + end + + + private + def check_date(date) + unless date.class == Date + parsed_date = Date.parse(date) + date = parsed_date + end + return date + end + end +end diff --git a/lib/room.rb b/lib/room.rb new file mode 100644 index 000000000..9d2fbdaed --- /dev/null +++ b/lib/room.rb @@ -0,0 +1,51 @@ +require 'awesome_print' +require 'pry' +require 'date' +require 'simplecov' + +module Hotel + class Room + attr_accessor :reserved_dates, :block_dates + attr_reader :room_number, :cost_per_night + def initialize(number) + + # rooms that know their room number and cost + # make more sense as objects than a hash, for future + # proofing reasons, maybe overcomplicating things + + @room_number = number + @cost_per_night = 200 + @block_dates = {} + @reserved_dates = {} + + end + + def block_price(price) + @cost_per_night = price + end + + def check_block_dates(date_range) + date_range.each do |date| + if @block_dates.has_key?(date) + raise StandardError.new("Room is reserved to a block on #{date}") + end + end + end + + def check_reserved_dates(date_range) + date_range.each do |date| + if @reserved_dates.has_key?(date) + raise StandardError.new("Room is already reserved on #{date}") + end + end + end + + def free?(date) + if @block_dates.has_key?(date) || @reserved_dates.has_key?(date) + return true + end + return false + end + + end +end diff --git a/specs/block_spec.rb b/specs/block_spec.rb new file mode 100644 index 000000000..9bc13ccc8 --- /dev/null +++ b/specs/block_spec.rb @@ -0,0 +1,44 @@ +require_relative 'spec_helper' + +describe "Block" do +before do + @booking_manager = Hotel::BookingManager.new + @block1 = @booking_manager.reserve_block("apr4, 2018", "apr7, 2018") +end +describe "Block#initialize" do +it "creates a block with an id, rooms and a date_range" do + # test that it creates a user + result = @block1 + + result.must_be_instance_of Hotel::Block + + result.id.must_be_kind_of Integer + result.rooms.must_be_kind_of Array + result.period.length.wont_be_nil +end + +it "won't accept a block with rooms from another block" do + proc{ + @booking_manager.reserve_block("apr4, 2018", "apr7, 2018") + }.must_raise StandardError +end + + +end + +it "can do things" do + # create as many tests as are needed to make sure + # that the user can actually navigate the program +end + +# find {n} rooms that are near eachother and available for those dates +# requirements: +#=> date range, no more than 5 rooms at a discounted rate +#=> only include rooms available for the given date range +#=> a room in a block is not available or included in another block + +#needs: +#=> date_range +#=> availablity_checking +#=> uniqueness +end diff --git a/specs/booking_manager_spec.rb b/specs/booking_manager_spec.rb new file mode 100644 index 000000000..e3a0b3493 --- /dev/null +++ b/specs/booking_manager_spec.rb @@ -0,0 +1,221 @@ +require_relative 'spec_helper' + +describe "BookingManager" do + before do + date1 = Date.today + date2 = date1 + 3 + + @date = date2 + @result = Hotel::BookingManager.new + @booking1 = @result.set_booking(date1.to_s, date2.to_s) + @booking2 = @result.set_booking((date1 + 1).to_s, date2.to_s) + end + describe "Initialize" do + it "can be created" do + @result.must_be_instance_of Hotel::BookingManager + end + + it "creates a list of rooms" do + result = @result.rooms + + result.must_be_kind_of Array + result.length.must_equal 20 + result.last.room_number.must_equal 20 + + end + end + + describe "BookingManager - Business Logic" do + describe "Reservation Handling" do + it "has a list of reservations" do + + result = @result.reservations + result.must_be_kind_of Array + result.length.must_equal 2 + + end + + it "can find a list of reservations by date" do + date = Date.today + 2 + result = @result.get_bookings_by_date(date) + + result.must_be_kind_of Array + result[0].must_be_instance_of Hotel::Booking + result[0].period.must_include date + + end + + it "can find a reservation by id" do + id_to_find = @result.reservations[0].id + result = @result.get_booking_by_id(id_to_find) + + result.must_be_instance_of Hotel::Booking + result.id.must_equal id_to_find + end + + it "can handle :AVAILABLE/:UNAVAILABLE reservation logic" do + proc{ 20.times do + @result.set_booking(date1.to_s, date2.to_s) + end}.must_raise StandardError + end + + it "can get a list of available rooms by date" do + result = @result.get_availability_by_date(@date) + result.must_be_kind_of Array + #--- + + result = @result.get_availability_by_date(@date).first + result2 = result.reserved_dates.include?(@date) + + result.must_be_instance_of Hotel::Room + result2.must_equal false + + end + end + + describe "Date Handling" do + + it "raises an error if an invalid date is used" do + #this test doesn't seem to pass for the right reasons, investigate further + today = Date.today + start_date = "frog" + proc{ @result.reserve_room(start_date, today)}.must_raise StandardError + end + + it "raises an error if the first day of the reservation is before the current day" do + #this test doesn't seem to pass for the right reasons, investigate further + today = Date.today + start_date = today << 1 + proc{ @result.reserve_room(start_date, today)}.must_raise StandardError + end + + end + + + end +end + +describe "Instructor Suggested Test Cases" do + describe "Instructor Test Cases (without before block)" do + it "handles no reservations" do + bookingmanager1 = Hotel::BookingManager.new + + result = bookingmanager1.reservations + + result.must_be_kind_of Array + result.length.must_equal 0 + end + end + + describe "Instructor Test Cases (with before block)" do + before do + @date1 = Date.today + @date2 = @date1 + 3 + + @result = Hotel::BookingManager.new + end + + it "allows reservations that do not overlap on dates" do + 20.times do + booking1 = @result.set_booking(@date1.to_s, @date2.to_s) + end + + 20.times do + booking2 = @result.set_booking((@date2 + 3).to_s, (@date2 + 7).to_s) + end + + result = @result.reservations + + result.length.must_equal 40 + result.first.must_be_kind_of Hotel::Booking + result.last.must_be_kind_of Hotel::Booking + end + + it "handles dates that overlap in the front" do + 20.times do + booking1 = @result.set_booking(@date1.to_s, @date2.to_s) + end + + proc { + 20.times do + booking2 = @result.set_booking((@date1 - 1).to_s, (@date2 - 1).to_s) + end + }.must_raise StandardError + + result = @result.reservations + + result.length.must_equal 20 + result.first.must_be_kind_of Hotel::Booking + result.last.must_be_kind_of Hotel::Booking + + end + + it "handles dates that overlap in the back" do + 20.times do + booking1 = @result.set_booking(@date1.to_s, @date2.to_s) + end + + proc { + 20.times do + booking2 = @result.set_booking((@date2 - 2).to_s, (@date2 + 4).to_s) + end + }.must_raise StandardError + + result = @result.reservations + + result.length.must_equal 20 + result.first.must_be_kind_of Hotel::Booking + result.last.must_be_kind_of Hotel::Booking + end + + it "handles dates that completely contain others" do + 20.times do + booking1 = @result.set_booking(@date1.to_s, @date2.to_s) + end + + proc { + 20.times do + booking2 = @result.set_booking((@date1 + 1).to_s, (@date2 - 1).to_s) + end + }.must_raise StandardError + + result = @result.reservations + + result.length.must_equal 20 + result.first.must_be_kind_of Hotel::Booking + result.last.must_be_kind_of Hotel::Booking + end + + it "allows dates that end on the checkin date" do + 20.times do + booking1 = @result.set_booking(@date1.to_s, @date2.to_s) + end + + 20.times do + booking2 = @result.set_booking((@date1 - 3).to_s, @date1.to_s) + end + + result = @result.reservations + + result.length.must_equal 40 + result.first.must_be_kind_of Hotel::Booking + result.last.must_be_kind_of Hotel::Booking + end + + it "allows reservations that start on the checkout date" do + 20.times do + booking1 = @result.set_booking(@date1.to_s, @date2.to_s) + end + + 20.times do + booking2 = @result.set_booking(@date2.to_s, (@date2 + 2).to_s) + end + + result = @result.reservations + + result.length.must_equal 40 + result.first.must_be_kind_of Hotel::Booking + result.last.must_be_kind_of Hotel::Booking + end + end +end diff --git a/specs/booking_spec.rb b/specs/booking_spec.rb new file mode 100644 index 000000000..5aa0caba9 --- /dev/null +++ b/specs/booking_spec.rb @@ -0,0 +1,53 @@ +require_relative 'spec_helper' + +describe "Booking" do + before do + @date1 = Date.today + @date2 = @date1 + 4 + + @manager = Hotel::BookingManager.new + @booking = @manager.set_booking(@date1.to_s, @date2.to_s).first + end + describe "Booking#initialize" do + it "can be created" do + result = @booking + result.must_be_kind_of Hotel::Booking + end + end + + describe "Booking#cost_estimate" do + it "creates a cost estimate" do + result = @booking.cost_estimate + days = @booking.period.length + 1 + nights = @booking.period.length + + result.must_equal nights * 200 + result.wont_equal days * 200 + end + + describe "Booking#reserve_room" do + it "reserves a room" do + + end + + it "raises an error if there are no available rooms" do + proc{ + 30.times do + @manager.set_booking(@date1.to_s, @date2.to_s) + end + }.must_raise StandardError + end + + it "won't accept a reservation for a room that is reserved" do + proc{ + 30.times do + @booking_manager.reserve_room("apr4, 2018", "apr7, 2018") + end + }.must_raise StandardError + end + end + end + + + +end diff --git a/specs/duration_spec.rb b/specs/duration_spec.rb new file mode 100644 index 000000000..235017a2d --- /dev/null +++ b/specs/duration_spec.rb @@ -0,0 +1,14 @@ +require_relative 'spec_helper' + +describe "Stay" do +it "can be created" do + # test that it creates a user + result = Hotel::Duration.new("apr4, 2018", "apr7, 2018") + result.must_be_instance_of Hotel::Duration +end + +it "can do things" do + # create as many tests as are needed to make sure + # that the user can actually navigate the program +end +end diff --git a/specs/room_spec.rb b/specs/room_spec.rb new file mode 100644 index 000000000..69e3caa91 --- /dev/null +++ b/specs/room_spec.rb @@ -0,0 +1,58 @@ +require_relative 'spec_helper' + +describe "Room" do + describe "Room#Initialize" do + it "can be created" do + result = Hotel::Room.new(1) + result.must_be_instance_of Hotel::Room + end + + it "knows its number" do + numbers = (1..20).to_a + numbers.each do |num| + result = Hotel::Room.new(num) + + + result.room_number.must_equal num + + end + end + + it "knows its cost per night" do + result = Hotel::Room.new(1) + result.cost_per_night.must_equal 200 + end + end + + describe "Room#change_status" do + before do + @rooms = [] + numbers = (1..20).to_a + numbers.each do |num| + rooms << Room.new(num) + end + + it "does not accept an invalid status" do + room = @rooms[0] + proc{ room.change_status(new_status: :UNAVAILABLE)}.must_raise StandardError + end + + it "defaults to a status of :RESERVED with no argument" do + room = @rooms[0] + result = room.change_status + + result.must_equal :RESERVED + end + + it "Can change a :RESERVED status to :AVAILABLE" do + room = @rooms[0] + room.change_status + result = room.change_status(new_status: :AVAILABLE) + + result.must_equal :AVAILABLE + end + + + end + end +end diff --git a/specs/spec_helper.rb b/specs/spec_helper.rb new file mode 100644 index 000000000..3d00bdad6 --- /dev/null +++ b/specs/spec_helper.rb @@ -0,0 +1,20 @@ +require 'simplecov' +SimpleCov.start do + add_filter "/specs/" +end + +require 'time' +require 'minitest' +require 'minitest/autorun' +require 'minitest/reporters' +require 'minitest/skip_dsl' +require 'minitest/pride' + +Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new + +# Require_relative your lib files here! +require_relative '../lib/booking' +require_relative '../lib/booking_manager' +require_relative '../lib/room' +require_relative '../lib/block' +require_relative '../lib/duration'