Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get classroom available hours between date time range

I'm, using Oracle 11g and I have this problem. I couldn't come up with any ideas to solve it yet.

I have a table with occupied classrooms. What I need to find are the hours available between a datetime range. For example, I have rooms A, B and C, the table of occupied classrooms looks like this:

Classroom        start                 end  
   A         10/10/2013 10:00      10/10/2013 11:30  
   B         10/10/2013 09:15      10/10/2013 10:45  
   B         10/10/2013 14:30      10/10/2013 16:00  

What I need to get is something like this:

with date time range between '10/10/2013 07:00' and '10/10/2013 21:15'

Classroom    avalailable_from        available_to  
   A         10/10/2013 07:00      10/10/2013 10:00  
   A         10/10/2013 11:30      10/10/2013 21:15  
   B         10/10/2013 07:00      10/10/2013 09:15  
   B         10/10/2013 10:45      10/10/2013 14:30  
   B         10/10/2013 16:00      10/10/2013 21:15  
   C         10/10/2013 07:00      10/10/2013 21:15  

Is there a way I can accomplish that with sql or pl/sql?

like image 908
oechegaray Avatar asked Mar 20 '23 21:03

oechegaray


1 Answers

I was looking at a solution similar in concept at least to Wernfried's, but I think it's different enough to post as well. The start is the same idea, first generating the possible time slots, and assuming you're looking at 15-minute windows: I'm using CTEs because I think they're clearer than nested selects, particularly with this many levels.

with date_time_range as (
  select to_date('10/10/2013 07:00', 'DD/MM/YYYY HH24:MI') as date_start,
    to_date('10/10/2013 21:15', 'DD/MM/YYYY HH24:MI') as date_end
  from dual
),
time_slots as (
  select level as slot_num,
    dtr.date_start + (level - 1) * interval '15' minute as slot_start,
    dtr.date_start + level * interval '15' minute as slot_end
  from date_time_range dtr
  connect by level <= (dtr.date_end - dtr.date_start) * (24 * 4) -- 15-minutes
)
select * from time_slots;

This gives you the 57 15-minute slots between the start and end date you specified. The CTE for date_time_range isn't strictly necessary, you could put your dates straight into the time_slots conditions, but you'd have to repeat them and that then introduces a possible failure point (and means binding the same value multiple times, from JDBC or wherever).

Those slots can then be cross-joined to the list of classrooms, which I'm assuming are already in another table, which gives you 171 (3x57) combinations; and those can be compared with existing bookings - once those are eliminated you're left with the 153 15-minute slots that have no booking.

with date_time_range as (...),
time_slots as (...),
free_slots as (
  select c.classroom, ts.slot_num, ts.slot_start, ts.slot_end,
    lag(ts.slot_end) over (partition by c.classroom order by ts.slot_num)
      as lag_end,
    lead(ts.slot_start) over (partition by c.classroom order by ts.slot_num)
      as lead_start
  from time_slots ts
  cross join classrooms c
  left join occupied_classrooms oc on oc.classroom = c.classroom
    and not (oc.occupied_end <= ts.slot_start 
      or oc.occupied_start >= ts.slot_end)
  where oc.classroom is null
)
select * from free_slots;

But then you have to collapse those into contiguous ranges. There are various ways of doing that; here I'm peeking at the previous and next rows to decide if a particular value is the edge of a range:

with date_time_range as (...),
time_slots as (...),
free_slots as (...),
free_slots_extended as (
  select fs.classroom, fs.slot_num,
    case when fs.lag_end is null or fs.lag_end != fs.slot_start
      then fs.slot_start end as slot_start,
    case when fs.lead_start is null or fs.lead_start != fs.slot_end
      then fs.slot_end end as slot_end
  from free_slots fs
)
select * from free_slots_extended
where (fse.slot_start is not null or fse.slot_end is not null);

Now we're down to 12 rows. (The outer where clause eliminates all 141 of the 153 slots from the previous step which are mid-range, since we only care about the edges):

CLASSROOM   SLOT_NUM SLOT_START       SLOT_END       
--------- ---------- ---------------- ----------------
A                  1 2013-10-10 07:00                  
A                 12                  2013-10-10 10:00 
A                 19 2013-10-10 11:30                  
A                 57                  2013-10-10 21:15 
B                  1 2013-10-10 07:00                  
B                  9                  2013-10-10 09:15 
B                 16 2013-10-10 10:45                  
B                 30                  2013-10-10 14:30 
B                 37 2013-10-10 16:00                  
B                 57                  2013-10-10 21:15 
C                  1 2013-10-10 07:00                  
C                 57                  2013-10-10 21:15 

So those represent the edges, but on separate rows, and a final step combines them:

...
select distinct fse.classroom,
  nvl(fse.slot_start, lag(fse.slot_start)
    over (partition by fse.classroom order by fse.slot_num)) as slot_start,
  nvl(fse.slot_end, lead(fse.slot_end)
    over (partition by fse.classroom order by fse.slot_num)) as slot_end
from free_slots_extended fse
where (fse.slot_start is not null or fse.slot_end is not null)

Or putting all that together:

with date_time_range as (
  select to_date('10/10/2013 07:00', 'DD/MM/YYYY HH24:MI') as date_start,
    to_date('10/10/2013 21:15', 'DD/MM/YYYY HH24:MI') as date_end
  from dual
),
time_slots as (
  select level as slot_num,
    dtr.date_start + (level - 1) * interval '15' minute as slot_start,
    dtr.date_start + level * interval '15' minute as slot_end
  from date_time_range dtr
  connect by level <= (dtr.date_end - dtr.date_start) * (24 * 4) -- 15-minutes
),
free_slots as (
  select c.classroom, ts.slot_num, ts.slot_start, ts.slot_end,
    lag(ts.slot_end) over (partition by c.classroom order by ts.slot_num)
      as lag_end,
    lead(ts.slot_start) over (partition by c.classroom order by ts.slot_num)
      as lead_start
  from time_slots ts
  cross join classrooms c
  left join occupied_classrooms oc on oc.classroom = c.classroom
    and not (oc.occupied_end <= ts.slot_start
      or oc.occupied_start >= ts.slot_end)
  where oc.classroom is null
),
free_slots_extended as (
  select fs.classroom, fs.slot_num,
    case when fs.lag_end is null or fs.lag_end != fs.slot_start
      then fs.slot_start end as slot_start,
    case when fs.lead_start is null or fs.lead_start != fs.slot_end
      then fs.slot_end end as slot_end
  from free_slots fs
)
select distinct fse.classroom,
  nvl(fse.slot_start, lag(fse.slot_start)
    over (partition by fse.classroom order by fse.slot_num)) as slot_start,
  nvl(fse.slot_end, lead(fse.slot_end)
    over (partition by fse.classroom order by fse.slot_num)) as slot_end
from free_slots_extended fse
where (fse.slot_start is not null or fse.slot_end is not null)
order by 1, 2;

Which gives:

CLASSROOM SLOT_START       SLOT_END       
--------- ---------------- ----------------
A         2013-10-10 07:00 2013-10-10 10:00 
A         2013-10-10 11:30 2013-10-10 21:15 
B         2013-10-10 07:00 2013-10-10 09:15 
B         2013-10-10 10:45 2013-10-10 14:30 
B         2013-10-10 16:00 2013-10-10 21:15 
C         2013-10-10 07:00 2013-10-10 21:15 

SQL Fiddle.

like image 194
Alex Poole Avatar answered Apr 01 '23 23:04

Alex Poole