Jessicascn
Jessicascn

Reputation: 161

Search by availability/dates Ruby On Rails

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

Answers (2)

3limin4t0r
3limin4t0r

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 provide Time.new(2019, 10, 1)..Time.new(2019, 9, 23) the results are going to be skewed.

Upvotes: 1

SteveTurczyn
SteveTurczyn

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

Related Questions