Well that was an interesting and fun problem to work on. +1
So lets start up with a setup:
CREATE TABLE d (start_dt datetime, end_dt datetime)
INSERT INTO d VALUES ('3/5/2010 11:27', '3/5/2010 13:04')
INSERT INTO d VALUES ('3/5/2010 11:29', '3/5/2010 11:55')
INSERT INTO d VALUES ('3/5/2010 13:13', '3/5/2010 13:37')
INSERT INTO d VALUES ('3/5/2010 13:13', '3/5/2010 13:37')
INSERT INTO d VALUES ('3/5/2010 14:55', '3/5/2010 15:22')
INSERT INTO d VALUES ('3/5/2010 14:55', '3/5/2010 15:22')
INSERT INTO d VALUES ('3/5/2010 15:15', '3/5/2010 17:45')
Here is the code to actually calculate it, all via TSQL:
-- Create a cursor that we will use to loop over all the items in the base table
DECLARE cur CURSOR FOR SELECT start_dt, end_dt FROM d
-- Temp variables to store each rows data
DECLARE @start_dt datetime, @end_dt datetime
-- Temp table to hold the "adjusted" rows
DECLARE @d TABLE (id int identity, start_dt datetime, end_dt datetime)
OPEN cur
FETCH NEXT FROM cur INTO @start_dt, @end_dt
WHILE @@FETCH_STATUS = 0
BEGIN
-- Start by deleting any rows contained entirely within the current rows timeframe
DELETE FROM @d WHERE start_dt BETWEEN @start_dt AND @end_dt AND end_dt BETWEEN @start_dt AND @end_dt
DECLARE @id_start_in int = -1, @id_end_in int = -1
SELECT @id_start_in = id FROM @d WHERE @start_dt BETWEEN start_dt AND end_dt
SELECT @id_end_in = id FROM @d WHERE @end_dt BETWEEN start_dt AND end_dt
-- If our start and end dates are not contained in any other set, add it as a new row
IF (@id_start_in = -1 AND @id_end_in = -1)
INSERT INTO @d (start_dt, end_dt) VALUES (@start_dt, @end_dt)
-- If our start date and end dates are both contained in the same row, ignore because we are overlapping that row
-- If our start date and end dates are in two different rows, we combine those two
-- For example if there are 3 rows, 1-3, 2-5, 4-6, we actually have full coverage from 1-6
IF (@id_start_in != @id_end_in AND @id_start_in != -1 AND @id_end_in != -1)
BEGIN
-- Expand the start row to end at the same time the row our end time is in
UPDATE @d SET end_dt = (SELECT end_dt FROM @d WHERE id = @id_end_in) WHERE id = @id_start_in
-- Delete the row our end time is in
DELETE FROM @d WHERE id = @id_end_in
END
-- If our start date is contained in a row but our end date isnt, extend the existing row
-- to end at our end date
IF (@id_start_in != -1 AND @id_end_in = -1)
UPDATE @d SET end_dt = @end_dt WHERE id = @id_start_in
-- If our end date is contained in a row but our start date isnt, extend the existing row
-- to start at our start time
IF (@id_start_in = -1 AND @id_end_in != -1)
UPDATE @d SET start_dt = @start_dt WHERE id = @id_end_in
FETCH NEXT FROM cur INTO @start_dt, @end_dt
END
CLOSE cur
DEALLOCATE cur
-- Show the end table
SELECT start_dt, end_dt, DATEDIFF(MINUTE, start_dt, end_dt) FROM @d
-- Sum up to get the minutes and calculate the hours
SELECT SUM(DATEDIFF(MINUTE, start_dt, end_dt)) AS MINUTES, CAST(SUM(DATEDIFF(MINUTE, start_dt, end_dt)) AS DECIMAL) / 60 AS HOURS FROM @d