Reputation: 161
I've built a RoR app and implemented a simple booking system. The user is able to look for a space and can book it per day or per hour. Everything works well, but I would now like to make the user able to look for a space depending on its availability. I want to user to be able to select a start/end date and a start/end time and to show only spaces that don't have any booking included in this period.
I am using pg search at the moment to look for a space by category and location, but I have no idea how to implement a search by date and time, as it uses a different logic. I've tried to do it by hand by creating an array of bookings for each space so I could compare it with the params, but it sounded very complicated and not clean (and I started being stuck anyway, as making it available for one hour or several hours or several days makes it even more complicated)
Is there a gem that could do this for me? If I have to do it by hand, what's the best way to begin?
Thanks a lot
Upvotes: 0
Views: 1206
Reputation: 21130
Taking some inspiration from the answer of SteveTurczyn. The following might give you some inspiration.
class Space < ApplicationRecord
# attributes: id
has_many :bookings
def self.available(period)
bookings = Booking.overlap(period)
where.not(id: bookings.select(:space_id))
end
def available?(period)
if bookings.loaded?
bookings.none? { |booking| booking.overlap?(period) }
else
bookings.overlap(period).none?
end
end
end
class Booking < ApplicationRecord
# attributes: id, space_id, start, end
belongs_to :space
def self.overlap(period)
period = FormatConverters.to_period(period)
# lteq = less than or equal to, gteq = greater than or equal to
# Other methods available on attributes can be found here:
# https://www.rubydoc.info/gems/arel/Arel/Attributes/Attribute
where(arel_table[:start].lteq(period.end).and(arel_table[:end].gteq(period.start)))
end
def overlap?(period)
period = FormatConverters.to_period(period)
self.start <= period.end && self.end >= period.start
end
module FormatConverters
module_function
def to_period(obj)
return obj if obj.respond_to?(:start) && obj.respond_to?(:end)
obj..obj
end
end
end
With the above implemented you can query a single space if it is available during a period:
from = Time.new(2019, 10, 1, 9, 30)
to = Time.new(2019, 10, 5, 17, 30)
period = from..to
space.available?(period) # true/false
You can get all spaces available:
spaces = Space.available(period) # all available spaces during the period
Note that class methods will also be available on the scope chain:
spaces = Space.scope_01.scope_02.available(period)
I've also added the overlap
scope and overlap?
helper to simplify creating the above helpers.
Since in my version Booking
has a start
and end
attribute (similar to Range
) you can also provide it to any methods accepting a period.
booking_01.overlap?(booking_02) # true/false
To retrieve all bookings that that overlap this very moment:
bookings = Booking.overlap(Time.now) # all bookings overlapping the period
Hope this gave you some inspiration. If you'd like to know how the overlap checking works I have to forward you to this question.
Note: This answer assumes that the provided period is valid. A.k.a.
start
<=end
. If you for some reason provideTime.new(2019, 10, 1)..Time.new(2019, 9, 23)
the results are going to be skewed.
Upvotes: 1
Reputation: 36870
Just create an instance method available?
which tests there are no bookings that overlap the from to range. You can use none?
on the relationship.
class Space
has_many :bookings
def available?(from, to)
bookings.where('start_booking <= ? AND end_booking >= ?', to, from).none?
end
end
Upvotes: 4