Validate if new range overlaps existing range - ruby-on-rails

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

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

How to add less or equal to condition (`<=`) on rails active record association to select only certain data?

In general, my report shows Masters. But I need to display only one wizard for this algorithm.
This request displays several wizards on a selected date for a report.
def get_masters_name
str = ''
team.masters.each do |m|
str += "#{m.full_name}"
end
return str
end
And I need to display only one master in the report if his start date will be less (but approximately equal) or equal to the date of the report.
This is approximately what this query would look like in SQL:
SELECT * FROM * WHERE team.team_id and report_date = (SELECT MAX(master.date_work) FROM master WHERE master.date_work <= report_date)
report.model:
class Report < ActiveRecord::Base
self.table_name = 'reports'
belongs_to :team, foreign_key: 'teams_id'
def get_masters_name
str = ''
team.masters.each do |m|
str += "#{m.full_name}"
end
return str
end
end
You can add a where clause to filter master records based on reports report_date as additionally you can refactor you code using pluck and join
def get_masters_name
team.masters.where('start_date <= ?', self.report_date)
.order(start_date: :desc)
.limit(1).pluck(:full_name).join(',')
end
Here start_date should be the column of masters and report_date is column of reports table.

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')

The smallest and the largest possible date

I am creating a filtering partial view, where user can pick a from-date and a to-date using a calendar. These dates are used then within model scope to perform SQL Where clause in database query. If a user does not pick one of dates, the default value should be assigned: minimal available date for from and maximal for to.
unless params[:from].blank? and params[:to].blank?
from = begin Date.parse(params[:from]) rescue ??? end
to = begin Date.parse(params[:to]) rescue ??? end
#model_instances = #model_instances.start_end from, to
end
(...)
scope :start_end, -> (start_date, end_date) { where('(:start_date <= "from" AND "from" <= :end_date ) OR' +
'(:start_date <= "to" AND "to" <= :end_date ) OR' +
'("from" <= :start_date AND :end_date <= "to")',
{start_date: start_date, end_date: end_date}) }
The from and to model Date attributes are also database fields in related table.
In Rails, Date class has a family of methods beginning_of (day, week, month, etc.), but there are no methods such beginning_of_time, end_of_time, Date.min, Date.max.
Are there any methods to obtain the minimal and maximal dates?
You could skip the condition on start and end dates if no params is given:
if params[:from].present? and params[:to].present?
#model_instances.start_end(params[:from], params[:to])
end
And then you will get results not depending on dates since no from and/or end dates have been filled.
You could compare ranges of dates to your model's values and setup default values if params not passed.
#setup default if desired, or you can skip if params are blank
from = params[:from] || default_from
to = params[:to] || default_to
#model_instances.start_end(from, to)
and your scope would look something like this if you used date ranges for activerecord
scope :start_end, ->(start_date, end_date){where(from: start_date..end_date, to:start_date..end_date)}

Validate price range overlap

I'm working on an application where I'm entering multiple price ranges. I want to validate the price range to keep it from overlapping with another price range. I know how to check whether two arrays overlaps or not, e.g.,
a = [1.0, 2.0]
b = [2.0, 3.0]
a & b #=> true
I have two fields price_start and price_end, so no price range between these two fields should overlap with another
But here its a range, e.g. $1.0 - $10.0 then the next one $10.1 to $20, how we can implement this? please help! thanks
You can write a custom validation like this:
validates :price_range_must_not_overlap
private
def price_ranges_must_overlap
range = (price_start..price_end)
if self.class.
where('id <> ?', self.id)
where('(price_start BETWEEN :price_start AND :price_end) OR (price_end BETWEEN :price_start AND :price_end)',
{ :price_start => price_start,
:price_end => price_end }).any?
errors.add(:base, "Price range overlaps with an existing price range")(b)
end
end
The finder condition might be extracted into a scope.
Read more about this in the Rails guide: http://guides.rubyonrails.org/active_record_validations.html#custom-methods

Resources