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/lib/booking.rb b/lib/booking.rb new file mode 100644 index 000000000..8e92beaaa --- /dev/null +++ b/lib/booking.rb @@ -0,0 +1,78 @@ +require 'set' +require_relative 'validate' +require_relative 'reservation' +require_relative 'room' + +module Hotel + class Booking < Validate + + NUM_OF_ROOMS = 20 + + attr_reader :rooms, :reservations + + def initialize + @rooms = [] + @reservations = [] + @blocks = [] + create_rooms + end # initialize + + def create_rooms + NUM_OF_ROOMS.times do |i| + data = {number: i+1} + @rooms << Room.new(data) + end + end # create_rooms + + def room_by_num(num) + @rooms.find do |room| + room.number == num + end + end + + + def rooms_by_day(date, block: nil) + date = validate_date(date) + @rooms.reject do |room| + room.reserved?(date) && room.blocked?(date) + end + end + + def rooms_by_range(start_date, end_date, block: nil) + start_date = validate_date(start_date) + end_date = validate_date(end_date) + date_set = Set.new(start_date...end_date) + + avail_array = @rooms.reject do |room| + date_set.intersect? room.reserved.to_set + end + + if block + avail_array = avail_array.reject do |room| + date_set.intersect? room.booked[block].to_set + end + end + + return avail_array + end + + + def reserve(data) + start_date = validate_date(data[:start_date]) + end_date = validate_date(data[:end_date]) + available_rooms = rooms_by_range(start_date, end_date, block: data[:block]) + if available_rooms.length == 0 + raise ArgumentError.new("Sorry, we do not have a room available at this time") + else + room = available_rooms.sample + end + + reservation = Reservation.new(start_date: start_date, end_date: end_date, room: room) + @reservations << reservation + room.reserve(start_date, end_date, block: data[:block]) + return reservation + end + + + end # Class Booking +end # Module Hotel diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..584db1176 --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,39 @@ + +require_relative 'validate' +require_relative 'room' + +module Hotel + class Reservation < Validate + + BLOCK_DISCOUNT = 0.20 + + attr_reader :start_date, :end_date, :room + + def initialize(data) + if data[:room].class != Room + raise ArgumentError.new("Must enter room to be reserved") + end + @room = data[:room] + + @start_date = validate_date(data[:start_date]) + @end_date = validate_date(data[:end_date]) + validate_stay(@start_date, @end_date) + + # OPTIONAL + @block = data[:block] # NIL if not provided + end + + def total_cost + cost = stay_length * @room.price + if @block + cost *= BLOCK_DISCOUNT + end + return cost + end + + def stay_length + @end_date - @start_date + end + + end # Class Reservation +end # Module Hotel diff --git a/lib/room.rb b/lib/room.rb new file mode 100644 index 000000000..c4e72cb85 --- /dev/null +++ b/lib/room.rb @@ -0,0 +1,60 @@ + +require_relative 'validate' +require 'date' + +module Hotel + class Room < Validate + + attr_reader :number, :reserved, :blocked + attr_accessor :price + + def initialize(data) + if data[:number] == nil || data[:number].class != Integer || data[:number] < 1 + raise ArgumentError.new("Rooms must have a positive integer for room number. Received #{number}") + end + @number = data[:number] + @price = data[:price].to_f ||= 200.00 + @reserved = [] + @blocked = {} + end + + def reserved?(date) + date = validate_date(date) + if @reserved.include?(date) + return true + else + return false + end + end + + def reserve(start_date, end_date, block: nil) + start_date = validate_date(start_date) + end_date = validate_date(end_date) + validate_stay(start_date, end_date) + stay = end_date - start_date + + stay.to_i.times do |index| + @reserved << start_date + index + end + end + + def blocked?(date) + date = validate_date(date) + existing_blocks = @blocked.find do |name, dates| + dates.include?(date) + end + return existing_blocks.keys + end + + def block(date, name) + date = validate_date(date) + if @blocked[name] + @blocked[name] << date + else + @blocked[name] = [date] + end + end + + + end # Class Room +end # Module Hotel diff --git a/lib/validate.rb b/lib/validate.rb new file mode 100644 index 000000000..3f2792eae --- /dev/null +++ b/lib/validate.rb @@ -0,0 +1,35 @@ +require 'date' +module Hotel + class Validate + + OLDEST_RES_DATE = Date.new(1900, 01, 01) + NEWEST_RES_DATE = Date.new(3999, 12, 31) + + def validate_date(entry) + if entry.class == String + entry = Date.strptime(entry, '%m/%d/%Y') + if entry.class != Date || entry < OLDEST_RES_DATE || entry > NEWEST_RES_DATE + raise ArgumentError.new("Invalid date. Please enter MM/DD/YYYY Received #{entry}") + end + elsif entry.class == Date + if entry < OLDEST_RES_DATE || entry > NEWEST_RES_DATE + raise ArgumentError.new("Date: Invalid date. Received #{entry}") + end + else + raise ArgumentError.new("Other: Invalid date. Received #{entry}") + end + return entry + end + + def validate_stay(beginning, ending) + beginning = validate_date(beginning) + ending = validate_date(ending) + if beginning > ending + raise ArgumentError.new("Start date cannot be after end date") + end + return (ending - beginning).to_i + end + + end # class loader + +end # module Hotel diff --git a/specs/booking_spec.rb b/specs/booking_spec.rb new file mode 100644 index 000000000..60ccdc9eb --- /dev/null +++ b/specs/booking_spec.rb @@ -0,0 +1,111 @@ +require_relative 'spec_helper' + +describe "Booking Class" do + + describe "#initialize" do + + it "can be created" do + result = Hotel::Booking.new + result.must_be_kind_of Hotel::Booking + end + + it "has a rooms array" do + booking = Hotel::Booking.new + booking.rooms.must_be_kind_of Array + end + + it "has collection of 20 rooms" do + booking = Hotel::Booking.new + booking.rooms.length.must_equal 20 + booking.rooms.each do |room| + room.must_be_kind_of Hotel::Room + end + end + + end # initialize + + describe "#room_by_num" do + + before do + @booking = Hotel::Booking.new + end + + it "returns an instance of room" do + result = @booking.room_by_num(12) + result.must_be_kind_of Hotel::Room + end + + it "returns nil if room does not exist" do + result = @booking.room_by_num(67) + result.must_be_nil + end + + it "returns nil if invalid argument given" do + result = @booking.room_by_num("Some String") + result.must_be_nil + end + + end # room_by_num + + describe "#reserve" do + + it "returns a reservation with start date and end date." do + book = Hotel::Booking.new + start = Date.new(2018, 04, 02) + + result = book.reserve(start_date: start, end_date: start + 3) + result.must_be_kind_of Hotel::Reservation + end + + it "updates the reservations array" do + book = Hotel::Booking.new + before = book.reservations.length + book.reserve(start_date: "05/02/2018", end_date: "05/08/2018") + after = book.reservations.length + after.must_equal before + 1 + book.reservations[0].must_be_kind_of Hotel::Reservation + end + + it "raises an error if no available rooms" do + book = Hotel::Booking.new + 20.times do + book.reserve(start_date: "06/01/2018", end_date: "06/03/2018") + end + proc { + book.reserve(start_date: "06/01/2018", end_date: "06/03/2018") + }.must_raise ArgumentError + end + + it "raises an error if incorrect date information is given" do + book = Hotel::Booking.new + proc { + book.reserve(start_date: "August 29th", end_date: "09/30/2018") + }.must_raise ArgumentError + end + + it "raises an error if no start date or end date is given" do + book = Hotel::Booking.new + proc { + book.reserve(end_date: "09/30/2018") + }.must_raise ArgumentError + + proc { + book.reserve(start_date: "09/30/2018") + }.must_raise ArgumentError + + proc { + book.reserve() + }.must_raise ArgumentError + end + + it "raises an error if end date is before start date" do + book = Hotel::Booking.new + proc { + book.reserve(start_date: "11/01/2018", end_date: "09/30/2018") + }.must_raise ArgumentError + end + + + end # reserve + +end # Booking Class diff --git a/specs/reservation_spec.rb b/specs/reservation_spec.rb new file mode 100644 index 000000000..2394bcbb0 --- /dev/null +++ b/specs/reservation_spec.rb @@ -0,0 +1,130 @@ +require_relative 'spec_helper' + +describe "Reservation Class" do + + describe "#initialize" do + + it "can be created" do + data = {room: Hotel::Room.new(number:5), start_date: "11/24/2018", end_date: "12/01/2018"} + result = Hotel::Reservation.new(data) + result.must_be_kind_of Hotel::Reservation + end + + it "has start date and end date" do + data = {room: Hotel::Room.new(number: 9), start_date: "11/24/2018", end_date: "12/01/2018"} + reservation = Hotel::Reservation.new(data) + reservation.start_date.must_be_kind_of Date + reservation.end_date.must_be_kind_of Date + end + + it "raises an error if end date is before start date" do + data = {room: Hotel::Room.new(number: 14), start_date: "11/24/2018", end_date: "11/01/2018"} + proc { + Hotel::Reservation.new(data) + }.must_raise ArgumentError + end + + it "raises an error if incorrect date format is provided" do + data = {room: Hotel::Room.new(number: 17), start_date: "Nov. 17", end_date: "Dec 1st"} + proc { + Hotel::Reservation.new(data) + }.must_raise ArgumentError + end + + it "required room is room instance" do + data = {room: Hotel::Room.new(number: 8), start_date: "11/24/2018", end_date: "12/01/2018"} + reservation = Hotel::Reservation.new(data) + reservation.must_respond_to :room + reservation.room.must_be_kind_of Hotel::Room + reservation.room.number.must_equal 8 + end + + it "raises error if room entered is not instance of room" do + data = {room: 18, start_date: "11/24/2018", end_date: "12/01/2018"} + proc { + Hotel::Reservation.new(data) + }.must_raise ArgumentError + + data = {room: "Something else", start_date: "11/24/2018", end_date: "12/01/2018"} + proc { + Hotel::Reservation.new(data) + }.must_raise ArgumentError + end + + it "raises error if no room argument given" do + data = {start_date: "11/24/2018", end_date: "12/01/2018"} + proc { + Hotel::Reservation.new(data) + }.must_raise ArgumentError + end + + + it "raises error if no available rooms on that date" do + # data = {start_date: "11/24/2018", end_date: "12/01/2018"} + # reservation = Hotel::Reservation.new(data) + # reservation.room.must_be_kind_of Hotel::Room + end + + + + + it "raises error if specified room is unavailable" do + # data = {room: 25, start_date: "11/24/2018", end_date: "12/01/2018"} + # proc { + # Hotel::Reservation.new(data) + # }.must_raise ArgumentError + + end + + end # initialize + + describe "#stay_length helper method" do + + it "returns acurate days in integer form for one day reservation" do + room = Hotel::Room.new(number: 4) + reservation = Hotel::Reservation.new(room: room, start_date: "07/15/2018", end_date: "07/16/2018") + expected_price = 1 + result = reservation.stay_length + result.must_equal expected_price + end + + it "returns zero if checkin and checkout are same day" do + room = Hotel::Room.new(number: 4) + reservation = Hotel::Reservation.new(room: room, start_date: "07/15/2018", end_date: "07/15/2018") + expected_price = 0 + result = reservation.stay_length + result.must_equal expected_price + end + + end # stay_length + + describe "#total_cost" do + + it "returns an accurate total cost for 1 day stay" do + expected_price = 100 + room = Hotel::Room.new(number: 5, price: expected_price) + reservation = Hotel::Reservation.new(room: room, start_date: "05/01/2018", end_date: "05/02/2018") + result = reservation.total_cost + result.must_equal expected_price + end + + it "returns an accurate total cost for 3 day stay" do + room = Hotel::Room.new(number: 5, price: 100) + reservation = Hotel::Reservation.new(room: room, start_date: "05/01/2018", end_date: "05/04/2018") + expected_price = room.price * 3 + result = reservation.total_cost + result.must_equal expected_price + end + + it "returns zero if checkout is same day" do + room = Hotel::Room.new(number: 5, price: 300) + reservation = Hotel::Reservation.new(room: room, start_date: "05/01/2018", end_date: "05/01/2018") + expected_price = 0 + result = reservation.total_cost + result.must_equal expected_price + end + + + end # total_cost + +end # Reservation Class diff --git a/specs/room_spec.rb b/specs/room_spec.rb new file mode 100644 index 000000000..718fff4a4 --- /dev/null +++ b/specs/room_spec.rb @@ -0,0 +1,121 @@ +require_relative 'spec_helper' + +describe "Room Class" do + + describe "#initialize" do + + it "can be created" do + data = {number: 4, price: 400} + result = Hotel::Room.new(data) + result.must_be_kind_of Hotel::Room + end + + it "takes a room number" do + data = {number: 4, price: 400} + room = Hotel::Room.new(data) + room.must_respond_to :number + end + + it "has a room number that is an integer" do + data = {number: 4} + room = Hotel::Room.new(data) + room.number.must_be_kind_of Integer + end + + it "raises an argument if incorrect room number provided" do + data = {number: "whatever", price: 400} + proc{ + Hotel::Room.new(data) + }.must_raise ArgumentError + data = {number: 4.56, price: 400} + proc{ + Hotel::Room.new(data) + }.must_raise ArgumentError + end + + it "has a price that is a float" do + room = Hotel::Room.new({number: 5}) + room.must_respond_to :price + room.price.must_be_kind_of Float + end + + it "accepts an optional price" do + price = 456.82 + room = Hotel::Room.new({number: 5, price: price}) + room.price.must_equal price + end + + end # initialize + + describe "#reserve" do + before do + @room = Hotel::Room.new(number:5) + end + + it "adds each day of stay into reserved array" do + @room.reserved.length.must_equal 0 + start_date = Date.new(2018, 05, 03) + end_date = Date.new(2018, 05, 13) + days_in_stay = end_date - start_date + @room.reserve(start_date, end_date) + @room.reserved.length.must_equal days_in_stay + end + + end # reserve + + describe "#reserved?" do + + before do + start_date = Date.new(2018, 04, 12) + end_date = Date.new(2018, 04, 15) + @room = Hotel::Room.new(number: 12) + @room.reserve(start_date, end_date) + end + + it "returns true if room is reserved on that day" do + result = @room.reserved?(Date.new(2018, 04, 13)) + result.must_equal true + end + + it "returns false if room is not reserved on that day" do + result = @room.reserved?(Date.new(2018, 04, 17)) + result.must_equal false + end + + it "returns false for checkout day" do + result = @room.reserved?(Date.new(2018, 04, 15)) + result.must_equal false + end + + end # reserved? + + describe "#block" do + it "updates the room's blocked hash with name key value" do + + end + + it "updates the value of key to array of dates" do + + end + + it "raises error if incorrect date format entered" do + + end + + + end # block + + describe "#blocked?" do + + it "returns the block name if room is currently blocked" do + + end + + it "returns nil if the room is not blocked" do + + end + + end # blocked? + + +end # Room Class diff --git a/specs/spec_helper.rb b/specs/spec_helper.rb new file mode 100644 index 000000000..9b5f8e338 --- /dev/null +++ b/specs/spec_helper.rb @@ -0,0 +1,15 @@ +require 'simplecov' +SimpleCov.start do + add_filter "/specs" +end + +require 'minitest' +require 'minitest/autorun' +require 'minitest/reporters' + +Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new + +require_relative '../lib/room' +require_relative '../lib/reservation' +require_relative '../lib/booking' +require_relative '../lib/validate'