How to validate overlapping times in Rails with postgresql
Your scope is headed in the right direct, but doesn't cover all your cases.
scope :in_range, -> range {
where('(start_at BETWEEN ? AND ?)', range.first, range.last)
}
In your example, you end up checking start_at BETWEEN 7:05 AND 7:30
, but start_at
on Day 1 is 7:00
, which is outside that range.There are four cases you need to handle:
New range overlaps start
Existing: |------------|
New: |-------|
New range overlaps end
Existing: |------------|
New: |-------|
New range inside existing range
Existing: |------------|
New: |-------|
Existing range inside new range
Existing: |-------|
New: |------------|
Looking, you can see that the first three cases are handled by checking if new_start BETWEEN start_at AND end_at
OR
new_end BETWEEN start_at AND end_at
Then you just need to catch the fourth case by addingOR
start_at BETWEEN new_start AND new_end
You could add a similar check on end_at
for code symmetry, but it's not strictly necessary. Validate time overlap in ruby on rails
validate :no_overlap
def no_overlap
showtimes = Showtime.where(screen_id: screen_id)
overlap = showtimes.select{ |showtime| (start_time - showtime.end_time) * (showtime.start_time - end_time) > 0 }
unless overlap.blank?
raise "Conflicting showings for screen: #{screen_id}"
end
end
Checking overlaps between times
I achieved what I wanted by this method:
validate :not_overlapping_activity
def not_overlapping_activity
overlapping_activity = Activity.where(day_of_week: day_of_week)
.where(pool_zone: pool_zone)
overlapping_activity.each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, 'In this period of time there is some activity.')
end
end
end
If someone has better solution please write. I want to be better and more clever :) Find and sum date ranges with overlapping records in postgresql
demo:db<>fiddle (uses the old data set with the overlapping A-B-part)
Disclaimer: This works for day intervals not for timestamps. The requirement for ts came later.
SELECT
s.acts,
s.sum,
MIN(a.start) as start,
MAX(a.end) as end
FROM (
SELECT DISTINCT ON (acts)
array_agg(name) as acts,
SUM(count)
FROM
activities, generate_series(start, "end", interval '1 day') gs
GROUP BY gs
HAVING cardinality(array_agg(name)) > 1
) s
JOIN activities a
ON a.name = ANY(s.acts)
GROUP BY s.acts, s.sum
generate_series
generates all dates between start and end. So every date an activity exists gets one row with the specificcount
- Grouping all dates, aggregating all existing activities and sum of their counts
HAVING
filters out the dates where only one activity exist- Because there are different days with the same activities we only need one representant: Filter all duplicates with
DISTINCT ON
- Join this result against the original table to get the start and end. (note that "end" is a reserved word in Postgres, you should better find another column name!). It was more comfortable to lose them before but its possible to get these data within the subquery.
- Group this join to get the most early and latest date of each interval.
Here's a version for timestamps:
demo:db<>fiddle
WITH timeslots AS (
SELECT * FROM (
SELECT
tsrange(timepoint, lead(timepoint) OVER (ORDER BY timepoint)),
lead(timepoint) OVER (ORDER BY timepoint) -- 2
FROM (
SELECT
unnest(ARRAY[start, "end"]) as timepoint -- 1
FROM
activities
ORDER BY timepoint
) s
)s WHERE lead IS NOT NULL -- 3
)
SELECT
GREATEST(MAX(start), lower(tsrange)), -- 6
LEAST(MIN("end"), upper(tsrange)),
array_agg(name), -- 5
sum(count)
FROM
timeslots t
JOIN activities a
ON t.tsrange && tsrange(a.start, a.end) -- 4
GROUP BY tsrange
HAVING cardinality(array_agg(name)) > 1
The main idea is to identify possible time slots. So I take every known time (both start and end) and put them into a sorted list. So I can take the first tow known times (17:00 from start A and 18:00 from start B) and check which interval is in it. Then I check it for the 2nd and 3rd, then for 3rd an 4th and so on.In the first timeslot only A fits. In the second from 18-19 also B is fitting. In the next slot 19-20 also C, from 20 to 20:30 A isn't fitting anymore, only B and C. The next one is 20:30-22 where only B fits, finally 22-23 D is added to B and last but not least only D fits into 23-23:30.
So I take this time list and join it agains the activities table where the intervals intersect. After that its only a grouping by time slot and sum up your count.
- this puts both ts of a row into one array whose elements are expanded into one row per element with
unnest
. So I get all times into one column which can be simply ordered - using the lead window function allows to take the value of the next row into the current one. So I can create a timestamp range out of these both values with
tsrange
- This filter is necessary because the last row has no "next value". This creates a
NULL
value which is interpreted bytsrange
as infinity. So this would create an incredible wrong time slot. So we need to filter this row out. - Join the time slots against the original table. The
&&
operator checks if two range types overlap. - Grouping by single time slots, aggregating the names and the count. Filter out the time slots with only one activity by using the
HAVING
clause - A little bit tricky to get the right start and end points. So the start points are either the maximum of the activity start or the beginning of a time slot (which can be get using
lower
). E.g. Take the 20-20:30 slot: It begins 20h but neither B nor C has its starting point there. Similar the end time.
Checking overlaps between times
I achieved what I wanted by this method:
validate :not_overlapping_activity
def not_overlapping_activity
overlapping_activity = Activity.where(day_of_week: day_of_week)
.where(pool_zone: pool_zone)
overlapping_activity.each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, 'In this period of time there is some activity.')
end
end
end
If someone has better solution please write. I want to be better and more clever :) Validation for date/times that overlap
I am not sure if this will work for you but you can try something like this:
errors.add(:date, "and time is not available") unless Booking.where("? = date AND time BETWEEN ? AND ?", date, time - 4.hours, time + 4.hours).count == 0
This will work if your time
is a ruby Time
class object.
Related Topics
Private Messages with Faye and Rails
How to Use C# Style Enumerations in Ruby
Ruby Rspec. Get List of All Test
Using Ruby with Mechanize to Log into a Website
How to Programmatically Remove "Singleton Information" on an Instance to Make It Marshal
Regex to Check Alphanumeric String in Ruby
Axlsx - Formatting Text Within a Cell
Prawn Doesn't Seem to Push Layout Down When Using Repeat(:All)
Including Methods to a Controller from a Plugin
Activemodel::Validations on Anonymous Class
How to Test Helpers Blocks in Sinatra, Using Rspec
Nomethoderror: Undefined Method 'On' for Main:Object
How to Save Data with Has_Many: Through
Obtaining Number of Block Parameters
Define_Method with Predefined Keyword Arguments