Итак, представьте себе несколько таблиц:
USE tempdb;
GO
CREATE TABLE dbo.Users
(
UserID INT IDENTITY(1,1),
Username VARCHAR(32)
);
CREATE TABLE dbo.Groups
(
GroupID INT IDENTITY(1,1),
GroupName VARCHAR(32)
);
CREATE TABLE dbo.Membership
(
UserID INT,
GroupID INT
);
CREATE TABLE dbo.[event]
(
event_id INT IDENTITY(1,1),
event_start DATETIME,
event_end DATETIME,
group_id INT,
recurring BIT
);
И представьте, что некоторые примерные данные было не так сложно предоставить:
INSERT dbo.Users(Username)
SELECT 'User A' UNION ALL SELECT 'User B';
INSERT dbo.Groups(GroupName)
SELECT 'Group 1' UNION ALL SELECT 'Group 2';
INSERT dbo.Membership(UserID, GroupID)
SELECT 1,1 UNION ALL SELECT 2,2;
INSERT dbo.[event](event_start, event_end, group_id, recurring)
-- user A, almost all day meeting on a specific date
SELECT '20120313 07:00', '20120313 18:00', 1, 0
-- user A, recurring meeting every Monday
UNION ALL SELECT '20120312 17:00', '20120312 18:00', 1, 1
-- user A, recurring meeting every Tuesday (future)
UNION ALL SELECT '20120327 14:00', '20120327 15:00', 1, 1;
GO
Теперь мы можем построить эту хранимую процедуру:
CREATE PROCEDURE dbo.GetPossibleMeetingTimes
@AskingUserID INT,
@TargetUserID INT,
@Duration INT, -- in minutes!
@StartDate SMALLDATETIME, -- assumes date, no time!
@EndDate SMALLDATETIME -- again - date, no time!
AS
BEGIN
SET NOCOUNT ON;
;WITH dRange(d) AS
(
-- get the actual dates in the requested range
-- limited to number of rows in sys.objects
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
), possible(ds, de) AS
(
-- get all the timeslots of @Duration minutes
-- between 7:00 AM and 7:00 PM for each day in
-- the range - these are all *potential* slots
SELECT DATEADD(MINUTE, 30*rn, DATEADD(HOUR, 7, dRange.d)),
DATEADD(MINUTE, 30*rn + @Duration, DATEADD(HOUR, 7, dRange.d))
FROM (SELECT TOP (720/30) 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 p.de <= DATEADD(HOUR, 19, DATEADD(DAY, DATEDIFF(DAY, 0, p.de), 0))
AND NOT EXISTS
(
SELECT 1 FROM
(
-- filter down to users with events on the days in the range
SELECT group_id, event_start, event_end
FROM dbo.[event]
WHERE event_start >= @StartDate
AND event_start < DATEADD(DAY, 1, @EndDate)
UNION ALL
-- also include users with recurring events on same weekday(s)
-- normalized to the matching day in the range
SELECT group_id,
event_start = DATEADD(DAY, DATEDIFF(DAY, event_start, p.ds), event_start),
event_end = DATEADD(DAY, DATEDIFF(DAY, event_end, p.ds), event_end)
FROM dbo.[event]
WHERE recurring = 1
AND event_start <= DATEADD(DAY, 1, @EndDate) -- ignore future events
AND event_start >= DATEADD(WEEK, -52, @EndDate) -- 52 weeks out
AND DATEDIFF(DAY, event_start, p.ds) % 7 = 0 -- same weekday
) AS sub
WHERE sub.group_id IN
(
-- this checks that events are within previously scheduled times
SELECT GroupID FROM dbo.Membership
WHERE UserID IN (@AskingUserID, @TargetUserID)
AND (p.de > sub.event_start AND p.ds < sub.event_end)
)
)
ORDER BY p.ds, p.de;
END
GO
Пример звонков:
-- Case 1: User A tries to meet with User B on a day where
-- both schedules are clear.
EXEC dbo.GetPossibleMeetingTimes
@AskingUserID = 1,
@TargetUserID = 2,
@Duration = 30,
@StartDate = '20120314', -- no events for either user
@EndDate = '20120314';
Результаты:
-- Case 2: User A tries to meet with User B for an hour, on
-- a day where user A has meetings from 7 AM to 6 PM.
EXEC dbo.GetPossibleMeetingTimes
@AskingUserID = 1,
@TargetUserID = 2,
@Duration = 60,
@StartDate = '20120313', -- user A has an almost all-day event
@EndDate = '20120313';
Результаты:
-- Case 3: User A tries to meet with User B for two hours, on
-- a weekday where User A has a recurring meeting from 5-6 PM
EXEC dbo.GetPossibleMeetingTimes
@AskingUserID = 1,
@TargetUserID = 2,
@Duration = 120,
@StartDate = '20120319', -- user A has a recurring meeting
@EndDate = '20120319';
Результаты:
Теперь обратите внимание, что я позаботился о нескольких факторах, которые вы либо не учли, либо не упомянули (например, повторяющееся событие, которое начинается в будущем). С другой стороны, я также не имел дело с некоторыми другими факторами (например, летнее время, если это вообще может повлиять на это) и не тестировал все возможные сценарии (например, несколько событий в один и тот же день, которые уже существуют).
Я проверил, что если вы перейдете в диапазон (например, 2012-03-12 -> 2012-03-14), вы по существу просто получите объединение вышеуказанных результатов с примерно одинаковыми временными интервалами, доступными (они варьируются исходя из продолжительности курса). Важной частью является то, что временные интервалы затемнения соблюдаются. Я не проверял логику для случая, когда повторяющееся событие начинается в будущем, и предоставленный диапазон дат включает этот день недели как до, так и после первого экземпляра события.
Если какой-либо случай не работает для вас, именно поэтому важно, чтобы вы показали нам все свои случаи , используя образцы данных, а не проблемы со словами , а также объяснили желаемые результаты данного запроса эти данные.
РЕДАКТИРОВАТЬ - чтобы обрабатывать более 2 пользователей, вам нужно всего лишь несколько изменений. Если вы добавите функцию разделения следующим образом:
CREATE FUNCTION dbo.SplitInts( @List VARCHAR(MAX) )
RETURNS TABLE
AS
RETURN
( SELECT Item = CONVERT(INT, Item) FROM (
SELECT Item = x.i.value('(./text())[1]', 'INT') FROM (
SELECT [XML] = CONVERT(XML, '<i>' + REPLACE(@List, ',', '</i><i>')
+ '</i>').query('.')) AS a CROSS APPLY [XML].nodes('i') AS x(i)) AS y
WHERE Item IS NOT NULL
);
Теперь очень незначительные изменения в хранимой процедуре (я пропустил неизмененные биты):
ALTER PROCEDURE dbo.GetPossibleMeetingTimes
@UserIDList VARCHAR(MAX), -- removed other two parameters
@Duration INT,
@StartDate SMALLDATETIME,
@EndDate SMALLDATETIME
AS
...
WHERE sub.group_id IN -- changed the code within this subquery
(
SELECT GroupID FROM dbo.Membership AS m
INNER JOIN dbo.SplitInts(@UserIDList) AS i
ON m.UserID = i.Item
WHERE (p.de > sub.event_start AND p.ds < sub.event_end)
)
...
Итак, ваш звонок слегка меняется на:
EXEC dbo.GetPossibleMeetingTimes
@UserIDList = '1,2,3,4,5',
@Duration = 30,
@StartDate = '20120314',
@EndDate = '20120314';
Просто убедитесь, что запросчик включен в список через запятую.
PS это дополнение не проверено.