Find gaps in timesheet data between certain hours - sql-server

I am trying to find gaps in timesheets between the hours of 8AM and 6PM. I am able to find the gaps for records that are logged, but I cannot figure out how to determine if a record was "missed" - meaning if they started at 8:30 AM, I cannot figure out how to identify the 30 minute gap between 8AM and 8:30 AM (ie. they started work late).
In below example, I can find the two gaps between 12 and 12:30 pm, but not the 8am-8:30am gap and 5:30 to 6pm gap on 5/8, and 8am-8:30am gap on 5/10.
Any ideas to point me in the right direction on how I could approach this?
drop table #time;
create table #time (
TimesheetId int not null
, StartTime datetime not null
, EndTIme datetime not null
);
insert into #time (TimesheetId, StartTime, EndTime)
values (210, '2017-05-08 05:30:00.000', '2017-05-08 06:30:00.000')
, (210, '2017-05-08 06:30:00.000', '2017-05-08 08:30:00.000')
, (210, '2017-05-08 08:30:00.000', '2017-05-08 12:00:00.000')
, (210, '2017-05-08 12:30:00.000', '2017-05-08 18:30:00.000')
, (210, '2017-05-09 08:30:00.000', '2017-05-09 12:00:00.000')
, (210, '2017-05-09 12:30:00.000', '2017-05-09 17:30:00.000')
, (210, '2017-05-09 22:30:00.000', '2017-05-10 05:30:00.000')
, (210, '2017-05-10 08:30:00.000', '2017-05-10 18:00:00.000')
;
; with t1 as (
SELECT TimesheetId
, StartTime
, lag(EndTime) OVER (PARTITION BY TimesheetId ORDER BY StartTime) AS prev_endtime
FROM #time
where datepart(HH, StartTime) <= 18
and datepart(HH, EndTime) >= 8
)
select prev_endtime as gapStart
, StartTime as gapEnd
from t1
where StartTime <> prev_endtime
and cast(prev_endtime as date) = cast(StartTime as date)
;

WITH
a AS(SELECT DATEADD(hh, DATEDIFF(dd, 0, StartTime) * 24 + 8, 0) t,
TimesheetId FROM #time),
b AS(SELECT * FROM #time UNION ALL SELECT TimesheetId, t, t FROM a UNION ALL
SELECT TimesheetId, DATEADD(hh, 10, t), DATEADD(hh, 10, t) FROM a),
c AS(SELECT TimesheetId,
LAG(EndTime) OVER (
PARTITION BY TimesheetId ORDER BY StartTime
) prev_fin,
StartTime
FROM b),
d AS(SELECT *, DATEADD(hh, DATEDIFF(dd, 0, prev_fin) * 24 + 8, 0) beg,
DATEADD(hh, DATEDIFF(dd, 0, prev_fin) * 24 + 18, 0) fin
FROM c)
SELECT TimesheetId, prev_fin, StartTime
FROM d
WHERE prev_fin < StartTime AND
((prev_fin >= beg AND prev_fin < fin) OR
(StartTime > beg AND StartTime <= fin));
Check it on rextester.com.

You can use this to insert a record and then use what you have
Or you could use a UNION
select distinct t1.TimesheetId, dateadd(hh, 8, cast(CONVERT(date, StartTime) as datetime)) as StartTime, dateadd(hh, 8, cast(CONVERT(date, StartTime) as datetime)) as EndTime
from #time t1
where not exists ( select dateadd(hh, 8, cast(CONVERT(date, T2.StartTime) as datetime)), t2.*
from #time T2
where 1 = 1
and t2.TimesheetId = t1.TimesheetId
and CONVERT(date, T2.StartTime) = CONVERT(date, T1.StartTime)
and t2.StartTime = dateadd(hh, 8, cast(CONVERT(date, t2.StartTime) as datetime))
)

Related

Creating a Calendar table without recursion

I want to create a Calendar Table without using the recursion as I have prepared earlier. How can I achieve this task. All the required columns are mentioned in code below and few other details in code comments.
...........................................................................................................................................................................................................................................................................
DECLARE #StartDate date = '20200101'
DECLARE #CutoffDate date = GETDATE()
;WITH seq(n) AS
(
SELECT 0 UNION ALL SELECT n + 1 FROM seq
WHERE n < DATEDIFF(DAY, #StartDate, #CutoffDate)
),
d(d) AS
(
SELECT DATEADD(DAY, n, #StartDate) FROM seq
),
src AS /*SOURCE TABLE WITH OBJECT DEFINITION*/
(
SELECT
TheDate = CONVERT(date, d),
TheDay = DATEPART(DAY, d),
TheDayName = DATENAME(WEEKDAY, d),
TheWeek = DATEPART(WEEK, d),
TheDayOfWeek = DATEPART(WEEKDAY, d),
TheMonth = DATEPART(MONTH, d),
TheMonthName = DATENAME(MONTH, d),
TheQuarter = Concat('Q',DATEPART(Quarter, d)),
Financial_Year = DATEPART(YEAR, d),
Financial_Quarter=Datepart(QUARTER,d),
TheYear = DATEPART(YEAR, d),
TheFirstOfMonth = DATEFROMPARTS(YEAR(d), MONTH(d), 1),
TheFirstOfFYear = DATEFROMPARTS(YEAR(d), 4, 1),
TheFirstOfYear = DATEFROMPARTS(YEAR(d), 1, 1),
TheLastOfYear = DATEFROMPARTS(YEAR(d), 12, 31),
TheDayOfYear = DATEPART(DAYOFYEAR, d)
FROM d
),
Dimension AS
(
SELECT
TheDate,
TheDay,
TheDayName,
TheDayOfWeek,
TheDayOfWeekInMonth = CONVERT(tinyint, ROW_NUMBER() OVER
(PARTITION BY TheFirstOfMonth, TheDayOfWeek ORDER BY TheDate)),
TheDayOfYear,
TheWeek,
TheFirstOfWeek = DATEADD(DAY, 1 - TheDayOfWeek, TheDate),
TheLastOfWeek = DATEADD(DAY, 6, DATEADD(DAY, 1 - TheDayOfWeek, TheDate)),
TheWeekOfMonth = CONVERT(tinyint, DENSE_RANK() OVER
(PARTITION BY TheYear, TheMonth ORDER BY TheWeek)),
TheMonth,
TheMonthName,
TheFirstOfMonth,
TheLastOfMonth = MAX(TheDate) OVER (PARTITION BY TheYear, TheMonth),
TheFirstOfNextMonth = DATEADD(MONTH, 1, TheFirstOfMonth),
TheLastOfNextMonth = DATEADD(DAY, -1, DATEADD(MONTH, 2, TheFirstOfMonth)),
TheQuarter,
TheFirstOfQuarter = MIN(TheDate) OVER (PARTITION BY TheYear, TheQuarter),
TheLastOfQuarter = MAX(TheDate) OVER (PARTITION BY TheYear, TheQuarter),
TheYear,
TheFirstOfYear = DATEFROMPARTS(TheYear, 1, 1),
TheFirstOfFYear = DATEFROMPARTS(TheYear, 4, 1),
TheLastOfYear,
MMYYYY = CONVERT(char(2), CONVERT(char(8), TheDate, 101))
+ CONVERT(char(4), TheYear),
Financial_Quarter = Datepart(Quarter,DATEADD(MONTH, -3, TheFirstOfMonth)), /*Starting Financial Quarter from April*/
Financial_Year =CASE
WHEN Financial_Quarter = 1 THEN DATEPART(Year,Dateadd(Year,-1,TheFirstofYear)) ELSE THEYEAR END
FROM src
)
SELECT * FROM Dimension
ORDER BY TheDate
OPTION (MAXRECURSION 0);
As I mentioned in the comments, use a Tally. These are significantly faster than a rCTE as they aren't recursive. I use an inline tally here:
DECLARE #StartDate date = '20200101';
DECLARE #CutoffDate date = GETDATE();
/*
; is a terminator, not a "beginingator". It goes at the end of ALL your statements,
not at the start of statements that require the PREVIOUS statement to be properly terminated.
*/
WITH N AS
(SELECT N
FROM (VALUES (NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) N(N)),
Tally AS
(SELECT 0 AS I
UNION ALL
SELECT TOP (DATEDIFF(DAY, #StartDate, #CutoffDate))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1,N N2,N N3), --Up to 1000 rows. Add more cross joins for more rows
D AS
(SELECT DATEADD(DAY, T.I, #StartDate) AS d
FROM Tally T),
Src AS /*SOURCE TABLE WITH OBJECT DEFINITION*/
(SELECT CONVERT(date, d) AS TheDate,
DATEPART(DAY, d) AS TheDay,
DATENAME(WEEKDAY, d) AS TheDayName,
DATEPART(WEEK, d) AS TheWeek,
DATEPART(WEEKDAY, d) AS TheDayOfWeek,
DATEPART(MONTH, d) AS TheMonth,
DATENAME(MONTH, d) AS TheMonthName,
CONCAT('Q', DATEPART(QUARTER, d)) AS TheQuarter,
DATEPART(YEAR, d) AS Financial_Year,
DATEPART(QUARTER, d) AS Financial_Quarter,
DATEPART(YEAR, d) AS TheYear,
DATEFROMPARTS(YEAR(d), MONTH(d), 1) AS TheFirstOfMonth,
DATEFROMPARTS(YEAR(d), 4, 1) AS TheFirstOfFYear,
DATEFROMPARTS(YEAR(d), 1, 1) AS TheFirstOfYear,
DATEFROMPARTS(YEAR(d), 12, 31) AS TheLastOfYear,
DATEPART(DAYOFYEAR, d) AS TheDayOfYear
FROM d),
Dimension AS
(SELECT TheDate,
TheDay,
TheDayName,
TheDayOfWeek,
CONVERT(tinyint, ROW_NUMBER() OVER (PARTITION BY TheFirstOfMonth, TheDayOfWeek ORDER BY TheDate)) AS TheDayOfWeekInMonth,
TheDayOfYear,
TheWeek,
DATEADD(DAY, 1 - TheDayOfWeek, TheDate) AS TheFirstOfWeek,
DATEADD(DAY, 6, DATEADD(DAY, 1 - TheDayOfWeek, TheDate)) AS TheLastOfWeek,
CONVERT(tinyint, DENSE_RANK() OVER (PARTITION BY TheYear, TheMonth ORDER BY TheWeek)) AS TheWeekOfMonth,
TheMonth,
TheMonthName,
TheFirstOfMonth,
MAX(TheDate) OVER (PARTITION BY TheYear, TheMonth) AS TheLastOfMonth,
DATEADD(MONTH, 1, TheFirstOfMonth) AS TheFirstOfNextMonth,
DATEADD(DAY, -1, DATEADD(MONTH, 2, TheFirstOfMonth)) AS TheLastOfNextMonth,
TheQuarter,
MIN(TheDate) OVER (PARTITION BY TheYear, TheQuarter) AS TheFirstOfQuarter,
MAX(TheDate) OVER (PARTITION BY TheYear, TheQuarter) AS TheLastOfQuarter,
TheYear,
DATEFROMPARTS(TheYear, 1, 1) AS TheFirstOfYear,
DATEFROMPARTS(TheYear, 4, 1) AS TheFirstOfFYear,
TheLastOfYear,
CONVERT(char(2), CONVERT(char(8), TheDate, 101)) + CONVERT(char(4), TheYear) AS MMYYYY,
DATEPART(QUARTER, DATEADD(MONTH, -3, TheFirstOfMonth)) AS Financial_Quarter, /*Starting Financial Quarter from April*/
CASE
WHEN Financial_Quarter = 1 THEN DATEPART(YEAR, DATEADD(YEAR, -1, TheFirstOfYear))
ELSE TheYear
END AS Financial_Year
FROM src)
SELECT *
FROM Dimension
ORDER BY TheDate;

Is there a way to split shift records by date / time

I have roster data that has night shift records (eg has start time of 2019-01-30 21:00:00.000 and a finish time of 2019-01-31 05:30:00.000 the following day.
I need to split this into 2 rows as follows
2019-01-30 21:00:00.000 to 2019-01-30 23:59:59.999
2019-01-31 00:00:00.001 to 2019-01-31 05:30:00.000
I need to retain all other info from the line. and the day date is the date the shift started.
SELECT actual_id
, emp_id
, emp_number
, area
, area_id
, day_date
, start_time
, finish_time
FROM Roster
WHERE CONVERT(date,start_time, 112) <> CONVERT(date,finish_time, 112)
To create two rows you can CROSS JOIN with a temp table having two rows like following.
select *
FROM roster
INNER JOIN
(
SELECT 1 temp
UNION
SELECT 2 temp )t
ON 1=1
Above query will replicate each row two times.
Now based on the row number (1 or 2) you can get the end time of a day and start time of a day like
CASE WHEN t.rownum =1 THEN start_time
ELSE dateadd(second, 1, Dateadd(day, Datediff(day, 0, finish_time), 0)
end starttime ,
CASE
WHEN t.rownum=2 THEN finish_time
ELSE dateadd(ms, -2, dateadd(dd, 1, datediff(dd, 0, start_time)))
END AS endtime FROM roster
Your final query should look like following query.
SELECT actual_id ,
emp_id ,
emp_number ,
area ,
area_id ,
day_date ,
CASE
WHEN t.rownum=1 THEN start_time
ELSE dateadd(second, 1, Dateadd(day, Datediff(day, 0, finish_time), 0)
end starttime ,
CASE
WHEN t.rownum=2 THEN finish_time
ELSE dateadd(ms, -2, dateadd(dd, 1, datediff(dd, 0, start_time)))
END AS endtime FROM roster
INNER JOIN
(
SELECT 1 rownum
UNION
SELECT 2 rownum)t
ON 1=1
WHERE CONVERT(date,start_time, 112) <> CONVERT(date,finish_time, 112)
Please try the mentioned solution:
SELECT
s.emp_id,
f.theDay
FROM Roster AS s
CROSS APPLY (SELECT
DATEADD(DAY, Number, s.start_time)
FROM master..spt_values AS v
WHERE v.Type = 'P'
AND v.Number BETWEEN 0 AND DATEDIFF(DAY, s.start_time, s.finish_time)) AS f (theDay)
ORDER BY s.emp_id,
f.theDay

SQLSERVER 2008: Breaking a output for the entire day into 2 records for 12 hours

I am looking for the count of records as below.
PLANNED_SHIP_From_Date PLANNED_SHIP_To Date Total_Lines_Count ....
1) 09-04-2016 07:00:01 09-04-2016 18:59:59 165 .....
2) 09-04-2016 19:00:00 10-04-2016 07:00:00 121 .....
3) 10-04-2016 07:00:01 10-04-2016 18:59:59 165 .....
4) 10-04-2016 19:00:00 11-04-2016 07:00:00 123 .....
5) 11-04-2016 07:00:01 11-04-2016 18:59:59 234 .....
.
Currently my query is counting the records as per date.
SELECT
cast(shdr.PLANNED_SHIP_DATE as date),
SUM(sdtl_1_1.TOTAL_LINES_COUNT) AS TOTAL_LINES_COUNT
FROM
dbo.SHIPMENT_HEADER AS shdr WITH (NOLOCK)
INNER JOIN
(
SELECT
SHIPMENT_ID,
COUNT(*) AS TOTAL_LINES_COUNT
FROM
dbo.SHIPMENT_DETAIL AS SHIPMENT_DETAIL_1 WITH (NOLOCK)
WHERE
(
STATUS1 >= 401
)
AND (
DATEDIFF(day, PLANNED_SHIP_DATE, CONVERT(date, SYSDATETIME())) < 4
)
GROUP BY
SHIPMENT_ID
) AS sdtl_1_1
ON sdtl_1_1.SHIPMENT_ID = shdr.SHIPMENT_ID
WHERE
(
shdr.TRAILING_STS >= 401
)
AND (
DATEDIFF(day, shdr.PLANNED_SHIP_DATE, CONVERT(date, SYSDATETIME())) < 4
)
GROUP BY
cast(shdr.PLANNED_SHIP_DATE as date)
Try this -
DECLARE #ReportDays int = 30,
#StartHr int = 7,
#Today DATETIME2 = CAST(SYSDATETIME() AS DATE);
--http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/you-require-a-numbers-table.aspx
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),
numbers as (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS number FROM e),
StartDates AS (
SELECT
DATEADD(
HH,
#StartHr + (n2.number * 12),
DATEADD(D, 0-n1.number, #Today)
) AS StartDT
FROM
(SELECT * FROM numbers WHERE Number BETWEEN 0 AND #ReportDays) n1
CROSS JOIN (SELECT * FROM numbers WHERE Number IN (0,1)) n2
),
DateRanges AS
(SELECT StartDT, DATEADD(hh, 12, StartDT) AS EndDT FROM StartDates),
Shipments AS
(SELECT
StartDT AS PLANNED_SHIP_From_Date,
EndDT AS PLANNED_SHIP_To_Date,
1 AS Shipment
FROM
DateRanges dr
LEFT JOIN dbo.SHIPMENT_DETAIL sd
ON sd.Status1 >=401
AND sd.PLANNED_SHIP_DATE BETWEEN dr.StartDT AND dr.EndDT)
SELECT
PLANNED_SHIP_From_Date,
PLANNED_SHIP_To_Date,
SUM(Shipment) AS TOTAL_LINES_COUNT
FROM
Shipments
ORDER BY
PLANNED_SHIP_From_Date;
What we're doing is -
Building a numbers table
Using that to pull a list of days, with two records per day
Working out the start & finish times for each time window
Joining the time windows to the records and summing
Hope that helps :-)
Add another column to your select....
CASE
WHENE DATEPART(HOUR, Planned_SHIP_DATE) < 12 THEN 'AM' ELSE 'PM'
END AS ShipPeriod
You could then add that column into a GROUPING to seperate the 'AM's from 'PM's
Of course I have assuumed you wanted AM/PM. But you can modify the CASE statement to break the hours up as you see fit.
Hope this helps
Thank you all for helping me out.
I have created a SQL query which worked for me. This query gives the count of records from morning 7 AM to 7 PM as MORNING_SHIFT count and 7PM to next day 7AM morning as EVENING_SHIFT for dates greater than 14 days in the past.
SELECT
CASE
WHEN convert(VARCHAR(50), sh.PLANNED_SHIP_DATE, 120) BETWEEN
(convert(VARCHAR(10), sh.PLANNED_SHIP_DATE, 120) + ' 07:00:00') AND
(convert(VARCHAR(10), sh.PLANNED_SHIP_DATE, 120) + ' 18:59:59')
THEN (CONCAT(cast(sh.PLANNED_SHIP_DATE as date),' ','morning_shift'))
WHEN convert(VARCHAR(50), sh.PLANNED_SHIP_DATE, 120) BETWEEN
(convert(VARCHAR(10), sh.PLANNED_SHIP_DATE, 120) + ' 00:00:00') AND
(convert(VARCHAR(10), sh.PLANNED_SHIP_DATE, 120) + ' 06:59:59')
then (CONCAT(cast(DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) as date),' ','EVENING_shift'))
when
convert(VARCHAR(50), DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) , 120) BETWEEN (convert(VARCHAR(10), cast(DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) as date), 120) + ' 19:00:00') AND
(convert(VARCHAR(10), cast(DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) as date), 120) + ' 23:59:59')
THEN (CONCAT(cast(DATEADD(DAY, -1, sh.PLANNED_SHIP_DATE) as date),' ','EVENING_shift'))
END AS 'actual_date_time', sh.PLANNED_SHIP_DATE
FROM dbo.SHIPMENT_HEADER AS sh WITH (nolock)
WHERE (shdr.TRAILING_STS >= 401) AND (DATEDIFF(day, shdr.ACTUAL_SHIP_DATE_TIME, CONVERT(date, SYSDATETIME())) < 14)
group by sh.ACTUAL_SHIP_DATE_TIME;

SSRS loop over select by months

I am creating report in SSRS that should show event duration in a month. This report should do it 12 time for every calendar month separately. At the moment my select has 12 hardcoded selects, for every month each. How can I do this with one select repeated 12 times and just iterated start and end times, either in SQL language (applicable as a view) or even better from SSRS? Thank you.
SELECT DATEDIFF(mi, '2014-01-01 00:00:00.000', end_date) AS total_event
FROM MY_TABLE
WHERE (start_date <= '2014-01-01 00:00:00.000') AND (end_date BETWEEN '2014-01-01 00:00:00.000' AND '2014-02-01 00:00:00.000')
UNION ALL
SELECT DATEDIFF(mi, start_date, '2014-02-01 00:00:00.000') AS total_event
FROM MY_TABLE
WHERE (end_date >= '2014-02-01 00:00:00.000') AND (start_date BETWEEN '2014-01-01 00:00:00.000' AND '2014-02-01 00:00:00.000')
UNION ALL
SELECT DATEDIFF(mi, '2014-01-01 00:00:00.000', '2014-02-01 00:00:00.000') AS total_event
FROM MY_TABLE
WHERE (start_date <= '2014-01-01 00:00:00.000') AND (end_date >= '2014-02-01 00:00:00.000')
UNION ALL
SELECT DATEDIFF(mi, start_date, end_date) AS total_event
FROM MY_TABLE
WHERE (start_date BETWEEN '2014-01-01 00:00:00.000' AND '2014-02-01 00:00:00.000') AND (end_date BETWEEN '2014-01-01 00:00:00.000' AND '2014-02-01 00:00:00.000')
try using below script
declare #start datetime
declare #end datetime
set #start = '2014-01-01' --firstDay year
set #end = '2014-12-31' --last Day of year
;with cte as
(
select #start firstday, DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,DATEADD(month, DATEDIFF(month, 0, #start), 0))+1,0)) lastday, 1 MM
union all
select DATEADD(month, DATEDIFF(month, 0, lastday + 1), 0) , DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,DATEADD(month, DATEDIFF(month, 0, lastday + 1), 0))+1,0)), MM + 1
from cte
where lastday < #end
)
SELECT * FROM CTE C
--LEFT OUTER JOIN MY_TABLE M ON (M.start_date <= firstday) AND (end_date BETWEEN firstday AND lastday)
just un-comment last line
--LEFT OUTER JOIN MY_TABLE M ON (M.start_date <= firstday) AND (end_date BETWEEN firstday AND lastday)
You could try this:
SELECT
DATEADD(MONTH, DATEDIFF(MONTH, 0, start_date), 0) AS [month], DATEDIFF(MINUTE, DATEADD(MONTH, DATEDIFF(MONTH, 0, start_date), 0), end_date) AS total_event
FROM
MY_TABLE
WHERE
YEAR(start_date) = 2014
ORDER BY
[month];

Divide up a work day into blocks of time set up in employee schedule SQL

I have a database which contains a table of time clock setup entries. This serves as the "buckets" of time an employee's day could fall into. This table can contain any number of buckets as long as they don't overlap eachother.
For example, this is our table of buckets:
ID Start End timeType
-------------------------------------------------
1 08:00:00.000 12:00:00.000 REGULAR
1 12:00:00.000 12:30:00.000 BREAK
1 12:30:00.000 16:00:00.000 REGULAR
1 16:00:00.000 00:00:00.000 OVERTIME
I have a punch in time of say, 07:55 and a punch out time of 17:00. I need to figure out how much of my day falls into each bucket, in hours, minutes and seconds. The data output has to look like this and I can not add columns to either table:
ID Start End timeType hrs
-----------------------------------------------------
1 07:55:00.000 12:00:00.000 REGULAR 4.08
1 12:00:00.000 12:30:00.000 BREAK 0.50
1 12:30:00.000 16:00:00.000 REGULAR 3.50
1 16:00:00.000 00:00:00.000 OVERTIME 1.00
I'm thinking a SQL inline table valued function that will be run for one day at a time, but I am having trouble getting to the hours calculation piece. So far, I think I have the logic for all scenarios, I just need help with calculating the hours as a decimal(5,2) for each scenario. I'm putting this out there for SQL suggestions but also...am I over complicating this?
Here's my stab at the logic for each scenario:
Select Case When CONVERT(time, #PunchInDate) <= CONVERT(time, EndDate)
And CONVERT(time, #PunchInDate) >= CONVERT(time, StartDate)
AND CONVERT(time, #PunchOutDate) <= CONVERT(time, EndDate)
AND CONVERT(time, #PunchOutDate) >= CONVERT(time, StartDate)Then
'Starts and ends in this range.'
Else
''
End as ScenarioA
, Case
When CONVERT(time, #PunchInDate) <= CONVERT(time, StartDate)
AND CONVERT(time, #PunchInDate) <= CONVERT(time, EndDate)
AND CONVERT(time, #PunchOutDate) >= CONVERT(time, StartDate)
AND CONVERT(time, #PunchOutDate) <= CONVERT(time, EndDate)
Then
'Starts before this range and ends in this range'
Else
''
End as ScenarioB
, Case
When CONVERT(time, #PunchInDate) >= CONVERT(time, StartDate)
And CONVERT(time, #PunchInDate) <= CONVERT(time, EndDate)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, StartDate)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, EndDate)Then
'Starts in this range and ends after the range'
Else ''
END as ScenarioC
, Case
When CONVERT(time, #PunchInDate) <= CONVERT(time, StartDate)
And CONVERT(time, #PunchInDate) >= CONVERT(time, EndDate)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, StartDate)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, EndDate)Then
'Starts before this range and ends after the range'
Else ''
END as ScenarioD
From MyTable
Where EmpID = #EmpID
If you don't already have one, create a number table. This is just a table with a single int column that contains all the integers between 0 and some large number (my table goes to 9999).
create table #numbers(num int)
insert #numbers
SELECT TOP 10000 row_number() over(order by t1.number) -1 as N
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
create table #bucket(Id int, StartTime time, EndTime time, TimeType varchar(10))
insert #bucket
select 0, '00:00:00', '08:00:00', 'OTHER' union
select 1, '08:00:00', '12:00:00', 'REGULAR' union
select 2, '12:00:00', '12:30:00', 'BREAK' union
select 3, '12:30:00', '16:00:00', 'REGULAR' union
select 4, '16:00:00', '00:00:00', 'OVERTIME'
declare #punchInDate datetime
declare #punchOutDate datetime
set #punchInDate = '5/15/2014 7:55'
set #punchOutDate = '5/15/2014 17:00'
--Using the number table, break the punchIn and punchOut times into individual rows for each minute and store them in a temp table.
select convert(time, dateadd(mi, n.num, #punchInDate)) TimeMinute
into #temp
from #numbers n
where n.num <= datediff(mi, #punchInDate, #punchOutDate)
order by 1
--Now you can just join your temp rows with your bucket table, grouping and getting the count of the number of minutes in each bucket.
select b.Id, b.StartTime, b.EndTime, b.TimeType, convert(decimal, COUNT(t.TimeMinute))/60
from #bucket b
join #temp t on t.TimeMinute>= b.StartTime and t.TimeMinute <= dateadd(mi, -1, b.EndTime)
group by b.Id, b.StartTime, b.EndTime, b.TimeType
Converting Datetime or Time Bucket Values to Numbers
You should probably store the bucket Start and End values as int values, either instead of the times or in addition-to.
To convert datetime values or expressions into something you can use for matching to the buckets, you can use something like in this code:
DECLARE #when datetime = GETDATE();
SELECT DATEDIFF(minute, DATEADD( day, DATEDIFF(day, 0, #when), 0 ), #when);
--MinutesSinceStartOfDate
-------------------------
--858
--(1 row(s) affected)
If you need seconds, just change the above to something like this:
DECLARE #when datetime = GETDATE();
SELECT DATEDIFF(second, DATEADD( day, DATEDIFF(day, 0, #when), 0 ), #when);
--SecondsSinceStartOfDate
-------------------------
--52466
--(1 row(s) affected)
Matching a Range (Start and End Values) to Buckets (Partitions)
Here's some code that you should be able to adapt to match 'buckets':
CREATE TABLE #buckets (
Id int,
Start int,
Finish int
);
GO
INSERT #buckets ( Id, Start, Finish )
VALUES ( 1, 8, 12 ),
( 2, 12, 13 ),
( 3, 13, 16 ),
( 4, 16, 24 );
DECLARE #beginning int = 9,
#ending int = 17;
SELECT x.*,
EffectiveInterval = x.EffectiveFinish - x.EffectiveStart
FROM ( SELECT *,
EffectiveStart = CASE WHEN Start < #beginning THEN #beginning ELSE Start END,
EffectiveFinish = CASE WHEN Finish > #ending THEN #ending ELSE Finish END
FROM #buckets
WHERE Finish >= #beginning
AND Start <= #ending
) x;
DROP TABLE #buckets;
--(4 row(s) affected)
--Id Start Finish EffectiveStart EffectiveFinish EffectiveInterval
------------- ----------- ----------- -------------- --------------- -----------------
--1 8 12 9 12 3
--2 12 13 12 13 1
--3 13 16 13 16 3
--4 16 24 16 17 1
--(4 row(s) affected)
Comparison of your Code and Mine
CREATE TABLE #buckets (
Id int,
Start time,
Finish time
);
INSERT #buckets ( Id, Start, Finish )
VALUES ( 1, '08:00', '12:00' ),
( 2, '12:00', '12:30' ),
( 3, '12:30', '16:00' ),
( 4, '16:00', '11:59:59.999' );
DECLARE #PunchInDate datetime = '2014-05-15 07:55',
#PunchOutDate datetime = '2014-05-15 17:00';
Select Case When CONVERT(time, #PunchInDate) <= CONVERT(time, Finish)
And CONVERT(time, #PunchInDate) >= CONVERT(time, Start)
AND CONVERT(time, #PunchOutDate) <= CONVERT(time, Finish)
AND CONVERT(time, #PunchOutDate) >= CONVERT(time, Start)Then
'Starts and ends in this range.'
Else
''
End as ScenarioA
, Case
When CONVERT(time, #PunchInDate) <= CONVERT(time, Start)
AND CONVERT(time, #PunchInDate) <= CONVERT(time, Finish)
AND CONVERT(time, #PunchOutDate) >= CONVERT(time, Start)
AND CONVERT(time, #PunchOutDate) <= CONVERT(time, Finish)
Then
'Starts before this range and ends in this range'
Else
''
End as ScenarioB
, Case
When CONVERT(time, #PunchInDate) >= CONVERT(time, Start)
And CONVERT(time, #PunchInDate) <= CONVERT(time, Finish)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, Start)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, Finish)Then
'Starts in this range and ends after the range'
Else ''
END as ScenarioC
, Case
When CONVERT(time, #PunchInDate) <= CONVERT(time, Start)
And CONVERT(time, #PunchInDate) >= CONVERT(time, Finish)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, Start)
And CONVERT(time, #PunchOutDate) >= CONVERT(time, Finish)Then
'Starts before this range and ends after the range'
Else ''
END as ScenarioD
FROM #buckets;
DECLARE #PunchInTime time,
#PunchOutTime time;
SELECT #PunchInTime = CONVERT(time, #PunchInDate),
#PunchOutTime = CONVERT(time, #PunchOutDate);
SELECT x.*,
EffectiveInterval = DATEDIFF(second, x.EffectiveStart, x.EffectiveFinish),
EffectiveIntervalDecimalHourse = DATEDIFF(second, x.EffectiveStart, x.EffectiveFinish) / 3600.
FROM ( SELECT *,
EffectiveStart = CASE WHEN Start < #PunchInTime THEN #PunchInTime ELSE Start END,
EffectiveFinish = CASE WHEN Finish > #PunchOutTime THEN #PunchOutTime ELSE Finish END
FROM #buckets
WHERE Finish >= #PunchInTime
AND Start <= #PunchOutTime
) x;
DROP TABLE #buckets;
--(4 row(s) affected)
--ScenarioA ScenarioB ScenarioC ScenarioD
-------------------------------- ----------------------------------------------- --------------------------------------------- -------------------------------------------------
--(4 row(s) affected)
--Id Start Finish EffectiveStart EffectiveFinish EffectiveInterval EffectiveIntervalDecimalHourse
------------- ---------------- ---------------- ---------------- ---------------- ----------------- ---------------------------------------
--1 08:00:00.0000000 12:00:00.0000000 08:00:00.0000000 12:00:00.0000000 14400 4.000000
--2 12:00:00.0000000 12:30:00.0000000 12:00:00.0000000 12:30:00.0000000 1800 0.500000
--3 12:30:00.0000000 16:00:00.0000000 12:30:00.0000000 16:00:00.0000000 12600 3.500000
--4 16:00:00.0000000 11:59:59.9990000 16:00:00.0000000 11:59:59.9990000 -14401 -4.000277
--(4 row(s) affected)
If you're using SQLServer 2012 you can get the expected result in a single select.
declare #punchInDate datetime = '5/15/2014 7:55'
declare #punchOutDate datetime = '5/15/2014 17:00';
WITH punchTime AS (
SELECT punchIn = cast(#punchInDate AS Time)
, punchOut = cast(#punchOutDate AS Time)
), T AS (
SELECT b.ID
, b.StartTime, b.EndTime
, pt.punchIn, pt.punchOut
, sIn = SUM(CASE WHEN pt.punchIn < b.StartTime
THEN 1
ELSE 0
END) OVER (ORDER BY ID)
, sOut = SUM(CASE WHEN pt.punchOut > b.StartTime
THEN 1
ELSE 0
END) OVER (ORDER BY ID DESC)
FROM bucket b
CROSS JOIN punchTime pt
), C AS (
SELECT ID
, StartTime = CASE sIn WHEN 1 THEN punchIn ELSE StartTime END
, EndTime = CASE sOut WHEN 1 THEN punchOut ELSE EndTime END
FROM T
)
SELECT ID
, StartTime
, EndTime
, hrs = Cast(DateDiff(mi, StartTime, EndTime) / 60.0 AS Decimal(4, 2))
FROM C
ORDER BY ID
SQLFiddle demo (in the demo the punch times are in a table)
Using SUM OVER(ORDER BY) we get a rolling sum.
sIn will be 1 in the first row where StartTime is after punchIn.
sOut will be 1 in the last row where StartTime is before punchOut.
With those pointers is easy to substitute the punch time to the standard bucket time and get the worked hours.

Resources