diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..669c12ca0 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 5e1422c9c..a4d4ff8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ build-iPhoneSimulator/ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc +sandbox.rb 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/README.md b/README.md index 07dc42bcf..735f9ab65 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ Remember that your job is only to build the classes that store information and h ### User Stories - As an administrator, I can view a list of rooms that are not reserved for a given date range + - reservation class returns reservations for that range + - book class looks at reservations - As an administrator, I can reserve an available room for a given date range ### Constraints @@ -123,7 +125,7 @@ If you are not familiar with what a block of hotel rooms, here is a brief descri ## Before Submissions -Usually by the end of a project, we can look back on what we made with a clearer understanding of what we actually needed. In industry, this is a great time to do a refactor of some sort. For this project however, you're off the hook... for the moment. We will be revisiting our hotels later on on the course, and you may want to make some changes at that point. +Usually by the end of a project, we can look back on what we made with a clearer understanding of what we actually needed. In industry, this is a great time to do a refactor of some sort. For this project however, you're off the hook... for the moment. We will be revisiting our hotels later on on the course, and you may want to make some changes at that point. - Create a new file in the project called `refactors.txt` - Make a short list of the changes that you could make, particularly in terms of naming conventions @@ -139,4 +141,4 @@ You should not be working on these (or even thinking about them) until you have - Create a CLI to interact with your hotel system ## What we're looking for -You can find what instructors will be looking for in the [feedback](feedback.md) markdown document. +You can find what instructors will be looking for in the [feedback](feedback.md) markdown document. diff --git a/data/room_numbers.csv b/data/room_numbers.csv new file mode 100644 index 000000000..079740ecf --- /dev/null +++ b/data/room_numbers.csv @@ -0,0 +1 @@ +1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 diff --git a/design-activity.md b/design-activity.md new file mode 100644 index 000000000..929d8a07c --- /dev/null +++ b/design-activity.md @@ -0,0 +1,59 @@ +# Design Activity +https://github.com/Ada-Developers-Academy/textbook-curriculum/blob/master/02-intermediate-ruby/exercises/hotel-revisited.md + +## Prompts + +#### What classes does each implementation include? Are the lists the same? +Both implementations have the same three classes: `CartEntry`, `ShoppingCart`, and `Order`. + +#### Write down a sentence to describe each class. +| Class | A | B | +| :------------ | :------------- | :------------- | +| CartEntry | **State:** knows its unit price and its quantity. | **State:** knows its unit price and its quantity.

**Behavior:** can calculate its own price. | +| ShoppingCart | **State:** stores an array of CartEntries. | **State:** stores an array of CartEntries.

**Behavior:** can ask each CartEntry for its price, and then calculate the subtotal (price of all entries in the array). | +| Order | **State:** knows the SALES TAX constant, stores an instance of the ShoppingCart class.

**Behavior:** can calculate a CartEntries price, can calculate a ShoppingCart's price, and can assemble the two into a total price. | **State:** knows the SALES TAX constant, stores an instance of the ShoppingCart class.

**Behavior:** can ask the ShoppingCart for its price (subtotal), and then calculate the total price. | + +("stores" == "knows") + +#### How do the classes relate to each other? It might be helpful to draw a diagram on a whiteboard or piece of paper. + +The difference between A and B is that the Order class is concerned with *how* (A) versus *what* (B). + +In A, the Order class knows too much outside of its "jurisdiction" -- it knows *how* to calculate CartEntry's price and *how* to calculate ShoppingCart's price. In A, classes are tightly coupled. + +In B, the Order class asks CartEntry *what* its price is, asks ShoppingCart *what* its price is, and, from there, knows *how* to calculate a total price. In B, classes are loosely coupled. + +#### What **data** does each class store? How (if at all) does this differ between the two implementations? +Classes store the **State** described in the above table. + +#### What **methods** does each class have? How (if at all) does this differ between the two implementations? +Classes have methods for the **Behavior** described in the above table. + +#### 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`? + +A: the latter. B: the former. + + - Does `total_price` directly manipulate the instance variables of other classes? + +A: yes. B: no. + +#### If we decide items are cheaper if bought in bulk, how would this change the code? Which implementation is easier to modify? + +In the real world, like on Zazzle.com or CafePress.com, items are likely to each have unique wholesale price brackets based on quantity (though I suppose it could be something like 10% off if you spend $100 or more). Going with the former, we'd do this by adding some data - probably a price_table of quantities and prices, either an array of arrays or an array of hashes. + +To modify A, CartEntry's unit_price has to be changed to price_table, and Order's total_price method has to be changed to include an enumerable method to look up a price by a quantity. + +To modify B, CartEntry's unit_price also has to be changed to price_table, and CartEntry's price method also has to be changed to look up a price by a quantity. + +B is easier to modify because changes in one class do not necessitate changes in another class. The person making the change doesn't have to hunt through the code to figure out effects of the change. The change is more proportional to the cost of change. There's less risk of far-off, hidden, unwanted effects of change. + +#### Which implementation better adheres to the single responsibility principle? + +I think B does, but I also think that classes in both A and B are *seemingly* single-responsibility. In both A and B, CartEntry has price and quantity, ShoppingCart has entries, and Order has a cart and a total price. + +However, I think B is the better answer because A's CartEntry can and should shift the responsibility of knowing about the instance variables of other classes from itself to the classes in question. + +#### Bonus question once you've read Metz ch. 3: Which implementation is more loosely coupled? + +B! diff --git a/hotbook.rb b/hotbook.rb new file mode 100644 index 000000000..aeb542c30 --- /dev/null +++ b/hotbook.rb @@ -0,0 +1,24 @@ +# gems the project needs +require "csv" +require "date" + +# Optional - for developer use +require "pry" +require "awesome_print" + +# project constants +ROOM_NUMBERS_FILENAME = "data/room_numbers.csv" +TEST_RESERVATION_FILENAME = "support/test_reservation_data.csv" +RESERVATION_DATA_FILENAME = TEST_RESERVATION_FILENAME #"data/reservation_data.csv" + +# namespace module +module HotBook; +end + +# all of the classes that live in the module +require_relative "lib/block.rb" +require_relative "lib/bookingsmanager.rb" +require_relative "lib/daterange.rb" +require_relative "lib/errors.rb" +require_relative "lib/hotel.rb" +require_relative "lib/reservation.rb" diff --git a/lib/.keep b/lib/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/block.rb b/lib/block.rb new file mode 100644 index 000000000..c83011395 --- /dev/null +++ b/lib/block.rb @@ -0,0 +1,25 @@ +module HotBook + class Block + attr_reader :available, :rooms, :daterange, :room_rate + + def initialize(daterange:, rooms:, room_rate: 185.0) + @daterange = daterange + @rooms = rooms.map! {|room| room.upcase} + @available = rooms.clone + @room_rate = room_rate + end + +# Removes a room from @available once it's reserved + def disable(query) + room = available.find { |room| room == query } # guaranteed to return 1 + return available.delete(room) # returns value of what it deleted + end + +# Does the block conflict with another block? +# Is this being used?? + def conflict?(other) + return daterange.conflict?(other) + end + + end +end diff --git a/lib/bookingsmanager.rb b/lib/bookingsmanager.rb new file mode 100644 index 000000000..b5193656e --- /dev/null +++ b/lib/bookingsmanager.rb @@ -0,0 +1,170 @@ +module HotBook +# The BookingsManager class is responsible for: +# holding all reservations and blocks, +# searching through them, and +# making new reservations and blocks. +# It includes support methods for determining availability/conflicts. + + class BookingsManager + attr_reader :reservations, :hotel, :blocks + + def initialize(hotel) # expects a dependency injection (HotBook::Hotel.new) + @hotel = hotel + @reservations = Reservation.from_csv(RESERVATION_DATA_FILENAME) + @blocks = [] # a list of Blocks + end + + def new_reservation(daterange, room_number: suggest_room(daterange)) + validate(:daterange, daterange) + validate(:room_number, room_number) + room_number = room_number.upcase + if room_taken?(daterange, room_number) + raise RoomIsTakenError, "Room is already reserved some time within "\ + "this daterange" + elsif room_blocked?(daterange, room_number) + raise RoomIsBlockedError, "Room is blocked some time within this "\ + "daterange" + end + + room_rate = hotel.find_rate(room_number) + + new_reservation = HotBook::Reservation.new(daterange: daterange, + room_number: room_number, + room_rate: room_rate) + reservations << new_reservation + return new_reservation + end + + def new_block(daterange, rooms, discount_rate: 185.0) + validate(:daterange, daterange) + validate(:rooms, rooms) + if rooms.size > 5 + raise ArgumentError, "A Block cannot have more than 5 rooms" + end + # Cannot overlap or conflict with existing reservation + rooms.each do |room_number| + if room_taken?(daterange, room_number) + raise RoomIsTakenError, "Room already has reservation during "\ + "this daterange" + elsif room_blocked?(daterange, room_number) + raise BlockConflictError, "A block already exists on a room during " \ + "this daterange" + end + end + new_block = HotBook::Block.new(daterange: daterange, + rooms: rooms, + room_rate: discount_rate) # Default + blocks << new_block + return new_block + end + + def new_block_reservation(block, room_number: block.available.first) + validate(:block, block) + # raise error if the block has no available reservations + if block.available == [] || nil + raise NoRoomsAvailableError, "This block is fully booked" + end + validate(:room_number, room_number) + room_number = room_number.upcase + # make sure the room number is part of the block + unless block.rooms.include?(room_number) + raise ArgumentError, "Room is not part of the given block" + end + # make sure the room is available + unless block.available.include?(room_number) + raise RoomIsTakenError, "Room already reserved during this block" + end + # Remove this room from its memory array of what's still available: + block.disable(room_number) + new_reservation = HotBook::Reservation.new(daterange: block.daterange, + room_number: room_number, + room_rate: block.room_rate) + @reservations << new_reservation + return new_reservation + end + +# Returns the first room in the array of available rooms + def suggest_room(daterange) + validate(:daterange, daterange) + available = public_avail_rooms(daterange) + if available == nil || available == [] + raise HotBook::NoRoomsAvailableError, "All rooms are booked " \ + "during this daterange" + end + return available.first + end + +# Returns an array of reservations (EXCLUDING checkout day) + def list_by_nights(date) + validate(:date, date) + return reservations.select {|reservation| + reservation.range.include?(date) } + end + +# Returns array of room numbers that are publicly available during a daterange + def public_avail_rooms(daterange) + validate(:daterange, daterange) + a = conflicting_reservations(daterange).map { |reservation| + reservation.room_number } + b = conflicting_blocks(daterange).flat_map { |block| block.rooms } + available_rooms = hotel.room_numbers - a - b + return available_rooms + end + +# Searches all reservation dateranges for any conflict with given daterange, +# returns true if room number is already part of a reservation + def room_taken?(daterange, room_number) + validate(:daterange, daterange) + validate(:room_number, room_number) + a = conflicting_reservations(daterange).map { |reservation| + reservation.room_number } + return a.include?(room_number) + end + +# Searches all block dateranges for any conflict with given daterange, +# and returns true if room number is already part of a block + def room_blocked?(daterange, room_number) + validate(:daterange, daterange) + validate(:room_number, room_number) + b = conflicting_blocks(daterange).flat_map { |block| block.rooms } + return b.include?(room_number) + end + +# Returns an array of reservations with a daterange conflict + def conflicting_reservations(daterange) + validate(:daterange, daterange) + return reservations.select { |reservation| + reservation.daterange.conflict?(daterange) } + end + + #TODO: There's an inconsistency here-- daterange.conflict?(other) vs. block.conflict?(daterange)--PICK ONE! +# Returns an array of blocks with a daterange conflict + def conflicting_blocks(daterange) + validate(:daterange, daterange) + return blocks.select { |block| block.conflict?(daterange) } + end + + private + + def validate(type, var) + case type + when :date + raise ArgumentError, "Invalid date - use Date.parse (expected Date, " \ + "not #{var.class})" unless var.is_a?(Date) + when :room_number + raise ArgumentError, "Invalid room number (expected String, " \ + "not #{var.class})" unless var.is_a?(String) + when :daterange + raise ArgumentError, "Invalid daterange (expected HotBook::DateRange," \ + " not #{var.class})" unless var.is_a?(HotBook::DateRange) + when :rooms + raise ArgumentError, "Invalid rooms (expected Array of " \ + "Strings)" unless var.is_a?(Array) && var.first.is_a?(String) + when :block + raise ArgumentError, "Invalid block (expected HotBook::Block, " \ + "not #{var.class})" unless var.is_a?(HotBook::Block) + end + end + + end +end diff --git a/lib/daterange.rb b/lib/daterange.rb new file mode 100644 index 000000000..1eb78f6d0 --- /dev/null +++ b/lib/daterange.rb @@ -0,0 +1,34 @@ +module HotBook +# The DateRange class compares and does calculations on other DateRanges +# Refer to Date gem docu to understand what date format to use (i.e. y-m-d) + class DateRange + attr_reader :start_date, :end_date + + def initialize(start_date:, end_date:) + @start_date = Date.parse(start_date.to_s) + @end_date = Date.parse(end_date.to_s) + raise ArgumentError, "Invalid range #{self}: End must be > Start)" unless + @end_date > @start_date + end + + def duration + return (end_date - start_date).to_i + end + +# Does daterange conflict with another daterange? + def conflict?(other) + if start_date >= other.end_date || end_date <= other.start_date + return false + else + return true + end + end + +# Range only includes overnights and EXCLUDES checkout day. +# should change this to "contains(date)" + def to_range + return (@start_date...@end_date) + end + end + +end diff --git a/lib/errors.rb b/lib/errors.rb new file mode 100644 index 000000000..4865dc4e5 --- /dev/null +++ b/lib/errors.rb @@ -0,0 +1,13 @@ +module HotBook + class RoomIsTakenError < StandardError + end + + class RoomIsBlockedError < StandardError + end + + class BlockConflictError < StandardError + end + + class NoRoomsAvailableError < StandardError + end +end diff --git a/lib/hotel.rb b/lib/hotel.rb new file mode 100644 index 000000000..e2de6d8d7 --- /dev/null +++ b/lib/hotel.rb @@ -0,0 +1,41 @@ +module HotBook +# The Hotel class is responsible for knowing about rooms and all rooms + + class Hotel + attr_reader :room_rate, :room_numbers, :rooms + + def initialize(room_rate: 200.0, room_numbers: ROOM_NUMBERS_FILENAME) + @room_rate = room_rate + @room_numbers = load_room_numbers(room_numbers) # Array of Strings + # do i even need room hash? or just room numbers? + @rooms = load_rooms # array of hashes + # a "room" Hash ==== {room_number: "String", room_rate: 200.0} + # if @rooms == nil || @rooms == [] + # raise StandardError, "Hotel has no rooms (failed to initalize)" + # end + end + + def load_room_numbers(filename) + CSV.open(filename).flat_map{ |line| line.map { |row| row.upcase }} + end + +# refactor with load CSV of rooms in order to truly test + def find_rate(room_number) + room_number = room_number.upcase + room = rooms.find {|room| room[:room_number] == room_number} + return room[:room_rate] + end + + def load_rooms + new_rooms = [] + room_numbers.each do |room_number| + a_room = Hash.new + a_room[:room_number] = room_number + a_room[:room_rate] = room_rate + new_rooms << a_room + end + return new_rooms + end + end + +end diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..5490a6ee0 --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,48 @@ +module HotBook +# The Reservation class is responsible for calculating reservation cost + class Reservation + attr_reader :daterange, :room_number, :room_rate, :notes + + def initialize(daterange:, room_number:, room_rate:, notes: nil) + @daterange = daterange + @room_number = room_number + @room_rate = room_rate + @notes = notes + end + +# Does the reservation conflict with another reservation? +# Is this being used?? + def conflict?(other) + return daterange.conflict? other.daterange + end + + def duration + return daterange.duration + end + + def cost + return duration * room_rate + end + +# Range only includes overnights and EXCLUDES checkout day. + def range + return daterange.to_range + end + + def self.from_csv(filename) + reservations = CSV.open(filename).map { |row| + start_date = row[0] + end_date = row[1] + room_number = row[2] + room_rate = row[3].to_f + notes = row[4] + + HotBook::Reservation.new( + daterange: HotBook::DateRange.new(start_date: start_date, + end_date: end_date), + room_number: room_number, room_rate: room_rate, notes: notes) + } + return reservations + end + end +end diff --git a/refactors.txt b/refactors.txt new file mode 100644 index 000000000..8d2708631 --- /dev/null +++ b/refactors.txt @@ -0,0 +1,25 @@ +# hotbook/refactors.txt + +1. Creating a block should not require user to make a list of rooms - make a helper method to find a list of rooms. + +2. Consider suggestion for block class to contain "reservations" array. + +3. Refactor/expand spec tests per suggestions. + +4. Could all classes inherit from a CSVLoader class? + + + + +## Previous refactors: +1. Create "rooms" CSV and load into hotel class into hash. Unless rooms have a lot more information than just room number and rate, they don't have to become a Rooms class object yet. The Hotel class is named Hotel rather than Rooms so as not to be confusing. There's a tradeoff there. + +2. More consistency in terms of when keyword arguments vs positional arguments are used. + +3. Hotel class needs to be able to add and remove rooms. + +4. Block class could parse CSV block data (which will have an identical format as CSV reservation data) into Blocks--or, another approach is for CSV block data to be loaded by Reservations, and for Block class to parse an array of Reservations into Blocks. + +5. Learn about how better to structure classes if they load CSVs similarly. Like FarMar? + +6. Come up with a more methodical way to do spec tests that more carefully ensures date/reservation/block conflicts are all caught as they should be. diff --git a/spec/block_spec.rb b/spec/block_spec.rb new file mode 100644 index 000000000..4cd725a7f --- /dev/null +++ b/spec/block_spec.rb @@ -0,0 +1,44 @@ +require_relative "spec_helper" + +describe "HotBook::Block class" do + let(:rooms) { ["1","2","3"] } + let(:daterange) { + HotBook::DateRange.new(start_date: "apr_6", end_date: "apr_7") } + let(:block) { + HotBook::Block.new(daterange: daterange, + rooms: rooms) } + + + describe "initialize method" do + it "will store an array of rooms" do + expect(block.rooms).must_equal rooms + expect(block.available).must_equal rooms + end + it "will store upcase room numbers" do + rooms = ["one", "two"] + block = HotBook::Block.new(daterange: daterange, + rooms: rooms) + expect(block.available).must_equal ["ONE", "TWO"] + end + end + + describe "disable method" do + it "won't alter the rooms array when it executes" do + block.disable("1") + expect(block.available).must_equal ["2", "3"] + expect(block.rooms).must_equal ["1", "2", "3"] + end + end + + describe "conflict? method" do + it "will correctly return a daterange conflict" do + conflict = HotBook::DateRange.new(start_date: "apr_6", end_date: "apr_7") + anotherconflict = HotBook::DateRange.new(start_date: "apr_5", end_date: "apr_8") + noconflict = HotBook::DateRange.new(start_date: "apr_1", end_date: "apr_6") + + expect(block.conflict?(conflict)).must_equal true + expect(block.conflict?(anotherconflict)).must_equal true + expect(block.conflict?(noconflict)).must_equal false + end + end +end diff --git a/spec/bookingsmanager_spec.rb b/spec/bookingsmanager_spec.rb new file mode 100644 index 000000000..0896bd946 --- /dev/null +++ b/spec/bookingsmanager_spec.rb @@ -0,0 +1,178 @@ +require_relative "spec_helper" + +describe "HotBook::BookingsManager class" do + let(:hotel) { HotBook::Hotel.new } + let(:bookingsmanager) { HotBook::BookingsManager.new(hotel) } + let(:daterange) { HotBook::DateRange.new( + start_date: "apr_15", end_date: "apr_30") } + let(:new_reservation) { bookingsmanager.new_reservation(daterange) } + let(:load_reservations) { hotel.reservations } + let(:block) { HotBook::Block.new(daterange, ["1", "2", "3", "4", "5"]) } + let(:rooms) { ["1", "2", "3", "4", "5"] } + let(:load_block) { bookingsmanager.new_block(daterange, rooms) } + let(:date) {Date.parse("apr_15")} + let(:overlaprange) { HotBook::DateRange.new( start_date: "apr_08", + end_date: "apr_16") } + let(:shortrange) { HotBook::DateRange.new( start_date: "may_01", + end_date: "may_02") } + let(:current_block){bookingsmanager.blocks[0]} + + before do + # Test reservation data is loaded into @reservations: + bookingsmanager + # A single block, rooms 1-5, 4/15-4/30, is loaded into @blocks: + block = load_block + + # This mimics what would happen if test reservations 6-9 were actually + # created as block reservations: + block.disable("1") + block.disable("2") + block.disable("3") + block.disable("4") + end + + describe "new_reservation method" do + it "creates a Reservation object and stores in @reservations" do + expect(new_reservation).must_be_instance_of HotBook::Reservation + expect(bookingsmanager.reservations.size).must_equal 11 + 3.times {bookingsmanager.new_reservation(daterange) } + expect(bookingsmanager.reservations.size).must_equal 11 + 3 + end + + it "raises unique errors if room is booked or blocked during daterange" do + expect{bookingsmanager.new_reservation(daterange, room_number: "6")}.must_raise HotBook::RoomIsTakenError + expect{bookingsmanager.new_reservation(daterange, room_number: "1")}.must_raise HotBook::RoomIsBlockedError + end + end + + describe "suggest_room method" do + it "returns the first publicly available room number that isn't booked or blocked" do + expect(bookingsmanager.suggest_room(daterange)).must_equal "7" + end + end + + describe "edge case extravaganza: being super sure you can't overbook" do + it "new reservation nominal case" do + 20.times{bookingsmanager.new_reservation(shortrange)} + expect(bookingsmanager.reservations.last.room_number).must_equal "20" + end + + it "new reservation edge case" do + expect{ 21.times { bookingsmanager.new_reservation(shortrange) } }.must_raise HotBook::NoRoomsAvailableError + expect{bookingsmanager.new_reservation(shortrange, room_number: "1")}.must_raise HotBook::RoomIsTakenError + end + + it "new block case" do + expect{2.times { bookingsmanager.new_block(shortrange, rooms)} }.must_raise HotBook::BlockConflictError + end + + it "new_block_reservation case" do + new_block = bookingsmanager.new_block(shortrange, rooms) + expect{ 6.times{ bookingsmanager.new_block_reservation(new_block) } }.must_raise HotBook::NoRoomsAvailableError + end + end + + describe "new_block_reservation method" do + it "will remove the room from block's memo array of what's still reservable" do + expect(current_block.available.size).must_equal 1 + bookingsmanager.new_block_reservation(current_block) + expect(current_block.available.size).must_equal 0 + end + + it "will raise an error if the room number given is not in the given block" do + expect{bookingsmanager.new_block_reservation(current_block, room_number: "20")}.must_raise ArgumentError + end + + it "will raise an error if the room is in the block, but already booked" do + expect{bookingsmanager.new_block_reservation(current_block, room_number: "1")}.must_raise HotBook::RoomIsTakenError + end + + it "will add the new reservation to the @reservations array" do + expect(bookingsmanager.reservations.last.room_number).must_equal "6" + bookingsmanager.new_block_reservation(current_block) + expect(bookingsmanager.reservations.last.room_number).must_equal "5" + end + end + + describe "new_block method" do + it "cannot overlap or conflict with an existing block" do + expect{bookingsmanager.new_block(daterange, rooms)}.must_raise HotBook::BlockConflictError + end + + it "cannot overlap or conflict with an existing reservation" do + expect{bookingsmanager.new_block(overlaprange, rooms)}.must_raise HotBook::RoomIsTakenError + expect{bookingsmanager.new_block(daterange, ["6"])}.must_raise HotBook::RoomIsTakenError + end + + it "cannot contain more than 5 rooms" do + bad_rooms = %w(1 2 3 4 5 6) + expect{bookingsmanager.new_block(daterange, bad_rooms)}.must_raise ArgumentError + end + end + + describe "list_by_nights method" do + it "returns an array of reservations excluding checkout date" do + expect(bookingsmanager.list_by_nights(date).size).must_equal 1 + expect(bookingsmanager.list_by_nights(date)[0].room_number).must_equal "6" + end + end + + describe "public_avail_rooms method" do + it "returns an array of room numbers that aren't booked or blocked" do + list = %w(7 8 9 10 11 12 13 14 15 16 17 18 19 20) + expect(bookingsmanager.public_avail_rooms(overlaprange)).must_equal list + bookingsmanager.new_reservation(overlaprange) + list.shift + expect(bookingsmanager.public_avail_rooms(overlaprange)).must_equal list + end + end + + describe "room_taken? method" do + it "given daterange, lists rezzies with conflict, " \ + "and tells you if the given room is in that list" do + expect(bookingsmanager.room_taken?(daterange, "6")).must_equal true + expect(bookingsmanager.room_taken?(daterange, "1")).must_equal false + end + end + + describe "room_blocked? method" do + it "given daterange, lists blocks with conflict, " \ + "and tells you if the given room is in that list" do + expect(bookingsmanager.room_blocked?(daterange, "1")).must_equal true + expect(bookingsmanager.room_blocked?(daterange, "6")).must_equal false + end + end + + describe "conflicting_reservations method" do + it "returns an array of reservations with a daterange conflict" do + array = bookingsmanager.conflicting_reservations(daterange) + expect(array.size).must_equal 1 + expect(array[0]).must_be_instance_of HotBook::Reservation + end + end + + describe "conflicting_blocks method" do + it "returns an array of blocks with a daterange conflict" do + array = bookingsmanager.conflicting_blocks(daterange) + expect(array.size).must_equal 1 + expect(array[0]).must_be_instance_of HotBook::Block + end + end + + describe "validate(type, var) method" do + it "raises ArgError for invalid inputs" do + bad_range = nil + bad_room_number = nil + bad_date = nil + bad_rooms = [] + another_bad_rooms = nil + bad_block = nil + expect{bookingsmanager.new_reservation(bad_range)}.must_raise ArgumentError + expect{bookingsmanager.new_reservation(daterange, room_number: bad_room_number)}.must_raise ArgumentError + expect{bookingsmanager.list_by_nights(bad_date)}.must_raise ArgumentError + expect{bookingsmanager.new_block(daterange, bad_rooms)}.must_raise ArgumentError + expect{bookingsmanager.new_block(daterange, another_bad_rooms)}.must_raise ArgumentError + expect{bookingsmanager.new_block_reservation(bad_block, room_number: "2")}.must_raise ArgumentError + end + end +end diff --git a/spec/daterange_spec.rb b/spec/daterange_spec.rb new file mode 100644 index 000000000..61adce117 --- /dev/null +++ b/spec/daterange_spec.rb @@ -0,0 +1,122 @@ +require_relative "spec_helper" + +describe "HotBook::DateRange class" do + let(:daterange) { + HotBook::DateRange.new(start_date: "apr_1", end_date: "apr_2") } + + describe "initialize method" do + it "accepts Strings as args" do + expect(daterange.start_date).must_equal Date.parse("apr_1") + end + + it "accepts Date objects as args" do + date1 = Date.parse("apr_1_00") + date2 = Date.parse("apr_6_00") + daterange = HotBook::DateRange.new(start_date: date1, end_date: date2) + end + + it "start_date and end_date instance vars are Date class objects" do + expect(daterange.start_date).must_be_instance_of Date + end + + it "correctly raises a StandardError if end !>= start" do + end_dates = ["apr_6", "apr_1"] + end_dates.each do |end_date| + expect{HotBook::DateRange.new(start_date: "apr_6", + end_date: end_date) + }.must_raise StandardError + end + end + + it "works with Time.to_date, including case T2>T1 but same date" do + time1 = Time.now # today + time2 = time1 + 60 # (arbitrary) 1 minute later today + date1 = time1.to_date + date2 = time2.to_date + expect{HotBook::DateRange.new(start_date: date1, end_date: date2) + }.must_raise StandardError + end + + it "handles edge cases for both 1 nil arg AND 2 nil args" do # might break if I raise errors elsewhere + expect{HotBook::DateRange.new(start_date: nil, end_date: nil) + }.must_raise StandardError + expect{HotBook::DateRange.new(start_date: nil, end_date: "apr_1") + }.must_raise StandardError + expect{HotBook::DateRange.new(start_date: "apr_1", end_date: nil) + }.must_raise StandardError + end + end + + describe "duration method" do + it "correctly sums total NIGHTS of the stay, even for a one night stay" do + daterange = HotBook::DateRange.new(start_date: "apr_1", end_date: "apr_2") + expect(daterange.duration).must_equal 1 + end + end + # + # describe "is_valid? method" do + # it "returns true for valid range" do + # expect(daterange.is_valid?).must_equal true + # end + # it "raises ArgError for invalid range" do + # end + # end + + describe "conflict? method" do + range1 = HotBook::DateRange.new(start_date: "apr_6", end_date: "apr_10") + # check these new ranges against range1: + noconflict1 = HotBook::DateRange.new(start_date: "apr_1", end_date: "apr_6") + noconflict2 = HotBook::DateRange.new(start_date: "apr_1", end_date: "apr_3") + noconflict3 = HotBook::DateRange.new(start_date: "apr_10", end_date: "apr_30") + noconflict4 = HotBook::DateRange.new(start_date: "apr_11", end_date: "apr_30") + conflict1= HotBook::DateRange.new(start_date: "apr_1", end_date: "apr_7") + conflict2 = HotBook::DateRange.new(start_date: "apr_6", end_date: "apr_7") + conflict3= HotBook::DateRange.new(start_date: "apr_6", end_date: "apr_10") + conflict4 = HotBook::DateRange.new(start_date: "apr_6", end_date: "apr_12") + conflict5 = HotBook::DateRange.new(start_date: "apr_7", end_date: "apr_10") + conflict6 = HotBook::DateRange.new(start_date: "apr_7", end_date: "apr_30") + + it "will return false if ranges don't conflict" do + expect(range1.conflict?(noconflict1)).must_equal false + expect(range1.conflict?(noconflict2)).must_equal false + expect(range1.conflict?(noconflict3)).must_equal false + expect(range1.conflict?(noconflict4)).must_equal false + end + + it "will return true if ranges do conflict" do + expect(range1.conflict?(conflict1)).must_equal true + expect(range1.conflict?(conflict2)).must_equal true + expect(range1.conflict?(conflict3)).must_equal true + expect(range1.conflict?(conflict4)).must_equal true + expect(range1.conflict?(conflict5)).must_equal true + expect(range1.conflict?(conflict6)).must_equal true + end + + it "will work in reverse" do + expect(noconflict1.conflict?(range1)).must_equal false + expect(noconflict2.conflict?(range1)).must_equal false + expect(noconflict3.conflict?(range1)).must_equal false + expect(noconflict4.conflict?(range1)).must_equal false + expect(conflict1.conflict?(range1)).must_equal true + expect(conflict2.conflict?(range1)).must_equal true + expect(conflict3.conflict?(range1)).must_equal true + expect(conflict4.conflict?(range1)).must_equal true + expect(conflict5.conflict?(range1)).must_equal true + expect(conflict6.conflict?(range1)).must_equal true + end + end + + describe "to_range method" do + it "will return a Range class object" do + range = daterange.to_range + expect(range).must_be_instance_of Range + end + + it "correctly allows a res end and another res start on the same day" do + date = Date.parse("apr_2") + range = daterange.to_range + expect(range.include? date).must_equal false + end + end + +end diff --git a/spec/hotel_spec.rb b/spec/hotel_spec.rb new file mode 100644 index 000000000..cfeac2352 --- /dev/null +++ b/spec/hotel_spec.rb @@ -0,0 +1,63 @@ +require_relative "spec_helper" + +describe "HotBook::Hotel class" do + let(:hotel) { HotBook::Hotel.new } + let(:room_rate) { hotel.room_rate } + let(:room_numbers) { hotel.room_numbers } + let(:rooms) { hotel.rooms } + let(:valid_room_numbers) { ["1", "2", "3", "4", "5", "6", "7", "8", "9", + "10", "11", "12", "13", "14", "15", "16", "17", + "18", "19", "20"] } + + describe "initialize method" do + it "loads room rate correctly" do + expect(room_rate).must_be_instance_of Float + expect(room_rate).must_equal 200.0 + end + + it "loads room numbers correctly" do + expect(room_numbers).must_equal valid_room_numbers + end + + it "upcases room numbers before storing them" do + test_room_numbers_filename = "support/test_room_numbers.csv" + hotel = HotBook::Hotel.new(room_numbers: test_room_numbers_filename) + expect(hotel.room_numbers).must_equal ["ROOM THIRTEEN"] + end + end + +## Project Requirement: User can view a list of rooms + describe "load_rooms method" do + it "room numbers are all valid-type key/value pairs" do + rooms.each_with_index do |room, index| + expect(room[:room_number]).must_be_instance_of String + expect(room[:room_number]).must_equal valid_room_numbers[index] + end + end + + it "room rates are all valid-type key/value pairs" do + rooms.each do |room| + expect(room[:room_rate]).must_equal 200.0 + end + end + end + + describe "project requirement: user can view a list of rooms" do + # This requires that test(s) MUST pass for "load rooms method" + it "can return a list of rooms" do + expect(hotel.rooms).must_be_instance_of Array + end + end + +# TODO: AFter the CSV load function is added +# write both these tests +##### THIS ISN'T REALLY TESTING THAT MUCH + describe "find_rate method" do + it "Hotel object can look up the correct room" do + expect(hotel.find_rate("3")).must_equal 200.0 + end + # + # it "handles edge case @rooms = nil" do + # end + end +end diff --git a/spec/reservation_spec.rb b/spec/reservation_spec.rb new file mode 100644 index 000000000..d47b04320 --- /dev/null +++ b/spec/reservation_spec.rb @@ -0,0 +1,59 @@ +require_relative "spec_helper" + +## Project Requirement: User can reserve a room for a given date range +describe "HotBook::Reservation class" do + let(:daterange) { HotBook::DateRange.new(start_date: "apr_6", + end_date: "apr_8") } + let(:reservation) { HotBook::Reservation.new(daterange: daterange, + room_number: "one", + room_rate: 1.0) } + let(:load_reservations) { HotBook::Reservation.from_csv(TEST_RESERVATION_FILENAME) } + describe "initialize method" do + it "will correctly calculate duration" do + expect(reservation.duration).must_equal 2 + end + + it "will correctly accept room_number" do + expect(reservation.room_number).must_equal "one" + end + + it "will correctly accept room_rate" do + expect(reservation.room_rate).must_equal 1.0 + end + end + + describe "range method" do + it "range only includes overnights and excludes checkout day" do + range = load_reservations[0].range + checkout_date = Date.parse("apr_6") + expect(range).must_be_instance_of Range + expect(range.include?(checkout_date)).must_equal false + end + end + + describe "cost method" do + it "will return correct cost" do + expect(reservation.cost).must_equal 2.0 + end + end + + describe "conflict? method" do + it "will correctly determine if there's a daterange conflict" do + thisres = load_reservations[1] + thatres = load_reservations[3] + anotherres = load_reservations[4] + expect(thisres.conflict?(thatres)).must_equal false + expect(thisres.conflict?(anotherres)).must_equal true + end + end + + describe "csv loading" do + it "will correctly load lines of the CSV" do + blocknote = "This is a block (private) reservation" + expect(load_reservations[5].room_number).must_equal "1" + expect(load_reservations[5].notes).must_equal blocknote + expect(load_reservations.first.daterange.start_date).must_equal Date.parse("apr_1") + end + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4d1e3fdc8..bf4c53755 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,8 +1,16 @@ -require 'minitest' +require 'simplecov' +SimpleCov.start + require 'minitest/autorun' require 'minitest/reporters' -# Add simplecov +require 'minitest/skip_dsl' +require "minitest/pride" +require "pry" +require "awesome_print" +require "date" +require "csv" -Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new +# Add lib files here (require_relative "../") +require_relative "../hotbook.rb" -# Require_relative your lib files here! +Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new diff --git a/support/test_reservation_data.csv b/support/test_reservation_data.csv new file mode 100644 index 000000000..1cef15f5e --- /dev/null +++ b/support/test_reservation_data.csv @@ -0,0 +1,10 @@ +2018-04-01,2018-04-06,1,200.0,"This is the first reservation" +2018-04-01,2018-04-06,2,200.0,"thisres" +2018-04-01,2018-04-06,3,200.0, +2018-04-06,2018-04-08,3,200.0,"thatres" +2018-04-05,2018-04-06,4,200.0,"anotherres" +2018-04-10,2018-04-15,1,185.0,"This is a block (private) reservation" +2018-04-10,2018-04-15,2,185.0,"This is a block (private) reservation" +2018-04-10,2018-04-15,3,185.0,"This is a block (private) reservation" +2018-04-10,2018-04-15,4,185.0,"This is a block (private) reservation" +2018-04-15,2018-04-30,6,200.0,"This is a public reservation" diff --git a/support/test_room_numbers.csv b/support/test_room_numbers.csv new file mode 100644 index 000000000..a8b97d995 --- /dev/null +++ b/support/test_room_numbers.csv @@ -0,0 +1 @@ +room thirteen