4

T-SQL DateTime Question.

I have a set of time ranges. During those time ranges there could be a set of overlapping time ranges that I call 'blocked' out time. The blocked time wouldn't span more than one day. What I want to do is split the time to exclude the blocked out time, basically giving me the time ranges that are not 'blocked'. Its safe to assume that blocked times cant fall outside of the times ranges.

Example: I work 9am to 5pm with a 30 min lunch break at 1pm. I want the result of 2 rows: 9am to 1pm and 1.30pm to 5pm.

As mentioned, I have a set of time ranges so in the above example the working hours may differ on a daily basis and the number of breaks as well as their duration may differ.

I guess in terms of SQL the input parameters would look like this:

declare @timeranges table ( StartDateTime datetime, EndDateTime datetime ) declare @blockedtimes table ( StartDateTime datetime, EndDateTime datetime ) insert into @timeranges select '01 Jan 2009 09:00:00', '01 Jan 2009 17:00:00' union select '02 Feb 2009 10:00:00', '02 Feb 2009 13:00:00' insert into @blockedtimes select '01 Jan 2009 13:00:00', '01 Jan 2009 13:30:00' union select '02 Feb 2009 10:30:00', '02 Feb 2009 11:00:00' union select '02 Feb 2009 12:00:00', '02 Feb 2009 12:30:00' 

The result set would look like this.

Start End --------------------- --------------------- '01 Jan 2009 09:00:00' '01 Jan 2009 13:00:00' '01 Jan 2009 13:30:00' '01 Jan 2009 17:00:00' '02 Feb 2009 10:00:00' '02 Feb 2009 10:30:00' '02 Feb 2009 11:00:00' '02 Feb 2009 12:00:00' '02 Feb 2009 12:30:00' '02 Feb 2009 13:00:00' 

I could do this with a cursor or while loop but if someone could suggest how to do this without iteration that would be great - thanks.

3
  • This has come up before. Commented Aug 13, 2009 at 13:04
  • @Daniel A. White: I can't find it, care to share a link? Commented Aug 13, 2009 at 13:51
  • I couldn't find an answer which is why I posted the question. Commented Aug 13, 2009 at 13:55

4 Answers 4

2

First cut, may have some issues, but I'll keep working on it.
Works for the given data, just need to try additional scenarios

declare @timeranges table ( StartDateTime datetime, EndDateTime datetime ) declare @blockedtimes table ( StartDateTime datetime, EndDateTime datetime ) insert into @timeranges select '01 Jan 2009 09:00:00', '01 Jan 2009 17:00:00' union select '02 Feb 2009 10:00:00', '02 Feb 2009 13:00:00' --union select '03 Feb 2009 10:00:00', '03 Feb 2009 15:00:00' insert into @blockedtimes select '01 Jan 2009 13:00:00', '01 Jan 2009 13:30:00' union select '02 Feb 2009 10:30:00', '02 Feb 2009 11:00:00' union select '02 Feb 2009 12:00:00', '02 Feb 2009 12:30:00' --build an ordered, time range table with an indicator --to determine which ranges are timeranges 'tr' --and which are blockedtimes 'bt' -- declare @alltimes table (row int, rangetype varchar(10), StartDateTime datetime, EndDateTime datetime ) insert into @alltimes select row_number() over (order by a.startdatetime), * from ( select 'tr' as rangetype ,startdatetime, enddatetime from @timeranges union select 'bt' as rangetype ,startdatetime, enddatetime from @blockedtimes )a --what does the data look like -- select * from @alltimes -- -- build up the results select --start time is either the start time of a timerange, or the end of a blockedtime case when at1.rangetype = 'tr' then at1.startdatetime when at1.rangetype = 'bt' then at1.enddatetime end as [Start], case --a time range followed by another time range : end time from the current time range when at1.rangetype = 'tr' and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) = 'tr' then at1.enddatetime --a time range followed by nothing (last record) : end time from the currenttime range when at1.rangetype = 'tr' and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) is null then at1.enddatetime --a time range followed by a blockedtime : end time is start time of blocked time when at1.rangetype = 'tr' and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) = 'bt' then (select top 1 at2.startdatetime from @alltimes at2 where at2.row > at1.row and at2.rangetype = 'bt' order by row) --a blocked time followed by a blockedtime : end time is start time of next blocked time when at1.rangetype = 'bt' and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) = 'bt' then (select top 1 at2.startdatetime from @alltimes at2 where at2.row > at1.row and at2.rangetype = 'bt' order by row) --a blocked time followed by a time range : end time is end time of previous time range when at1.rangetype = 'bt' and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) = 'tr' then (select top 1 at2.enddatetime from @alltimes at2 where at2.row < at1.row and at2.rangetype = 'tr' order by row desc) --a blocked time followed by nothing (last record) : end time is end time of previous time range when at1.rangetype = 'bt' and (select at2.rangetype from @alltimes at2 where at2.row = at1.row+1) is null then (select top 1 at2.enddatetime from @alltimes at2 where at2.row < at1.row and at2.rangetype = 'tr' order by row desc) end as [End] from @alltimes at1 
Sign up to request clarification or add additional context in comments.

Comments

1

I thought I'd share the solution I finally settled on:

Slight adjustment to the temp table in that I've added a StartDate field to both @timeranges and @blockedtimes

declare @timeranges table ( StartDate datetime, StartDateTime datetime, EndDateTime datetime ) declare @blockedtimes table ( StartDate datetime, StartDateTime datetime, EndDateTime datetime ) 

Anyways seems simpler than some of the other answer posted - cheers for everyones help :)

select * from ( -- first SELECT get start boundry select t.StartDateTime s, b.StartDateTime e from @timeranges t, @blockedtimes b where -- same day and blocks overlaps timerange t.StartDate = b.StartDate and (t.StartDateTime <= b.EndDateTime and b.StartDateTime <= t.EndDateTime) and -- the following is the important bit for this SELECT not exists (select 1 from @blockedtimes b2 where b2.StartDate = b.StartDate and b2.StartDateTime < b.StartDateTime) union -- second SELECT get spikes ie middle select b1.EndDateTime s, b2.StartDateTime e from @timeranges t, @blockedtimes b1, @blockedtimes b2 where -- same day and blocks overlaps timerange t.StartDate = b1.StartDate and (t.StartDateTime <= b1.EndDateTime and b1.StartDateTime <= t.EndDateTime) and -- same day and blocks overlaps timerange t.StartDate = b2.StartDate and (t.StartDateTime <= b2.EndDateTime and b2.StartDateTime <= t.EndDateTime) and -- the following is the important bit for this SELECT b1.EndDateTime < b2.StartDateTime union -- third SELECT get end boundry select b.EndDateTime s, t.EndDateTime e from @timeranges t, @blockedtimes b where -- same day and blocks overlaps timerange t.StartDate = b.StartDate and (t.StartDateTime <= b.EndDateTime and b.StartDateTime <= t.EndDateTime) and -- the following is the important bit for this SELECT not exists (select 1 from @blockedtimes b2 where b2.StartDate = b.StartDate and b2.StartDateTime > b.StartDateTime) ) t1 

Comments

0

Here's a solution that should work if two conditions hold for the input data: A) Each blocked time interval falls within a single time range interval. (You said this could be assumed.), and B) Blocked time intervals do not overlap - that is, no time is "doubly blocked" by falling in more than one blocked interval.

 with TB(src,S,E) as ( select 'T', StartDateTime, EndDateTime from @timeranges as T union all select 'B', StartDateTime, EndDateTime from @blockedtimes as B ), TBP(evt,switch,DT,rk) AS ( select src+DT, CHARINDEX(src+DT,'TEBSTSBE')/5 AS OffOn, EventDT, row_number() over ( order by EventDT, CHARINDEX(src+DT,'TEBSTSBE')/5 desc ) as rk from TB UNPIVOT ( EventDT FOR DT in ([S],[E]) ) as U ) select min(DT) as StartDateTime, max(DT) as EndDateTime from TBP group by (rk-1)/2 having min(DT) < max(DT) order by (rk-1)/2; 

How does this work?

It first tags all the datetime values with a 0 or 1 to indicate whether availability ends (0, for EndDateTime values in @timeranges and for StartDateTime values in @blockedtimes) or begins (1, for the other two possibilities) at the particular time. Then the times and tags are put in order by time, tag and numbered with a column rk that uses the row_number function. The tagging could be made more readable with a CASE expression, but CHARINDEX was less to type...

Because of the assumptions, the tag sequence will alternate between 0 and 1: 0,1,0,1,0,1..., with each consecutive (0,1) pair indicating the start and end of an interval of availability. These intervals can be numbered with (rk-1)/2.

The rows are grouped over each interval of availability. The minimum datetime in the group is the start time, and the maximum is the end time, and if these are different, the group represents a non-empty interval that belongs in the result set. Note that for your data, there are no empty intervals, but there would be if two blocked times abutted or if a blocked time ended simultaneously with a time range.

Finally, the results are pivoted for the display format you want.

It's not the easiest code to read, but it might be worth puzzling over. Solutions like this that use row_number and grouping are sometimes handy for solving tricky questions.

Comments

0
SELECT COALESCE(bt.StartDateTime, tr.StartDateTime), bt.EndDateTime FROM @timeranges tr CROSS APPLY ( SELECT bp.StartDateTime, bt.StartDateTime AS EndDateTime FROM ( SELECT StartDateTime FROM @blockedtimes bt WHERE bt.EndDateTime >= tr.StartDateTime AND bt.StartDateTime <= tr.EndDateTime UNION ALL SELECT tr.EndDateTime ) bt OUTER APPLY ( SELECT TOP 1 EndDateTime AS StartDateTime FROM @blockedtimes bti WHERE bti.EndDateTime >= tr.StartDateTime AND bti.StartDateTime <= tr.EndDateTime AND bti.StartDateTime < bt.StartDateTime ORDER BY bti.StartDateTime DESC ) AS bp ) bt 

This solutions relies on the following assumptions:

  • Timeranges never overlap other timeranges
  • Blocked times never overlap other blocked times

3 Comments

This looked like an elegant solution, so I tried to understand how it worked, but I noticed that it doesn't produce the right result - 3rd and 4th results are incorrect :(
OK before I start to look at it- HOW did you come up with this?? Can you give me a heads up on how you managed to arrive at this solution. My head is spinning!
@Kev: You need to come up with both an algorithm and a set-based formula and amalgamate them. Can't explain how, I just see it :) You can read my article on this: explainextended.com/2009/07/12/double-thinking-in-sql

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.