The following solution does everything you want, all in SQL. It merges overlapping and contiguous timespans (including wholly-overlapping timespans) and also breaks out multi-day timespans by date using a calendar table.
declare @TimeSheetEntries table
(
ID int identity not null primary key,
[Start] datetime not null,
[End] datetime null
);
declare @CalendarTable table
(
CalDate datetime
)
DECLARE @CurDate datetime = '2000-01-01 00:00:00';
WHILE @CurDate <= '2020-12-31'
BEGIN
INSERT INTO @CalendarTable
SELECT @CurDate;
SELECT @CurDate = DATEADD(day,1,@CurDate);
END
insert into @TimeSheetEntries
(
[Start],
[End]
)
select
'2012-06-06 19:32:00',
'2012-06-08 06:00:00'
union all select
'2012-06-08 06:00:00',
NULL
union all select
'2012-06-08 02:00:00',
'2012-06-08 03:00:00'
union all select
'2012-01-01 14:09:00',
'2012-01-01 17:30:00'
union all select
'2012-01-01 18:30:00',
'2012-01-01 19:30:00';
WITH ClockData AS
(
SELECT ID, [Start], [End] AS EffectiveEnd, [Start] AS LastStart, 1 AS NumTimespans
FROM @TimeSheetEntries ts
WHERE NOT EXISTS
(
SELECT tsWhere.ID FROM @TimeSheetEntries tsWhere
WHERE
tsWhere.ID <> ts.ID -- don't match yourself!
AND tsWhere.[Start] < ts.[Start]
AND
(
-- Three types of things we don't want our outer SELECT to return
-- 1) Completed timespans with another appointment's end date sitting inside;
-- 2) Incomplete timespans (NULL ClockedOut) with another appointment's end date after ClockedIn
-- 3) Any timespan wholly inside another one
(ts.[End] IS NULL AND tsWhere.[End] >= ts.[Start]) OR
(ts.[End] IS NOT NULL AND tsWhere.[End] BETWEEN ts.[Start] AND ts.[End]) OR
(tswhere.[End] > ts.[End])
)
)
UNION ALL
SELECT cd.ID, cd.[Start], ts.[End] AS EffectiveClockout, ts.Start AS LastStart, cd.NumTimespans + 1 AS NumTimespans
FROM @TimeSheetEntries ts
INNER JOIN ClockData cd
ON ts.[Start] BETWEEN cd.[Start] AND cd.EffectiveEnd AND ts.ID <> cd.ID
AND (ts.[End] IS NULL OR ts.[End] > cd.EffectiveEnd)
),
MergedTimespans AS
(
SELECT [ID], [Start], [EffectiveEnd], [LastStart]
FROM ClockData cd
WHERE NumTimespans = (SELECT MAX(NumTimespans) FROM ClockData WHERE ID = cd.ID)
)
SELECT
CASE
WHEN mt.Start > cal.CalDate
THEN mt.Start
ELSE cal.CalDate
END AS [Start],
CASE
WHEN (mt.EffectiveEnd IS NOT NULL AND mt.EffectiveEnd >= DATEADD(day,1,cal.CalDate)) OR (mt.EffectiveEnd IS NULL AND cal.CalDate < CAST(mt.LastStart AS date)) THEN DATEADD(second, -1,DATEADD(day,1,cal.CalDate))
ELSE mt.EffectiveEnd
END AS [End]
FROM MergedTimespans mt
INNER JOIN @CalendarTable cal
ON (mt.EffectiveEnd IS NULL AND cal.CalDate BETWEEN CAST(mt.Start AS date) AND mt.LastStart) -- Handle NULL end dates
OR (mt.EffectiveEnd IS NOT NULL AND cal.CalDate BETWEEN CAST(mt.Start AS date) AND mt.EffectiveEnd)
ORDER BY Start