1

I am using the awesome stored procedure created by Aaron Bertrand here as a start since it almost does what I need. Sadly my SQL skills are not as good as his so I need some help here. What I need to do is to find free time slots between a start and end date constrained by the time of work of a specific user. My requirements are a little different from OP:

  1. I only need the free slots from 1 user.
  2. A user could have different work time on each day of the week.
  3. A user could not work some days of the week
  4. A user can have a break, an interval when he cannot do any work
  5. There is no recursive events

So, the new table structure would be something like this:

CREATE TABLE [dbo].[event]
(
    [event_id] [int] IDENTITY(1,1) NOT NULL,
    [event_start] [datetime] NULL,
    [event_end] [datetime] NULL,
    [UserId] [int] NULL
) ON [PRIMARY]

CREATE TABLE [dbo].[Users]
(
    [UserID] [int] IDENTITY(1,1) NOT NULL,
    [Username] [varchar](32) NULL,
    [Sunday] [bit] NULL,
    [Monday] [bit] NULL,
    [Tuesday] [bit] NULL,
    [Wednesday] [bit] NULL,
    [Thursday] [bit] NULL,
    [Friday] [bit] NULL,
    [Saturday] [bit] NULL,
    [WorkStartSunday] [time](7) NULL,
    [WorkEndSunday] [time](7) NULL,
    [IntervalStartSunday] [time](7) NULL,
    [IntervalEndSunday] [time](7) NULL,
    [WorkStartMonday] [time](7) NULL,
    [WorkEndMonday] [time](7) NULL,
    [IntervalStartMonday] [time](7) NULL,
    [IntervalEndMonday] [time](7) NULL,
    [WorkStartTuesday] [time](7) NULL,
    [WorkEndTuesday] [time](7) NULL,
    [IntervalStartTuesday] [time](7) NULL,
    [IntervalEndTuesday] [time](7) NULL,
    [WorkStartWednesday] [time](7) NULL,
    [WorkEndWednesday] [time](7) NULL,
    [IntervalStartWednesday] [time](7) NULL,
    [IntervalEndWednesday] [time](7) NULL,
    [WorkStartThursday] [time](7) NULL,
    [WorkEndThursday] [time](7) NULL,
    [IntervalStartThursday] [time](7) NULL,
    [IntervalEndThursday] [time](7) NULL,
    [WorkStartFriday] [time](7) NULL,
    [WorkEndFriday] [time](7) NULL,
    [IntervalStartFriday] [time](7) NULL,
    [IntervalEndFriday] [time](7) NULL,
    [WorkStartSaturday] [time](7) NULL,
    [WorkEndSaturday] [time](7) NULL,
    [IntervalStartSaturday] [time](7) NULL,
    [IntervalEndSaturday] [time](7) NULL,
 CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED 
(
    [UserID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

And some sample data will be this:

INSERT [dbo].[Users] ([UserID], [Username], [Sunday], [Monday], [Tuesday], [Wednesday], [Thursday], [Friday], [Saturday], [WorkStartSunday], [WorkEndSunday], [IntervalStartSunday], [IntervalEndSunday], [WorkStartMonday], [WorkEndMonday], [IntervalStartMonday], [IntervalEndMonday], [WorkStartTuesday], [WorkEndTuesday], [IntervalStartTuesday], [IntervalEndTuesday], [WorkStartWednesday], [WorkEndWednesday], [IntervalStartWednesday], [IntervalEndWednesday], [WorkStartThursday], [WorkEndThursday], [IntervalStartThursday], [IntervalEndThursday], [WorkStartFriday], [WorkEndFriday], [IntervalStartFriday], [IntervalEndFriday], [WorkStartSaturday], [WorkEndSaturday], [IntervalStartSaturday], [IntervalEndSaturday]) 
VALUES (1, N'User A', 0, 1, 1, 0, 0, 0, 0, NULL, NULL, NULL, NULL, CAST(N'08:00:00' AS Time), CAST(N'18:00:00' AS Time), CAST(N'12:00:00' AS Time), CAST(N'13:00:00' AS Time), CAST(N'10:00:00' AS Time), CAST(N'15:00:00' AS Time), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL)

I need to be able to pass the duration of the event, the start/end date and the UserId. All other data comes from the user table. I was able to resolve requirement 3 and partially 2. 2 is my biggest problem - I can start at the right time but do not know how I can make it to end at the right time. 4 I did not even try yet :/

Warning: This is the first time ever that I play with such a complex stored procedures so it is not beautiful what I done to it (sorry, Aaron). I am sure this will make DBAs cry. Here is what I got until now (it is in the form of a SQL because it is easier to test):

--parameters
declare @UserId int =  1
declare @Duration INT = 30;
declare @StartDate SMALLDATETIME = '12-22-2014';
declare @EndDate SMALLDATETIME = '12-25-2014';

--user configurations
declare @Sunday bit = 0;
declare @Monday bit = 0;
declare @Tuesday bit = 0;
declare @Wednesday bit = 0;
declare @Thursday bit = 0;
declare @Friday bit = 0;
declare @Saturday bit = 0;

declare @StartSunday time;
declare @EndSunday time;

declare @StartMonday time;
declare @EndMonday time;
declare @StartIntervalMonday time;
declare @EndIntervalMonday time;

declare @StartTuesday time;
declare @EndTuesday time;
declare @StartIntervalTuesday time;
declare @EndIntervalTuesday time;

declare @StartWednesday time;
declare @EndWednesday time;

declare @StartThursday time;
declare @EndThursday time;

declare @StartFriday time;
declare @EndFriday time;

declare @StartSaturday time;
declare @EndSaturday time;

--load user configurations
select 
   @Sunday = us.Sunday,
   @Monday = us.Monday,
   @Tuesday = us.Tuesday,
   @Wednesday = us.Wednesday,
   @Thursday = us.Thursday,
   @Friday = us.Friday,
   @Saturday = us.Saturday,
   @StartSunday = us.WorkStartMonday,
   @EndSunday = us.WorkEndSunday,
   @StartMonday = us.WorkStartMonday,
   @EndMonday = us.WorkEndMonday,
   @StartIntervalMonday = us.IntervalStartMonday,
   @EndIntervalMonday = us.IntervalEndMonday,
   @StartTuesday = us.WorkStartTuesday,
   @EndTuesday = us.WorkEndTuesday,
   @StartIntervalTuesday = us.IntervalStartTuesday,
   @EndIntervalTuesday = us.IntervalEndTuesday,
   @StartWednesday = us.WorkStartWednesday,
   @EndWednesday = us.WorkEndWednesday,
   @StartThursday = us.WorkStartThursday,
   @EndThursday = us.WorkEndThursday,
   @StartFriday = us.WorkStartFriday,
   @EndFriday = us.WorkEndThursday,
   @StartSaturday = us.WorkStartSaturday,
   @EndSaturday = us.WorkEndThursday
from 
   Users us
where 
   us.UserId = @UserID

;WITH dRange(d) AS
(

    SELECT TOP (DATEDIFF(DAY, @StartDate, @EndDate)+1) 
    DATEADD(DAY, n-1, @StartDate)
    FROM (SELECT n = ROW_NUMBER() OVER (ORDER BY [object_id])
        FROM sys.objects) AS x 

    where DATEADD(DAY, n-1, @StartDate) >= @StartDate
    and DATEADD(DAY, n-1, @StartDate) <= @EndDate

    and DATEPART(WEEKDAY,DATEADD(DAY, n-1, @StartDate)) in 
    (
        IIF(@Sunday =1,1,null),
        IIF(@Monday =1,2,null),
        IIF(@Tuesday =1,3,null),
        IIF(@Wednesday =1,4,null),
        IIF(@Thursday =1,5,null),
        IIF(@Friday =1,6,null),
        IIF(@Saturday=1,7,null)
        )

    ), possible(ds, de) AS
(
    --with this horrendous thing I set the start time for each weekday
    SELECT          
    CASE 
    WHEN DATEPART(WEEKDAY,dRange.d) = 1 THEN DATEADD(MINUTE, @Duration*rn, DATEADD(HOUR, datepart(HOUR, @StartSunday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 2 THEN DATEADD(MINUTE, @Duration*rn, DATEADD(HOUR, datepart(HOUR, @StartMonday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 3 THEN DATEADD(MINUTE, @Duration*rn, DATEADD(HOUR, datepart(HOUR, @StartTuesday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 4 THEN DATEADD(MINUTE, @Duration*rn, DATEADD(HOUR, datepart(HOUR, @StartWednesday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 5 THEN DATEADD(MINUTE, @Duration*rn, DATEADD(HOUR, datepart(HOUR, @StartThursday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 6 THEN DATEADD(MINUTE, @Duration*rn, DATEADD(HOUR, datepart(HOUR, @StartFriday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 7 THEN DATEADD(MINUTE, @Duration*rn, DATEADD(HOUR, datepart(HOUR, @StartSaturday), dRange.d)) 
    END,

    CASE 
    WHEN DATEPART(WEEKDAY,dRange.d) = 1 THEN DATEADD(MINUTE, @Duration*rn + @Duration, DATEADD(HOUR, datepart(HOUR, @StartSunday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 2 THEN DATEADD(MINUTE, @Duration*rn + @Duration, DATEADD(HOUR, datepart(HOUR, @StartMonday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 3 THEN DATEADD(MINUTE, @Duration*rn + @Duration, DATEADD(HOUR, datepart(HOUR, @StartTuesday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 4 THEN DATEADD(MINUTE, @Duration*rn + @Duration, DATEADD(HOUR, datepart(HOUR, @StartWednesday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 5 THEN DATEADD(MINUTE, @Duration*rn + @Duration, DATEADD(HOUR, datepart(HOUR, @StartThursday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 6 THEN DATEADD(MINUTE, @Duration*rn + @Duration, DATEADD(HOUR, datepart(HOUR, @StartFriday), dRange.d)) 
    WHEN DATEPART(WEEKDAY,dRange.d) = 7 THEN DATEADD(MINUTE, @Duration*rn + @Duration, DATEADD(HOUR, datepart(HOUR, @StartSaturday), dRange.d)) 
    END

    FROM        
    --but here is the problem - if I understood it correctly, this select limits the day events but I was not able to do a CASE here since I do not know how to access the date
    --@StartMonday/@EndMonday should change to reflect the specific day of the week

    (SELECT TOP ((DATEDIFF(HOUR,@StartMonday,@EndMonday)*60)/@Duration) rn = ROW_NUMBER() OVER (ORDER BY [object_id])-1 FROM sys.objects)               
    AS x

    CROSS JOIN dRange
    )    

SELECT p.ds, p.de 
FROM possible AS p 

where not exists
(
    select 1 from 
    (
        select UserId,event_start,event_end
        from event

        where event_start >= @StartDate
        and event_start < DATEADD(DAY, 1, @EndDate)
        ) as sub

    WHERE sub.UserId IN 
    (
        SELECT UserId FROM Users AS m
        WHERE UserId = @UserId
        AND (p.de > sub.event_start AND p.ds < sub.event_end)
        )        
    )
order by ds

It is obvious that different weekdays (with different star/end) should return a different number of rows but I do not know how to do it on the select top. At this point I still do not know how will I resolve the Interval problem but maybe this is another question, or maybe I can ignore them at client side. Help?

Edit 1

THe problem is that the TOP clause should limit to the number of hours of the specific week day.

If I pass to start/end the date 12-22-2014 and put (SELECT TOP ((DATEDIFF(HOUR,@StartMonday,@EndMonday)*60)/@Duration), it works perfectly. If I pass to start/end the date 12-23-2014 and put (SELECT TOP ((DATEDIFF(HOUR,@StartTuesday,@EndTuesday)*60)/@Duration), it works perfectly.

But if I pass start 12-22-2014 and end 12-23-2014, it does not work because I do not know hot to do a CASE there. Here is the results:

The configuration says:

WorkStartMonday WorkEndMonday   WorkStartTuesday    WorkEndTuesday
08:00           18:00           10:00               15:00

With start/end at the same day (22/12/2014, Duration = 60) the result is:

2014-12-22 08:00:00 - 2014-12-22 09:00:00
2014-12-22 09:00:00 - 2014-12-22 10:00:00
2014-12-22 10:00:00 - 2014-12-22 11:00:00
2014-12-22 11:00:00 - 2014-12-22 12:00:00
2014-12-22 12:00:00 - 2014-12-22 13:00:00
2014-12-22 13:00:00 - 2014-12-22 14:00:00
2014-12-22 14:00:00 - 2014-12-22 15:00:00
2014-12-22 15:00:00 - 2014-12-22 16:00:00
2014-12-22 16:00:00 - 2014-12-22 17:00:00
2014-12-22 17:00:00 - 2014-12-22 18:00:00

With start/end on 23/12/2014, the result is also correct:

2014-12-23 10:00:00 2014-12-23 11:00:00
2014-12-23 11:00:00 2014-12-23 12:00:00
2014-12-23 12:00:00 2014-12-23 13:00:00
2014-12-23 13:00:00 2014-12-23 14:00:00
2014-12-23 14:00:00 2014-12-23 15:00:00

This happens because the TOP clause limits to the total hours of each weekday. If I have more than one day I should do a case on that TOP condition but how can I access the "date"?

Community
  • 1
  • 1
Leo
  • 7,379
  • 6
  • 28
  • 44
  • 1
    The part you commented with `but here is the problem...` is a number generator, other than that this seems to suffer from [the XY problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) and I don't really understand what your base problem is – Serpiton Dec 27 '14 at 10:04
  • I added an Edit 1 to explain better the problem. It is clearer now? – Leo Dec 27 '14 at 19:29
  • I already understood that is not functioning the way you want, what I didn't understood are the requirements, those five point at the start of the question are not enought for me to work on a solution, and I don't want to go to a link to read it, make the post comprehensive. – Serpiton Dec 27 '14 at 22:15
  • @Serpiton, ok, I got it. I added my intent: "What I need to do is to find free time slots between a start and end date constrained by the time of work of a specific user.". Is this clear enough? English is not my native language so a little of the confusion may be it. – Leo Dec 27 '14 at 22:44
  • It looks like you want to generate the date info from the weird bit/time pattern on the user table, correct? then you want to check against the event table versus the scheduled work data and find times for a new meeting/event. – Kevin Cook Dec 29 '14 at 18:39
  • @KevinCook the user table define the days of the week the user work (sunday, monday, etc (0 = do not work / 1 = work) and the time he start/ends on each day. I only have a table where I record the actual events and the procedure should return all the possibilities between the dates "start" and "end" so I would return only the slots that is not on the events table (aka the free ones). My problem here is to correctly generate this "possibilities" data. – Leo Dec 29 '14 at 18:45

2 Answers2

1

Ok lets make the test data:

DECLARE @Users TABLE
(
    [UserID] [int],
    [Username] [varchar](32) NULL,
    [Sunday] [bit] NULL,
    [Monday] [bit] NULL,
    [Tuesday] [bit] NULL,
    [Wednesday] [bit] NULL,
    [Thursday] [bit] NULL,
    [Friday] [bit] NULL,
    [Saturday] [bit] NULL,
    [WorkStartSunday] [time](7) NULL,
    [WorkEndSunday] [time](7) NULL,
    [IntervalStartSunday] [time](7) NULL,
    [IntervalEndSunday] [time](7) NULL,
    [WorkStartMonday] [time](7) NULL,
    [WorkEndMonday] [time](7) NULL,
    [IntervalStartMonday] [time](7) NULL,
    [IntervalEndMonday] [time](7) NULL,
    [WorkStartTuesday] [time](7) NULL,
    [WorkEndTuesday] [time](7) NULL,
    [IntervalStartTuesday] [time](7) NULL,
    [IntervalEndTuesday] [time](7) NULL,
    [WorkStartWednesday] [time](7) NULL,
    [WorkEndWednesday] [time](7) NULL,
    [IntervalStartWednesday] [time](7) NULL,
    [IntervalEndWednesday] [time](7) NULL,
    [WorkStartThursday] [time](7) NULL,
    [WorkEndThursday] [time](7) NULL,
    [IntervalStartThursday] [time](7) NULL,
    [IntervalEndThursday] [time](7) NULL,
    [WorkStartFriday] [time](7) NULL,
    [WorkEndFriday] [time](7) NULL,
    [IntervalStartFriday] [time](7) NULL,
    [IntervalEndFriday] [time](7) NULL,
    [WorkStartSaturday] [time](7) NULL,
    [WorkEndSaturday] [time](7) NULL,
    [IntervalStartSaturday] [time](7) NULL,
    [IntervalEndSaturday] [time](7) NULL
);

INSERT INTO @Users 
([UserID], [Username], [Sunday], [Monday], [Tuesday], [Wednesday], [Thursday], [Friday], [Saturday], [WorkStartSunday], [WorkEndSunday], [IntervalStartSunday], [IntervalEndSunday], [WorkStartMonday], [WorkEndMonday], [IntervalStartMonday], [IntervalEndMonday], [WorkStartTuesday], [WorkEndTuesday], [IntervalStartTuesday], [IntervalEndTuesday], [WorkStartWednesday], [WorkEndWednesday], [IntervalStartWednesday], [IntervalEndWednesday], [WorkStartThursday], [WorkEndThursday], [IntervalStartThursday], [IntervalEndThursday], [WorkStartFriday], [WorkEndFriday], [IntervalStartFriday], [IntervalEndFriday], [WorkStartSaturday], [WorkEndSaturday], [IntervalStartSaturday], [IntervalEndSaturday]) 
VALUES 
(1, 'User A', 0, 1, 1, 0, 0, 0, 0, NULL, NULL, NULL, NULL, CAST('08:00:00' AS Time), CAST('18:00:00' AS Time), CAST('12:00:00' AS Time), CAST('13:00:00' AS Time), CAST('10:00:00' AS Time), CAST('15:00:00' AS Time), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL)

Now that we have the user record created, lets make a numbers table and set the beginning of the date range we are creating:

DECLARE @yearstart DATETIME = '01/01/2014'
DECLARE @number_of_numbers INT = 100000;
;WITH
    a AS (SELECT 1 AS i UNION ALL SELECT 1),
    b AS (SELECT 1 AS i FROM a AS x, a AS y),
    c AS (SELECT 1 AS i FROM b AS x, b AS y),
    d AS (SELECT 1 AS i FROM c AS x, c AS y),
    e AS (SELECT 1 AS i FROM d AS x, d AS y),
    f AS (SELECT 1 AS i FROM e AS x, e AS y),
    numbers AS 
(
    SELECT TOP(@number_of_numbers)
    ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS number
    FROM f
)

Using our numbers table, lets make some days to check against:

, mydays AS
(
    SELECT DATEADD(DAY, number, @yearstart) CheckDate
    FROM numbers
)

Now using the user table, we are going to make a all of the working days of the week for the user, based on whether the flag is set, and the starting date / time and ending date / time

, workdays AS
(
    SELECT 
        m.CheckDate + CAST(u.WorkStartSunday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndSunday as datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 1 AND u.Sunday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartMonday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndMonday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 2 AND u.Monday = 1)
        UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartTuesday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndTuesday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 3 AND u.Tuesday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartWednesday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndWednesday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 4 AND u.Wednesday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartThursday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndThursday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 5 AND u.Thursday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartFriday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndFriday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 6 AND u.Friday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartSaturday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndSaturday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 7 AND u.Saturday = 1)
)

Now lets look at the data we generated:

SELECT w.WorkStart, w.WorkEnd, DATENAME(WEEKDAY, w.WorkStart) DayOfWeek, w.UserID, w.Username
FROM workdays w
where w.WorkStart >= '12/01/2014' AND w.WorkEnd < '01/01/2015'

Here is the output:

WorkStart               WorkEnd                 DayOfWeek   UserID  Username
2014-12-01 08:00:00.000 2014-12-01 18:00:00.000 Monday  1   User A
2014-12-02 10:00:00.000 2014-12-02 15:00:00.000 Tuesday 1   User A
2014-12-08 08:00:00.000 2014-12-08 18:00:00.000 Monday  1   User A
2014-12-09 10:00:00.000 2014-12-09 15:00:00.000 Tuesday 1   User A
2014-12-15 08:00:00.000 2014-12-15 18:00:00.000 Monday  1   User A
2014-12-16 10:00:00.000 2014-12-16 15:00:00.000 Tuesday 1   User A
2014-12-22 08:00:00.000 2014-12-22 18:00:00.000 Monday  1   User A
2014-12-23 10:00:00.000 2014-12-23 15:00:00.000 Tuesday 1   User A
2014-12-29 08:00:00.000 2014-12-29 18:00:00.000 Monday  1   User A
2014-12-30 10:00:00.000 2014-12-30 15:00:00.000 Tuesday 1   User A

You can do the same thing for the intervals (I guess the interval is the lunch break?), and then join against them to find the work times available, and then join against the events to see which work times you can create an event against.

Kevin Cook
  • 1,922
  • 1
  • 15
  • 16
  • this is based on a default where Sunday is the first day of the week, change to suit your system settings. – Kevin Cook Dec 29 '14 at 19:55
  • Kevin, thanks for your help! I think my question is confusing (sorry). You resolved the time limit thing in a very elegant way but my main problem is that I need to return all free blocks of time with the specific duration (a parameter) of that user. Example, `return all blocks of 30 minutes from the user A`. The return should be like that on my edit 1. After I have all the "possible slots", I can join with the events table to find the free ones. You solution may be a better start to resolve my problem... but I still cannot see how :/ – Leo Dec 30 '14 at 03:35
1

Ok, this answer builds on the last one, we're going to add the lunch intervals and the events and going to look at a date range for events.

First lets make some test data:

DECLARE @event TABLE
(
    [event_id] [int] IDENTITY(1,1),
    [event_start] [datetime],
    [event_end] [datetime],
    [UserId] [int]
)

INSERT INTO @event
( event_start, event_end, UserId )
VALUES
('12/01/2014 09:00:00', '12/01/2014 09:30:00', 1),
('12/01/2014 09:30:00', '12/01/2014 10:30:00', 1),
('12/01/2014 11:00:00', '12/01/2014 12:00:00', 1),
('12/01/2014 13:00:00', '12/01/2014 16:30:00', 1);

DECLARE @Users TABLE
(
    [UserID] [int],
    [Username] [varchar](32) NULL,
    [Sunday] [bit] NULL,
    [Monday] [bit] NULL,
    [Tuesday] [bit] NULL,
    [Wednesday] [bit] NULL,
    [Thursday] [bit] NULL,
    [Friday] [bit] NULL,
    [Saturday] [bit] NULL,
    [WorkStartSunday] [time](7) NULL,
    [WorkEndSunday] [time](7) NULL,
    [IntervalStartSunday] [time](7) NULL,
    [IntervalEndSunday] [time](7) NULL,
    [WorkStartMonday] [time](7) NULL,
    [WorkEndMonday] [time](7) NULL,
    [IntervalStartMonday] [time](7) NULL,
    [IntervalEndMonday] [time](7) NULL,
    [WorkStartTuesday] [time](7) NULL,
    [WorkEndTuesday] [time](7) NULL,
    [IntervalStartTuesday] [time](7) NULL,
    [IntervalEndTuesday] [time](7) NULL,
    [WorkStartWednesday] [time](7) NULL,
    [WorkEndWednesday] [time](7) NULL,
    [IntervalStartWednesday] [time](7) NULL,
    [IntervalEndWednesday] [time](7) NULL,
    [WorkStartThursday] [time](7) NULL,
    [WorkEndThursday] [time](7) NULL,
    [IntervalStartThursday] [time](7) NULL,
    [IntervalEndThursday] [time](7) NULL,
    [WorkStartFriday] [time](7) NULL,
    [WorkEndFriday] [time](7) NULL,
    [IntervalStartFriday] [time](7) NULL,
    [IntervalEndFriday] [time](7) NULL,
    [WorkStartSaturday] [time](7) NULL,
    [WorkEndSaturday] [time](7) NULL,
    [IntervalStartSaturday] [time](7) NULL,
    [IntervalEndSaturday] [time](7) NULL
);

INSERT INTO @Users 
([UserID], [Username], [Sunday], [Monday], [Tuesday], [Wednesday], [Thursday], [Friday], [Saturday], [WorkStartSunday], [WorkEndSunday], [IntervalStartSunday], [IntervalEndSunday], [WorkStartMonday], [WorkEndMonday], [IntervalStartMonday], [IntervalEndMonday], [WorkStartTuesday], [WorkEndTuesday], [IntervalStartTuesday], [IntervalEndTuesday], [WorkStartWednesday], [WorkEndWednesday], [IntervalStartWednesday], [IntervalEndWednesday], [WorkStartThursday], [WorkEndThursday], [IntervalStartThursday], [IntervalEndThursday], [WorkStartFriday], [WorkEndFriday], [IntervalStartFriday], [IntervalEndFriday], [WorkStartSaturday], [WorkEndSaturday], [IntervalStartSaturday], [IntervalEndSaturday]) 
VALUES 
(1, 'User A', 0, 1, 1, 0, 0, 0, 0, NULL, NULL, NULL, NULL, CAST('08:00:00' AS Time), CAST('18:00:00' AS Time), CAST('12:00:00' AS Time), CAST('13:00:00' AS Time), CAST('10:00:00' AS Time), CAST('15:00:00' AS Time), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL)

Once again, I am going to create a numbers table, now lets set our data range, and I am also going to generate time slots based on a 30 minute duration.

DECLARE @yearstart DATETIME = '01/01/2014'
DECLARE @duration_minutes INT = 30
DECLARE @number_of_numbers INT = 100000;
;WITH
    a AS (SELECT 1 AS i UNION ALL SELECT 1),
    b AS (SELECT 1 AS i FROM a AS x, a AS y),
    c AS (SELECT 1 AS i FROM b AS x, b AS y),
    d AS (SELECT 1 AS i FROM c AS x, c AS y),
    e AS (SELECT 1 AS i FROM d AS x, d AS y),
    f AS (SELECT 1 AS i FROM e AS x, e AS y),
    numbers AS 
(
    SELECT TOP(@number_of_numbers)
    (ROW_NUMBER() OVER (ORDER BY (SELECT NULL))) - 1 AS number
    FROM f
)
, mydays AS
(
    SELECT DATEADD(DAY, number, @yearstart) CheckDate
    FROM numbers
)
, myduration AS
(
    SELECT DATEADD(MINUTE, number * @duration_minutes, @yearstart) CheckMinute
    FROM numbers
)
, workdays AS
(
    SELECT 
        m.CheckDate + CAST(u.WorkStartSunday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndSunday as datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 1 AND u.Sunday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartMonday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndMonday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 2 AND u.Monday = 1)
        UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartTuesday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndTuesday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 3 AND u.Tuesday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartWednesday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndWednesday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 4 AND u.Wednesday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartThursday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndThursday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 5 AND u.Thursday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartFriday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndFriday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 6 AND u.Friday = 1)
    UNION
    SELECT 
        m.CheckDate + CAST(u.WorkStartSaturday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.WorkEndSaturday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 7 AND u.Saturday = 1)
),
intervals AS
(
    SELECT 
        m.CheckDate + CAST(u.IntervalStartSunday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.IntervalEndSunday as datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 1 AND u.Sunday = 1)
        AND u.IntervalStartSunday IS NOT NULL
    UNION
    SELECT 
        m.CheckDate + CAST(u.IntervalStartMonday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.IntervalEndMonday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 2 AND u.Monday = 1)
        AND u.IntervalStartMonday IS NOT NULL
    UNION
    SELECT 
        m.CheckDate + CAST(u.IntervalStartTuesday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.IntervalEndTuesday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 3 AND u.Tuesday = 1)
        AND u.IntervalStartTuesday IS NOT NULL
    UNION
    SELECT 
        m.CheckDate + CAST(u.IntervalStartWednesday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.IntervalEndWednesday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 4 AND u.Wednesday = 1)
        AND u.IntervalStartWednesday IS NOT NULL
    UNION
    SELECT 
        m.CheckDate + CAST(u.IntervalStartThursday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.IntervalEndThursday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 5 AND u.Thursday = 1)
        AND u.IntervalStartThursday IS NOT NULL
    UNION
    SELECT 
        m.CheckDate + CAST(u.IntervalStartFriday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.IntervalEndFriday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 6 AND u.Friday = 1)
        AND u.IntervalStartFriday IS NOT NULL
    UNION
    SELECT 
        m.CheckDate + CAST(u.IntervalStartSaturday AS datetime) AS WorkStart,
        m.CheckDate + CAST(u.IntervalEndSaturday AS datetime) AS WorkEnd,
        u.UserID,
        u.Username
    FROM mydays m
    INNER JOIN @Users u
        ON (DATEPART(WEEKDAY, m.CheckDate ) = 7 AND u.Saturday = 1)
        AND u.IntervalStartSaturday IS NOT NULL
)
, timeslots AS
(
    SELECT m.CheckMinute AS StartSlot,
        LEAD(m.CheckMinute) OVER (ORDER BY m.CheckMinute) AS EndSlot
    FROM myduration m
)

Now I also created the intervals (lunch hour data) and our final output will be a list of time slots and whether it is open, event, or lunch for a date range:

select 
    t.StartSlot,
    t.EndSlot,
    CASE WHEN i.WorkStart IS NULL AND e.event_start IS NULL THEN 'Open' 
        WHEN i.WorkStart IS NOT NULL THEN 'Lunch'
        ELSE 'Event'
    END AS IType
from timeslots t
INNER JOIN workdays w
    ON t.StartSlot >= w.WorkStart 
    AND t.StartSlot < w.WorkEnd
LEFT JOIN intervals i
    ON t.StartSlot >= i.WorkStart
    AND t.StartSlot < i.WorkEnd
LEFT JOIN @event e
    ON t.StartSlot >= e.event_start
    AND t.StartSlot < e.event_end
WHERE w.WorkStart >= '12/01/2014'
    AND w.WorkEnd < '12/04/2014'
ORDER BY t.StartSlot

Here is the output:

StartSlot               EndSlot                 IType
2014-12-01 08:00:00.000 2014-12-01 08:30:00.000 Open
2014-12-01 08:30:00.000 2014-12-01 09:00:00.000 Open
2014-12-01 09:00:00.000 2014-12-01 09:30:00.000 Event
2014-12-01 09:30:00.000 2014-12-01 10:00:00.000 Event
2014-12-01 10:00:00.000 2014-12-01 10:30:00.000 Event
2014-12-01 10:30:00.000 2014-12-01 11:00:00.000 Open
2014-12-01 11:00:00.000 2014-12-01 11:30:00.000 Event
2014-12-01 11:30:00.000 2014-12-01 12:00:00.000 Event
2014-12-01 12:00:00.000 2014-12-01 12:30:00.000 Lunch
2014-12-01 12:30:00.000 2014-12-01 13:00:00.000 Lunch
2014-12-01 13:00:00.000 2014-12-01 13:30:00.000 Event
2014-12-01 13:30:00.000 2014-12-01 14:00:00.000 Event
2014-12-01 14:00:00.000 2014-12-01 14:30:00.000 Event
2014-12-01 14:30:00.000 2014-12-01 15:00:00.000 Event
2014-12-01 15:00:00.000 2014-12-01 15:30:00.000 Event
2014-12-01 15:30:00.000 2014-12-01 16:00:00.000 Event
2014-12-01 16:00:00.000 2014-12-01 16:30:00.000 Event
2014-12-01 16:30:00.000 2014-12-01 17:00:00.000 Open
2014-12-01 17:00:00.000 2014-12-01 17:30:00.000 Open
2014-12-01 17:30:00.000 2014-12-01 18:00:00.000 Open
2014-12-02 10:00:00.000 2014-12-02 10:30:00.000 Open
2014-12-02 10:30:00.000 2014-12-02 11:00:00.000 Open
2014-12-02 11:00:00.000 2014-12-02 11:30:00.000 Open
2014-12-02 11:30:00.000 2014-12-02 12:00:00.000 Open
2014-12-02 12:00:00.000 2014-12-02 12:30:00.000 Open
2014-12-02 12:30:00.000 2014-12-02 13:00:00.000 Open
2014-12-02 13:00:00.000 2014-12-02 13:30:00.000 Open
2014-12-02 13:30:00.000 2014-12-02 14:00:00.000 Open
2014-12-02 14:00:00.000 2014-12-02 14:30:00.000 Open
2014-12-02 14:30:00.000 2014-12-02 15:00:00.000 Open
Kevin Cook
  • 1,922
  • 1
  • 15
  • 16
  • Kevin, this is _amazing_. Not only this does exactly what I wanted, you explain it well. I will try to wrap my head around this because there is a lot for me to learn here. Thank you. This seems to be a fairly complex SQL so I do not know if it is possible but do you know where I can start looking at If I want to try to make it a little faster (it takes over 1 second to run as it is)? – Leo Jan 02 '15 at 12:11
  • Oh this can be trimmed down. the tables I am generating are for the whole year+ (100000 rows) and not the date range specified, but I was doing this for an example and not for performance. The easiest spot would be to just generate the timeslots only for the date range specified, and that would probably give you the best 'bang for buck' as far as optimization, since it would make the final set of joins faster. – Kevin Cook Jan 02 '15 at 17:24