Validate Date in Rails before it hits the database - ruby-on-rails

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

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

Datetime may not intersect other datetime for that user's activity

Might be a somewhat specific situation. And I kinda know how to do it in PHP/MySQL. But I was wondering if there was a faster way to do the following scenario:
A user has activities with a start- and end_date. (The activity starts at 12-10-2013 12:00:00 and ends at 12-10-2013 12:15:00 for example.)
Whenever the user creates a new activity. I want to check all the activities the user is part of(user has_many: activities) and see if none of the dates intersect with the date given for the new activity.
Since I'm pretty new to Rails I really don't know where to start searching for date comparisons and all...
Thanks in advance
An overlap is defined as another activity for which the end date is greater than or equal to the new activity's start date, and for which the start date is less than or equal to the new activity's end date.
Since you only want to detect whether such a record already exists, an appropriate test would be:
if Activity.where("starts_at <= ?" , new_activity_end_date ).
where("ends_at >= ?" , new_activity_start_date).
exists?
You can use regular comparators for date/times in Ruby (ie: >, < and ==).
Something like the following should do what you are looking for:
if current_user.activities.where("(starts_at <= ? AND ends_at >= ?) OR (starts_at >= ? AND starts_at <= ?)", start_datetime, start_datetime, start_datetime, end_datetime).count
# There exist activities that fall between start_datetime and end_datetime
else
# There exist no such activities
end
(starts_at <= start_datetime AND ends_at >= start_datetime) checks whether an event starts before and ends after start_datetime.
(starts_at >= start_datetime AND starts_at <= end_datetime) checks whether an event starts between start_datetime and end_datetime.
We must check that all this three kinds of intersection do not happen:
our activity must not happen within another activity
our activity must not start before another ends
our activity must not ends after another starts
We can do that using something like this:
class Activity < ActiveRecord::Base
validate :must_not_be_intersected
def intersected?
params = {
:start => start_date,
:end => end_date
}
Activity::where('start_date <= :start AND end_date >= :end', params)
.where('(start_date >= :start AND start_date <= :end) OR (end_date >= :start AND end_date <= :end)', params)
.exists?
end
private
def must_not_be_intersected
errors.add :base, 'Other task running on the same period' if intersected?
end
end

Checking a hash for the existence of a matching value

I'm building a school calendar that lists classes ("sections") and displays semester and exam dates for those classes. The calendar is fed a hash that is defined in the Section.rb model:
def get_calendar
calendar = {}
calendar[:semesters] = Semester.where(section_id: self.id)
calendar[:exams] = Exam.where(section_id: self.id)
{ :calendar => calendar }
end
The Semester and Exam objects have a name, start_date and end_date.
When looping through each day of the calendar, how can I check if there's a semester or exam with a start_date or end_date for that day of the loop?
If there is a semester or exam with either a start_date or end_date that matches the calendar date, I would to display the name.
The calendar dates and the start_date and end_date fields all use the Date class (link).
I sincerely appreciate the help. I can clarify the question if needed :)
Thank you!
Maybe you should re-think your hash, and use it this way:
def get_calendar
semesters = self.semesters
exams = self.exams
events = { }
(semesters + exams).each do |event|
start_date = event.start_date.to_s
events[start_date] ||= []
events[start_date] << event
end_date = event.end_date.to_s
events[end_date] ||= []
events[end_date] << event
end
events
end
And test the presence of an event in the loop that constructs the Calendar:
dates_of_calendar.each do |date|
if #events[date.to_s].present?
# well, there is an event happening for this date! treat it as your wishes!
end
# etc...
end
Would the enumberable detect method work? http://ruby-doc.org/core-1.9.3/Enumerable.html#method-i-detect
So as your looping through your condition could be...
semesters.detect{|s| s.begin_date > cal_begin_date ... //more conditions here} &&
exams.detect{|e| e.begin_date > cal_begin_date ... //more conditions here}

Rails filter by date with MetaSearch

I have some records which I show in a view. They have start_date and end_date.
When I access the view, by default I want it only to show records who's dates are not expired.
Expired being defined as:
End date and start date <= Now, and
End date is later than the start date
I then want to have a checkbox that shows all records including the ones that have been expired.
How do I approach this?
In your controller action, you want to have this somewhere:
params[:search] ||= {} # make sure this is instantiated
# filter to show the expired records
if params[:include_expired] # check if the checkbox is checked
params[:search][:end_date_lte] = Date.today # end_date <= Now
params[:search][:start_date_lte] = Date.today # end_date <= Now
end
#records = params[:include_expired] ? RecordClass.where("end_date > start_date").search(params[:search]) : RecordClass.search(params[:search])
You could also bypass all the search things and just use a scope like this:
#records = params[:include_expired] ? RecordClass.expired : RecordClass.all # or whatever logic you need
# scope in the RecordClass
scope :expired, where("end_date > start_date AND end_date <= ?", Date.today)

Resources