How to validate range dates with rails 6 - ruby-on-rails

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

Related

Date overlap unique per user rails

I read this question Date range overlap per user rails
How can you validate this per user using this code below:
def no_reservation_overlap
if (Reservation.overlapping(date_start, date_end).any?)
errors.add(:date_end, 'it overlaps another reservation')
end
end
I used this code below which is the answer but it doesn't validate the conflict inbetween dates
booked june 2 to 3 created another booking 1 to 5 and it created how can i prevent that from happening?
validate :no_reservation_overlap
def no_reservation_overlap
if (Reservation.where("(? BETWEEN date_start AND date_end OR ? BETWEEN date_start AND date_end) AND user_id = ?", self.date_start, self.date_end, self.user_id).any?)
errors.add(:date_end, 'it overlaps another reservation')
end
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

Validate Date in Rails before it hits the database

I need to validate two dates in date time format that come from create new record form. Right now the form has drop downs for year, date, month, hour, minute. In the controller, I need to validate that the start date is not greater than end date and it will not let me compare it using the params[:start_date] > params[:end_date].
How can I properly validate that the start date is not larger than the end date when adding a new record to the database, I should be doing this in the model but I cannot figure out how you do it. Does anyone here has any examples I can look from?
Add custom validation to your model to verify that the start date is less than the end date. Something like this would work:
# app/models/my_model.rb
validate :dates_in_order
def dates_in_order
errors.add(:start_date, "must be before end time") unless start_date < end_date
end
#some_model.rb
before_create -> {
errors.add(:base, "Start date cannot be later than end date.") if start_date > end_date
}
Not what you're asking for, but may also be a way to handle this. People sometimes don't read the labels so closely.
before_create :confirm_dates_in_order
def confirm_dates_in_order
start_date, end_date = end_date, start_date if start_date > end_date
end

Rails calculate and set a field value in the controller

I have an event table that contains start_at and ends_at. It also have a boolean for allday.
If the event is allday, then the user enters a field called hours. But, if they enter the start_at and ends_at, I'ld like to calculate and save in the hours field.
In English (not Ruby):
If event.allday = > true
hours = ends_at - starts_at
This way, I wouldn't have to calculate it during all the different display pages I have and it would be easier to sum.
OR - would this logic go into the model?
Thanks!
UPDATE-----------
I tried putting this into the model:
before_save :calculate_hours
def calculate_hours
if self.allday != true
self.hours = ((self.ends_at - self.starts_at)/ 3600).to_f.round(2)
end
end
Business logic always belongs in the model.
class Event < ActiveRecord::Base
before_validation :set_hours
private
def set_hours
if allday?
self.hours = ends_at - starts_at
end
end
end

Resources