How to check if a booking between two dates is possible - ruby-on-rails

An Offer has a number of places. It means that each day places bookings can be made for this offer. An Offer has_many bookings.
A Booking has a date_begin, a date_end and belongs_to an Offer. It means that each day between date_begin and date_end one place is used regarding the associated Offer.
Given a requested booking for an offer, how should I check if it can be validated?
Example :
- offer has 2 places (offer.places == 2).
- offer has currently 3 bookings (offer.bookings.count == 3)
- First booking B1 is between 01-04-2016 and 10-04-2016
- Second booking B2 is between 01-04-2016 and 05-04-2016
- Third booking B3 is between 08-04-2016 and 10-04-2016.
offer.available_between?("01-04-2016", "10-04-2016")
=> false (because of B1 and B2, and because of B1 and B3)
offer.available_between?("01-04-2016", "05-04-2016")
=> false (because of B1 and B2)
offer.available_between?("08-04-2016", "10-04-2016")
=> false (because of B1 and B3)
offer.available_between?("06-04-2016", "07-04-2016")
=> true (because there is only B1 during this period)
Here is a try:
class Offer < ActiveRecord::Base
# True if and only if each day between date_begin and date_end has at least one place left.
def available_between?(date_begin, date_end)
(date_begin.to_datetime.to_i .. date_end.to_datetime.to_i).step(1.day) do |date|
day = Time.at(date)
nb_places_taken_this_day = self.bookings.where("date_begin <= :date AND date_end >= :date", date: day).count
return false if nb_places_taken_this_day >= self.places
end
true
end
end
offer.available_between?(booking.date_begin, booking.date_end)
I don't feel confortable with this at all especially because there are multiple separated SQL queries.
Can you see a better way to achieve this using ActiveRecord more efficiently?

I don't have easy enough data to test this throughly, but I'm thinking something like this can work. Here I'm building into your query the condition that no booking exists where the start and end dates are not either both before or both after the requested dates. That is, every booking that does exist had better have either start and end before the requested start, or start and end after the requested end. If there exists a booking that does not meet this condition, then the requested dates are not available as a single booking. Does this work?
[EDIT - modified from original broken answer to reflect the actual problem]
This new answer still loops over days, but does so after grabbing all the conflicting bookings in one query. The ruby loop itself will be pretty quick.
(Note that the :all? method will immediately return false on the first false value and will return true if all elements evaluate to true, so adding the return false unless as suggested by the edit I believe is superfluous. In any case I've added a return to the last line to hopefully clarify the intent.)
def available_between?(date_begin, date_end)
conflicting_bookings = self.bookings.where.not("(date_begin < :requested_start_date AND date_end < :requested_start_date) OR (date_end > :requested_end_date AND date_end > :requested_end_date)", requested_start_date: date_begin, requested_end_date: date_end)
return (date_begin..date_end).all? do |day|
num_bookings_for_day = conflicting_bookings.select{|booking| booking.date_begin <= day && booking.date_end >= day}.count
num_bookings_for_day < self.places
end
end

Related

How to validate range dates with rails 6

hello i have a post module where the user can select the dates for publishing his posts, i have 2 fields
start_date:date and expiration_date:date
i want to make a validation like this
if user selects start_date = 2022-10-14 and expiration_date = 2022-10-22, he can't create another post that is between that range.
because i can have only 1 post published at a time, so with this validation i will force the user to select any other date that is not in between this selected range dates.
Just check that there is no other post that starts before the expiration date and ends after the start date. Also exclude this post's id in your check in case you're updating an existing post. (The post shouldn't prevent itself from changing).
This will catch posts that overlap the current post completely or partially, or that start and end within the current post.
validates :date_range
def date_range
if user.posts.where.not(id: id).where(start_date: ..expiration_date, expiration_date: start_date..).any?
errors.add(:start_date, 'there is already a post that overlaps these dates')
end
end
Inside you Post model you'll need a some validation.
You can create a custom method that will check whether the post you're trying to create has a start date between any current post.
class Post < ApplicationRecord
validate :post_exists? on: :create
private
def post_exists?
return unless Post.where("? BETWEEN start_date AND expiration_date", self.start_date).present?
errors.add(:start_date, "Post already exists")
end
end
I'm unable to test this at the moment, but its roughly what you'll need.
Some light reading on the on the subject ~
Custom validate methods: https://guides.rubyonrails.org/active_record_validations.html#custom-methods
Validating on certain actions, i.e. on: :create: in this case.
https://guides.rubyonrails.org/active_record_validations.html#on
It's worth mentioning there are some very similar other questions on stack, worth a google.
Rails ActiveRecord date between
So if I understand correctly you are looking to ensure there are no "overlaps". This entails ensuring all of the following are true
New start_date is not BETWEEN (inclusive) an existing start_date and expiration_date
New expiration_date is not BETWEEN (inclusive) an existing start_date and expiration_date
New start_date is not prior to an existing start_date and New expiration_date is not after the corresponding existing expiration_date
To satisfy these rules I would implement as follows:
class Post < ApplicationRecord
validates :start_date, presence: true,comparison: { less_than: :expiration_date}
validates :expiration_date, presence: true, comparison: { greater_than: :start_date }
validate :non_overlapping_date_range
def time_frame
start_date..expiration_date
end
private
def non_overlapping_date_range
overlaps_post = Period
.where(start_date: time_frame )
.or(Post.where(expiration_date: time_frame ))
.or(Post.where(start_date: start_date..., expiration_date: ...end_date))
.where.not(id: id)
.exists?
errors.add(:base, "overlaps another Post") if overlaps_post
end
end
This will result in the following query
SELECT 1 AS one
FROM
posts
WHERE
((posts.start_date BETWEEN '####-##-##' AND '####-##-##'
OR posts.expiration_date BETWEEN '####-##-##' AND '####-##-##')
OR (
posts.start_date > '####-##-##'
AND posts.expiration_date < '####-##-##'
))
AND posts.id != # -- IS NOT NULL on create
LIMIT 1
Using OVERLAPS (Postgres)
Postgres offers a function called OVERLAPS however the this does not fully fit the desired situation because this function treats end_date == new_start_date as continuous rather than overlapping. To counteract this we need to adjust the start_date and end_date for the purposes of the query
This can be achieved as follows:
def non_overlapping_date_range
overlaps_post = Post.where.not(id: id)
.where('(?,?) OVERLAPS (posts.start_date, posts.expiration_date)',
start_date - 1, end_date + 1))
.exists?
errors.add(:base, "overlaps another Post") if overlaps_post
end
SQL:
SELECT 1 AS one
FROM
posts
WHERE
('####-##-##','####-##-##') OVERLAPS (posts.start_date,posts.expiration_date)
AND posts.id != # -- IS NOT NULL on create
LIMIT 1
Arel port of the same:
def non_overlapping_date_range
left = Arel::Nodes::Grouping.new(
[Arel::Nodes::UnaryOperation.new(
'DATE',
[Arel::Nodes.build_quoted(start_date - 1)]),
Arel::Nodes::UnaryOperation.new(
'DATE',
[Arel::Nodes.build_quoted(expiration_date + 1)])])
right = Arel::Nodes::Grouping.new(
[Post.arel_table[:start_date],
Post.arel_table[:expiration_date]])
condition = Arel::Nodes::InfixOperation.new('OVERLAPS', left, right)
errors.add(:base, "overlaps another Post") if Post.where.not(id: id).where(condition).exists?
end

Validate if new range overlaps existing range

I have a Range model which has a start_range and an end_range column.
My range.rb model:
class Range < ActiveRecord::Base
validate :range_valid
protected
def range_valid
range = Range.all
range.each do |f|
if (f.start_range..f.end_range).overlaps?(self.start_range..self.end_range)
self.errors.add(:base, 'range is already alloted')
break
end
end
end
end
This code takes the start_range and end_range (say 100 and 500) and matches all the database records if any range overlap (or say the two ranges must be completely exclusive ) with the range which the user have entered.
This code is working fine but this code is not feasible if there are millions of records stored in the database
Can anyone please tell me how can I match the overlapping of the range without using loop and fetching all the records by Range.all so that the code should be feasible for real time.
You can easily query Range to check if an existing range overlaps with the given range.
Range.where("end_date >= ?", start_of_a_range).where("start_date <= ?", end_of_a_range).count
To wrap this into a validator I'd first define a scope
range.rb
scope :in_range, ->(range) { where("end_date >= ?", range.first).where("start_date <= ?", range.last) }
And then add the validator:
validates :range_cannot_overlap
def range_cannot_overlap
if Range.in_range(start_range..end_range).count > 0
errors.add(:base, 'range is already alloted')
end
end

How can I check if a new event (start time - endtime) doesn't overlap with previous startime - endtimes (Ruby)

I am making an appointment, scheduling API. I have many starttime and endtime pairings in DateTime format. I need to be sure that when I create a new appointment that times do not overlap with previous ones. What I mean is that if I have an appointment starting at 7/4/15 9:00 and ending at 7/4/15 13:00 I want to make a validation so that I can't make a new appintment starting at 7/4/15 10:00 ending at 7/4/15 12:00. I want to compare all the key value pairs to make sure the new one doesn't fall inside that range. Any ideas how I can do this?
An overlap happens when you have an a appointment that starts before this appointment ends, and ends after this one starts. If there are no appointments that fit that criteria, there are no overlaps.
Note that you need to also consider the special case that searching for appointments will find one overlap but it's the appointment you're currently editing, so you can ignore that one.
class Appointment < ActiveRecord::Base
validate :no_overlapping_appointments
def no_overlapping_appointments
overlaps = Appointment.where('start_time <= ? AND end_time >= ?', end_time, start_time)
return if overlaps.empty?
return if overlaps.count == 1 && overlaps.first.id == id
errors.add(:start_time, "This appointment overlaps others")
end
end
class Appointment < ActiveRecord::Base
validate :duration_not_overlap
private
def duration_not_overlap
verify_time(:starttime)
verify_time(:endtime)
end
# attr is :starttime | :endtime
def verify_time(attr)
errors[attr] << 'overlap' if Appointment.where(user_id: user_id, attr => (starttime..endtime)).exists?
end
end

Rails scope filter by date range

There are many questions relate to rails date range problem but mine is a little more complicated.
I have two models: house and booking. A House has_many bookings. A Booking has two attributes in date format: check_in and check_out.
What I want to achieve: Giving a valid date range, show all houses that are available during this range. In detail:
The start date of the range should not be in any booking.
The end date of the range should not be in any booking.
There should not be any booking between the start and the end.
Can this be done using the rails scope?
UPDATE:
I found the code below that can check scope date interval that overlaps.
named_scope :overlapping, lambda { |interval| {
:conditions => ["id <> ? AND (DATEDIFF(start_date, ?) * DATEDIFF(?, end_date)) >= 0", interval.id, interval.end_date, interval.start_date]
}}
How can I transfer this to my problem?
scope :overlapping, (lambda do |start_date, end_date|
House.includes(:bookings).where("bookings.check_in < ? AND bookings.check_out > ?",
start_date, end_date).references(:bookings).uniq
end)
I went ahead and deleted the >= and <= operators in favor of > and < to explicitly show these bookings being outside of the given range, but you can adjust them per your needs!
Update
Changed query to use #includes instead of #joins, since we're querying the attached table.
Yes it is possible to have this query through scope. Put this scope in house model.
scope :overlapping, -> (start_date, end_date) {
includes(:bookings).where('bookings.check_in < ? AND bookings.check_out > ?',
start_date.to_date, end_date.to_date)
}
And call as House.overlapping('2015-07-01', '2015-07-09')

Rails - How to check what the record count of a model 24 hours ago

I want to display the rate of change in the total number of records for a certain model. So I need to know, at any given time, how many there were 24 hours ago, and how many there are now.
It is very likely that a record will be deleted by users, so I can't just use the value of existing records that are older than 24 hours.
Is there a simple way to do this?
Thanks
class Item < ActiveRecord::Base
scope :since, lambda {|time| where("created_at > ?", time) }
scope :during_last, lambda {|time| where("created_at > ?", (Time.now - time)) }
end
Item.since(Time.now - 24.hours).count
Item.since(24.hours.ago).count
Item.during_last(24.hours).count
Well, you could do a count against the column created_at.
def count_rate(model, time=DateTime.now)
current_count = model.count(:conditions => ["created_at < ?", time])
last_24_hours_count = model.count(:conditions => ["created_at < ?", time-24.hours])
current_count - last_24_hours_count
end
This method count the record at a given compared with the last 24 hours.
Well, in case the user has deleted those records back, you would should create another table Rate(date, hour, count) and record every hour by using observer (only after_create).
def after_create
rate = Rate.find_or_initialize_by(Date.today.to_s, Time.now.hour)
rate.increment(:count)
rate.save!
end
In this observer, if it finds the existing record by date and hour, it simply increment the count column. You add the default value to zero on count column as well. Later on, you can query the data from that table by date and hour.

Resources