simplify a SQL case statement in a case expression - sql

How would I simplify this case statement in T-SQL? It provides the desired result, but it's very unwieldy and hard to read. I have to use the inner case statement to convert a Julian date (aka 6 digit number) into a regular date format.
Basically i'm doing a datediff( getdate(), case statement). Getdate() just returns the time now (ie. 2/27/2020) and the case statement converts a julian date (ie. 123456) into a normal date (ie, 1/1/2020).
Here's the expect output if the query was ran today on Feb 27.
Select CASE
WHEN Datediff(day, Getdate(), CASE
WHEN a.wadpl = 0
THEN NULL
ELSE Dateadd(d, Substring(Cast(wadpl AS VARCHAR(6)), 4, 3) - 1, CONVERT(DATETIME, CASE
WHEN LEFT(Cast(wadpl AS VARCHAR(6)), 1) = '1'
THEN '20'
ELSE '21'
END + Substring(Cast(wadpl AS VARCHAR(6)), 2, 2) + '-01-01'))
END) < 0
THEN 'Overdue Now'
WHEN Datediff(day, Getdate(), CASE
WHEN a.wadpl = 0
THEN NULL
ELSE Dateadd(d, Substring(Cast(wadpl AS VARCHAR(6)), 4, 3) - 1, CONVERT(DATETIME, CASE
WHEN LEFT(Cast(wadpl AS VARCHAR(6)), 1) = '1'
THEN '20'
ELSE '21'
END + Substring(Cast(wadpl AS VARCHAR(6)), 2, 2) + '-01-01'))
END) <= 30
THEN 'Coming due in 01-30 days'
ELSE 'Not Overdue'
END [Overdue Status]
FROM Table_X

Here is a easy one to understand, assuming a.wadpl is an integer:
SELECT CASE
WHEN DATEDIFF(DAY, GETDATE(), DATEADD(DAY, a.wadpl % 1000, DATEADD(YEAR,a.wadpl / 1000,'1899-12-31'))) <0 THEN 'Overdue now'
WHEN DATEDIFF(DAY, GETDATE(), DATEADD(DAY, a.wadpl % 1000, DATEADD(YEAR,a.wadpl / 1000,'1899-12-31'))) <= 30 THEN 'Coming due in 01-30 days'
ELSE 'Not Overdue'
END [Overdue Status]
FROM Table_X
or you can simplify by using a subquery (or you can use a WITH):
SELECT CASE
WHEN Age <0 THEN 'Overdue now'
WHEN Age <= 30 THEN 'Coming due in 01-30 days'
ELSE 'Not Overdue'
END [Overdue Status]
FROM (
SELECT DATEDIFF(DAY,GETDATE(),
DATEADD(DAY,wadpl%1000,DATEADD(YEAR,wadpl/1000,'1899-12-31'))) Age, *
FROM Table_X) a
This will of course cause you to do this arithmetic for each row, and you can't easily use any indexes. If you were asking about aggregates, then I would suggest doing the opposite, and pre-calculating the dates and use those in your query instead. You might also want to consider putting a persisted computed column on table_x:
ALTER TABLE TABLE_X
ADD wadpl_dt AS
(DATEADD(DAY,wadpl%1000,DATEADD(YEAR,wadpl/1000,'1899-12-31'))) PERSISTED;
Now you can just refer to table_x.wadpl_dt whenever you want the datetime, and your query would become:
SELECT CASE
WHEN Age <0 THEN 'Overdue now'
WHEN Age <= 30 THEN 'Coming due in 01-30 days'
ELSE 'Not Overdue'
END [Overdue Status]
FROM (
SELECT DATEDIFF(DAY,GETDATE(), wadpl_dt) Age, *
FROM Table_X) a
Here is the easy way to convert a date to what you refer to as the julian date:
SELECT (DATEPART(YEAR,GETDATE())-1900) * 1000 + DATEPART(DAYOFYEAR, GETDATE())
And this is how you can use it:
DECLARE #overdue int;
DECLARE #next30 int;
SET #overdue = (SELECT (DATEPART(YEAR,GETDATE())-1900) * 1000 + DATEPART(DAYOFYEAR, GETDATE()));
SET #next30 = (SELECT (DATEPART(YEAR,GETDATE()+30)-1900) * 1000 + DATEPART(DAYOFYEAR, GETDATE()+30));
SELECT CASE
WHEN wadpl < #overdue THEN 'Overdue now'
WHEN wadpl <= #next30 THEN 'Coming due in 01-30 days'
ELSE 'Not Overdue'
END [Overdue Status]
FROM Table_X

Related

Create view with variable declaration defined by a case expression, generating a dynamic date table based on getdate() reference

I have static date table and a working query that I use to add references fields for comparison between financial periods, I would like to save as a view however the variable declaration is not permitted.
Query is shown below, the specific issues is with declaring "#this_qtr_month" and calculating "Is QTD". The variable determines [Num of Month in QTR] for today.
declare #this_qtr_month as int = case when month(getdate()) in (1,4,7,10) then 1
when month(getdate()) in (2,5,8,11) then 2
when month(getdate()) in (3,6,9,12) then 3
end
select *
,DATEDIFF(day,[Date],getdate()) as 'Days Aged'
,DATEDIFF(ww,[Date],getdate()) as 'Weeks Aged'
,DATEDIFF(qq,[Date],getdate()) as 'QTRs Aged'
,DATEDIFF(yy,[Date],getdate()) as 'Years Aged'
,case when DATEPART(dd,[Date]) <= DATEPART(dd,getdate()) then 'Y' else 'N' end as 'Is MTD'
,case when #this_qtr_month > [Num of Month in QTR] then 'Y'
when #this_qtr_month = [Num of Month in QTR] and DATEPART(dd,[Date]) <= DATEPART(dd,getdate()) then 'Y'
else 'N'
end as 'Is QTD'
,case when getdate() >= DATEFROMPARTS(year(getdate()), [Month Num], day(date_modified_LeapYear)) then 'Y' else 'N' end as 'Is YTD'
from Date_Table_Static
You could write that without a variable:
SELECT *,
DATEDIFF(DAY, [Date], GETDATE()) AS 'Days Aged',
DATEDIFF(ww, [Date], GETDATE()) AS 'Weeks Aged',
DATEDIFF(qq, [Date], GETDATE()) AS 'QTRs Aged',
DATEDIFF(yy, [Date], GETDATE()) AS 'Years Aged',
CASE
WHEN DATEPART(dd, [Date]) <= DATEPART(dd, GETDATE()) THEN
'Y'
ELSE
'N'
END AS 'Is MTD',
CASE
WHEN (CASE
WHEN MONTH(GETDATE()) IN ( 1, 4, 7, 10 ) THEN
1
WHEN MONTH(GETDATE()) IN ( 2, 5, 8, 11 ) THEN
2
WHEN MONTH(GETDATE()) IN ( 3, 6, 9, 12 ) THEN
3
END
) > [Num of Month in QTR] THEN
'Y'
WHEN (CASE
WHEN MONTH(GETDATE()) IN ( 1, 4, 7, 10 ) THEN
1
WHEN MONTH(GETDATE()) IN ( 2, 5, 8, 11 ) THEN
2
WHEN MONTH(GETDATE()) IN ( 3, 6, 9, 12 ) THEN
3
END
) = [Num of Month in QTR]
AND DATEPART(dd, [Date]) <= DATEPART(dd, GETDATE()) THEN
'Y'
ELSE
'N'
END AS 'Is QTD',
CASE
WHEN GETDATE() >= DATEFROMPARTS(YEAR(GETDATE()), [Month Num], DAY(date_modified_LeapYear)) THEN
'Y'
ELSE
'N'
END AS 'Is YTD'
FROM Date_Table_Static;
Or, since all you need with this view is a SELECT, you could write that as a Stored Procedure or TVF (Table Valued Function).
EDIT: I didn't read your expression before, it doesn't need to be that complex:
SELECT *,
DATEDIFF(DAY, [Date], GETDATE()) AS 'Days Aged',
DATEDIFF(ww, [Date], GETDATE()) AS 'Weeks Aged',
DATEDIFF(qq, [Date], GETDATE()) AS 'QTRs Aged',
DATEDIFF(yy, [Date], GETDATE()) AS 'Years Aged',
CASE
WHEN DATEPART(dd, [Date]) <= DATEPART(dd, GETDATE()) THEN
'Y'
ELSE
'N'
END AS 'Is MTD',
CASE
WHEN (Month(Getdate())-1)%3+1 > [Num of Month in QTR] THEN
'Y'
WHEN (Month(Getdate())-1)%3+1 = [Num of Month in QTR]
AND DATEPART(dd, [Date]) <= DATEPART(dd, GETDATE()) THEN
'Y'
ELSE
'N'
END AS 'Is QTD',
CASE
WHEN GETDATE() >= DATEFROMPARTS(YEAR(GETDATE()), [Month Num], DAY(date_modified_LeapYear)) THEN
'Y'
ELSE
'N'
END AS 'Is YTD'
FROM Date_Table_Static;

How to query all rows that are due the next business day?

If it helps, I'm using BytePro which I believe is using T-SQL based on the statement they've generated.
I'm having an issue where when I try retrieving data, I need to switch based on the day of the week. (current day being today's date using GetDate()).
M-T statement:
convert(varchar(10), [Status.SchedFundingDate], 112) <= convert(varchar(10), getdate() + 1, 112)
F statement:
convert(varchar(10), [Status.SchedFundingDate], 112) <= convert(varchar(10), getdate() + 3, 112)
I'd like to combine the two to automate the switch but I get a problem with
CAST(CASE
WHEN DATENAME(DW, GETDATE()) = 'Friday'
AND [Status.SchedFundingDate] <= GETDATE() + 3
THEN 1
WHEN [Status.SchedFundingDate] <= GETDATE() + 1
THEN 1
ELSE 0
END AS BIT)
I get an error:
An expression of non-boolean type specified in a context where a condition is expected, near 'AND'
this is the right syntax :
CAST(CASE WHEN DATENAME(DW, GETDATE()) = 'Friday'
AND [Status].[SchedFundingDate] <= DATEADD(DAY, 3 , GETDATE())
THEN 1
WHEN [Status].[SchedFundingDate] <= DATEADD(DAY, 1 , GETDATE())
THEN 1
ELSE 0
END AS BIT)
db<>fiddle here

Difficulty in getting Shift Value in SQL Query

Currently my table structire for table SHIFT is as follow:
ID Name Start End
1 Shift1 06:00 14:00
2 Shift2 14:00 22:00
3 Shift3 22:00 06:00
Now I pass parameter to this query in hour like 11 or 15 or 22 or 03
For that parameter, I would like to get the result that in which shift the passed hour will reside.
So if I pass 11, it shoud give me Shift1. If I pass 23, it should give me Shift3.
Following query that I wrote works fine for any value from 07 to 21, it is giving me blank value and for obvious reasons.
select * from MII_SHIFT
where '[Param.1]' >= left(START,2) and '[Param.1]' < left(END,2)
Can anyone help me how can I change the query so that I can get proper response for 22,23,00,01,02,03,04,05.
Thanks
SELECT *
FROM shift
WHERE
( left(START,2) > left(END,2)
AND ('[Param.1]' >= left(START,2) OR '[Param.1]' < left(END,2))
)
OR ( left(START,2) < left(END,2)
AND '[Param.1]' >= left(START,2) AND '[Param.1]' < left(END,2)
)
I answer a similar answer a litle time ago.
Shorts start < end (5-9): the value need be between start and end
Jacket start > end (10-4): the value is < start or > end
Assuming the values are stored as strings, then this is pretty easy:
select s.*
from shifts s
where (start < end and right('00' + #param1, 2) >= start and right('00' + #param1, 2) < end) or
(start > end and (right('00' + #param1, 2) >= start or right('00' + #param1, 2) < end))
This assumes that #param1 is a string. The right() is used to left pad the string with zeroes. If that is already true, then the code would be even simpler.
EDIT:
With padding, this simplifies to:
select s.*
from shifts s
where (start < end and #param1 >= start and #param1< end) or
(start > end and (#param1 >= start or #param1 < end))
Simplest way is most likely to convert the times into dates, and if the end date is earlier than start, then add one day. You could use time datatype as input too, instead of just hour, but this is now an example with int:
declare #hour int, #date datetime
set #hour = 3
set #date = convert(datetime, convert(varchar(2), #hour) + ':00', 108)
select Name
from (
select Name,
[Start] as Start1,
case when [End] < [Start] then dateadd(day, 1, [End]) else [End] End as End1,
case when [End] < [Start] then dateadd(day, -1, [Start]) else [Start] End as Start2,
[End] as End2
from (
select Name, convert(datetime, [Start], 108) as [Start], convert(datetime, [End], 108) as [End]
from Table1
) X
) Y
where ((Start1 <= #date and #date < End1) or (Start2 <= #date and #date < End2))
Edit: added 2nd start / end columns to the derived table to handle second part of the shift.
Example in SQL Fiddle
Thankk you all. With the hep from all of your refrences, I was able to build the query which gave me appropriate results.
Query is as foolow:
SELECT Name FROM SHIFT WHERE
(LEFT(START,2) < LEFT(END,2) AND '[Param.1]' >= LEFT(START,2) AND '[Param.1]' < LEFT(END,2))
OR
(LEFT(START,2) > LEFT(END,2) AND ('[Param.1]' >= LEFT(START,2) OR '[Param.1]' < LEFT(END,2)))

Running a select statement for each case

I'm a complete SQL novice. I want to display a simple grid.....
Sign up......>30.....30-60.....60-90.....>90.....Total
Feb...............4..........30........... 6 ......... 0 .......40
Mar ............. 0 .........11 ...........1 ..........4 .......16
Apr
May etc
Jun etc
Where total represents the total of number of customer sign ups per month, and where the likes of '30-60' represents the number sign ups on the date 30-60 days since today's date
The query I am using is as follows......
SELECT case MONTH(Datecreated)
when 2 then 'Febrary'
when 3 then 'March'
when 4 then 'April'
when 5 then 'May'
when 6 then 'June'
end as 'Signup Month',
(select count(*) from WS_USER_DETAILS where datediff(day, datecreated, GETUTCDATE()) < 30 ) as '<= 30 days',
(select count(*) from WS_USER_DETAILS where datediff(day, datecreated, GETUTCDATE()) between 30 and 60) as '<= 60 days',
(select count(*) from WS_USER_DETAILS where datediff(day, datecreated, GETUTCDATE()) between 60 and 90) as '<= 90 days',
(select count(*) from WS_USER_DETAILS where datediff(day, datecreated, GETUTCDATE()) >90) as '<= 120 days',
count(userid) AS 'Total' FROM WS_USER_DETAILS
group by MONTH(Datecreated)
The problem I have is....
I want to run the select statements for each month. However it populates all months. For example......
Sign up......>30.....30-60.....60-90.....>90.....Total
Feb...............0..........11........... 1 ......... 4 .......40
Mar ............. 0 .........11 ...........1 ..........4 .......16
If possible can someone advise me on how to run the select statements for each month? Thanks
First select the time span per record, then group and count. Be careful not to have overlapping ranges.
select
monthname,
isnull(sum(lessthan30),0) as lt30,
isnull(sum(between30and59),0) as btw30a59,
isnull(sum(between60and89),0) as btw60a89,
isnull(sum(between90and119),0) as btw90a119,
isnull(sum(morethan119),0) as mt119,
isnull(sum(lessthan30),0) + isnull(sum(between30and59),0) + isnull(sum(between60and89),0) + isnull(sum(between90and119),0) + isnull(sum(morethan119),0) as total
from
(
select
month(datecreated) as mon,
datename(month,datecreated) as monthname,
case when datediff(day, datecreated, GETUTCDATE()) < 30 then 1 else 0 end as lessthan30,
case when datediff(day, datecreated, GETUTCDATE()) between 30 and 59 then 1 else 0 end as between30and59,
case when datediff(day, datecreated, GETUTCDATE()) between 60 and 89 then 1 else 0 end as between60and89,
case when datediff(day, datecreated, GETUTCDATE()) between 90 and 119 then 1 else 0 end as between90and119,
case when datediff(day, datecreated, GETUTCDATE()) >= 120 then 1 else 0 end as morethan119
from WS_USER_DETAILS
) as dummy
group by monthname, mon
order by mon;
I think something like this would fit your needs better. You can use Sum Function and a case statement to work like a count if.
SELECT
DATENAME(MM,Datecreated), --This Function automatically gets the name of the month
--Here is where you can use case statements like a count if.
SUM(CASE WHEN DATEDIFF(dd, datecreated, GETUTCDATE()) < 30 THEN 1 ELSE 0 END) '<= 30 days',
SUM(CASE WHEN DATEDIFF(dd, datecreated, GETUTCDATE()) < 60 THEN 1 ELSE 0 END) '<= 60 days',
SUM(CASE WHEN DATEDIFF(dd, datecreated, GETUTCDATE()) < 90 THEN 1 ELSE 0 END) '<= 90 days',
SUM(CASE WHEN DATEDIFF(dd, datecreated, GETUTCDATE()) < 120 THEN 1 ELSE 0 END) '<= 120 days'
FROM WS_USER_DETAILS
GROUP BY Datepart(MM, Datecreated)
Sources:
http://msdn.microsoft.com/en-us/library/ms189794.aspx
http://msdn.microsoft.com/en-us/library/ms174420.aspx
http://msdn.microsoft.com/en-us/library/ms174420.aspx
Based on #ThorstenKettner but adding performance by removing function calls and adding sergability
declare #today date
declare #_30 date
declare #_31 date
declare #_59 date
declare #_60 date
declare #_89 date
declare #_90 date
declare #_119 date
declare #_120 date
set #today = GETUTCDATE()
set #_30 = datediff(day, #today, 30)
set #_31 = datediff(day, #today, 31)
set #_59 = datediff(day, #today, 59)
set #_60 = datediff(day, #today, 60)
set #_89 = datediff(day, #today, 89)
set #_90 = datediff(day, #today, 90)
set #_119 = datediff(day, #today, 119)
set #_120 = datediff(day, #today, 120)
select
monthname,
isnull(sum(lessthan30),0) as lt30,
isnull(sum(between30and59),0) as btw30a59,
isnull(sum(between60and89),0) as btw60a89,
isnull(sum(between90and119),0) as btw90a119,
isnull(sum(morethan119),0) as mt119,
isnull(sum(lessthan30),0) + isnull(sum(between30and59),0) + isnull(sum(between60and89),0) + isnull(sum(between90and119),0) + isnull(sum(morethan119),0) as total
from
(
select
month(datecreated) as mon,
datename(month,datecreated) as monthname,
case when datecreated < #_30 then 1 else 0 end as lessthan30,
case when datecreated between #_30 and #_59 then 1 else 0 end as between30and59,
case when datecreated between #_60 and #_89 then 1 else 0 end as between60and89,
case when datecreated between #_90 and #_119 then 1 else 0 end as between90and119,
case when datecreated >= #_120 then 1 else 0 end as morethan119
from WS_USER_DETAILS
)as dummy
group by monthname, mon
order by mon;

SQL DateDiff advanced usage?

I need to calculate the DateDiff (hours) between two dates, but only during business-hours (8:30 - 16:00, no weekends). This result will then be put into the Reaction_Time column as per the example below.
ID Date Reaction_Time Overdue
1 29.04.2003 15:00:00
1 30.04.2003 11:00:00 3:30
2 30.04.2003 14:00:00
2 01.05.2003 14:00:00 7:30 YES
*Note: I didn't check to see if the dates in example were holidays.
I'm using SQL Server 2005
This will be combined with a bigger query, but for now all I need is this to get started, I'll try to figure out how to put it all together on my own. Thanks for the help!
Edit: Hey, thanks everyone for the replies. But due to the obvious complexity of a solution on SQL side, it was decided we would do this in Excel instead as that's where the report will be moved anyway. Sorry for the trouble, but I really figured it would be simpler than this. As it is, we just don't have the time.
I would recommend building a user defined function that calculates the date difference in business hours according to your rules.
SELECT
Id,
MIN(Date) DateStarted,
MAX(Date) DateCompleted,
dbo.udfDateDiffBusinessHours(MIN(Date), MAX(Date)) ReactionTime
FROM
Incident
GROUP BY
Id
I'm not sure where your Overdue value comes from, so I left it off in my example.
In a function you can write way more expressive SQL than in a query, and you don't clog your query with business rules, making it hard to maintain.
Also a function can easily be reused. Extending it to include support for holidays (I'm thinking of a Holidays table here) would not be too hard. Further refinements are possible without the need to change hard to read nested SELECT/CASE WHEN constructs, which would be the alternative.
If I have time today, I'll look into writing an example function.
EDIT: Here is something with bells and whistles, calculating around weekends transparently:
ALTER FUNCTION dbo.udfDateDiffBusinessHours (
#date1 DATETIME,
#date2 DATETIME
) RETURNS DATETIME AS
BEGIN
DECLARE #sat INT
DECLARE #sun INT
DECLARE #workday_s INT
DECLARE #workday_e INT
DECLARE #basedate1 DATETIME
DECLARE #basedate2 DATETIME
DECLARE #calcdate1 DATETIME
DECLARE #calcdate2 DATETIME
DECLARE #cworkdays INT
DECLARE #cweekends INT
DECLARE #returnval INT
SET #workday_s = 510 -- work day start: 8.5 hours
SET #workday_e = 960 -- work day end: 16.0 hours
-- calculate Saturday and Sunday dependent on SET DATEFIRST option
SET #sat = CASE ##DATEFIRST WHEN 7 THEN 7 ELSE 7 - ##DATEFIRST END
SET #sun = CASE ##DATEFIRST WHEN 7 THEN 1 ELSE #sat + 1 END
SET #calcdate1 = #date1
SET #calcdate2 = #date2
-- #date1: assume next day if start was after end of workday
SET #basedate1 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate1))
SET #calcdate1 = CASE WHEN DATEDIFF(mi, #basedate1, #calcdate1) > #workday_e
THEN #basedate1 + 1
ELSE #calcdate1
END
-- #date1: if Saturday or Sunday, make it next Monday
SET #basedate1 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate1))
SET #calcdate1 = CASE DATEPART(dw, #basedate1)
WHEN #sat THEN #basedate1 + 2
WHEN #sun THEN #basedate1 + 1
ELSE #calcdate1
END
-- #date1: assume #workday_s as the minimum start time
SET #basedate1 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate1))
SET #calcdate1 = CASE WHEN DATEDIFF(mi, #basedate1, #calcdate1) < #workday_s
THEN DATEADD(mi, #workday_s, #basedate1)
ELSE #calcdate1
END
-- #date2: assume previous day if end was before start of workday
SET #basedate2 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate2))
SET #calcdate2 = CASE WHEN DATEDIFF(mi, #basedate2, #calcdate2) < #workday_s
THEN #basedate2 - 1
ELSE #calcdate2
END
-- #date2: if Saturday or Sunday, make it previous Friday
SET #basedate2 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate2))
SET #calcdate2 = CASE DATEPART(dw, #calcdate2)
WHEN #sat THEN #basedate2 - 0.00001
WHEN #sun THEN #basedate2 - 1.00001
ELSE #date2
END
-- #date2: assume #workday_e as the maximum end time
SET #basedate2 = DATEADD(dd, 0, DATEDIFF(dd, 0, #calcdate2))
SET #calcdate2 = CASE WHEN DATEDIFF(mi, #basedate2, #calcdate2) > #workday_e
THEN DATEADD(mi, #workday_e, #basedate2)
ELSE #calcdate2
END
-- count full work days (subtract Saturdays and Sundays)
SET #cworkdays = DATEDIFF(dd, #basedate1, #basedate2)
SET #cweekends = #cworkdays / 7
SET #cworkdays = #cworkdays - #cweekends * 2
-- calculate effective duration in minutes
SET #returnval = #cworkdays * (#workday_e - #workday_s)
+ #workday_e - DATEDIFF(mi, #basedate1, #calcdate1)
+ DATEDIFF(mi, #basedate2, #calcdate2) - #workday_e
-- return duration as an offset in minutes from date 0
RETURN DATEADD(mi, #returnval, 0)
END
The function returns a DATETIME value meant as an offset from date 0 (which is "1900-01-01 00:00:00"). So for example a timespan of 8:00 hours would be "1900-01-01 08:00:00" and 25 hours would be "1900-01-02 01:00:00". The function result is the time difference in business hours between two dates. No special handling/support for overtime.
SELECT dbo.udfDateDiffBusinessHours('2003-04-29 15:00:00', '2003-04-30 11:00:00')
--> 1900-01-01 03:30:00.000
SELECT dbo.udfDateDiffBusinessHours('2003-04-30 14:00:00', '2003-05-01 14:00:00')
--> 1900-01-01 07:30:00.000
The function assumes the start of the next available work day (08:30 h) when the #date1 is off-hours, and the end of the previous available work day (16:00 h) when #date2 is off-hours.
"next/previous available" means:
if #date1 is '2009-02-06 07:00:00' (Fri), it will become '2009-02-06 08:30:00' (Fri)
if #date1 is '2009-02-06 19:00:00' (Fri), it will become '2009-02-09 08:30:00' (Mon)
if #date2 is '2009-02-09 07:00:00' (Mon), it will become '2009-02-06 16:00:00' (Fri)
if #date2 is '2009-02-09 19:00:00' (Mon), it will become '2009-02-09 16:00:00' (Mon)
DECLARE #BusHourStart DATETIME, #BusHourEnd DATETIME
SELECT #BusHourStart = '08:30:00', #BusHourEnd = '16:00:00'
DECLARE #BusMinutesStart INT, #BusMinutesEnd INT
SELECT #BusMinutesStart = DATEPART(minute,#BusHourStart)+DATEPART(hour,#BusHourStart)*60,
#BusMinutesEnd = DATEPART(minute,#BusHourEnd)+DATEPART(hour,#BusHourEnd)*60
DECLARE #Dates2 TABLE (ID INT, DateStart DATETIME, DateEnd DATETIME)
INSERT INTO #Dates2
SELECT 1, '15:00:00 04/29/2003', '11:00:00 04/30/2003' UNION
SELECT 2, '14:00:00 04/30/2003', '14:00:00 05/01/2003' UNION
SELECT 3, '14:00:00 05/02/2003', '14:00:00 05/06/2003' UNION
SELECT 4, '14:00:00 05/02/2003', '14:00:00 05/04/2003' UNION
SELECT 5, '07:00:00 05/02/2003', '14:00:00 05/02/2003' UNION
SELECT 6, '14:00:00 05/02/2003', '23:00:00 05/02/2003' UNION
SELECT 7, '07:00:00 05/02/2003', '08:00:00 05/02/2003' UNION
SELECT 8, '22:00:00 05/02/2003', '23:00:00 05/03/2003' UNION
SELECT 9, '08:00:00 05/03/2003', '23:00:00 05/04/2003' UNION
SELECT 10, '07:00:00 05/02/2003', '23:00:00 05/02/2003'
-- SET DATEFIRST to U.S. English default value of 7.
SET DATEFIRST 7
SELECT ID, DateStart, DateEnd, CONVERT(VARCHAR, Minutes/60) +':'+ CONVERT(VARCHAR, Minutes % 60) AS ReactionTime
FROM (
SELECT ID, DateStart, DateEnd, Overtime,
CASE
WHEN DayDiff = 0 THEN
CASE
WHEN (MinutesEnd - MinutesStart - Overtime) > 0 THEN (MinutesEnd - MinutesStart - Overtime)
ELSE 0
END
WHEN DayDiff > 0 THEN
CASE
WHEN (StartPart + EndPart - Overtime) > 0 THEN (StartPart + EndPart - Overtime)
ELSE 0
END + DayPart
ELSE 0
END AS Minutes
FROM(
SELECT ID, DateStart, DateEnd, DayDiff, MinutesStart, MinutesEnd,
CASE WHEN(#BusMinutesStart - MinutesStart) > 0 THEN (#BusMinutesStart - MinutesStart) ELSE 0 END +
CASE WHEN(MinutesEnd - #BusMinutesEnd) > 0 THEN (MinutesEnd - #BusMinutesEnd) ELSE 0 END AS Overtime,
CASE WHEN(#BusMinutesEnd - MinutesStart) > 0 THEN (#BusMinutesEnd - MinutesStart) ELSE 0 END AS StartPart,
CASE WHEN(MinutesEnd - #BusMinutesStart) > 0 THEN (MinutesEnd - #BusMinutesStart) ELSE 0 END AS EndPart,
CASE WHEN DayDiff > 1 THEN (#BusMinutesEnd - #BusMinutesStart)*(DayDiff - 1) ELSE 0 END AS DayPart
FROM (
SELECT DATEDIFF(d,DateStart, DateEnd) AS DayDiff, ID, DateStart, DateEnd,
DATEPART(minute,DateStart)+DATEPART(hour,DateStart)*60 AS MinutesStart,
DATEPART(minute,DateEnd)+DATEPART(hour,DateEnd)*60 AS MinutesEnd
FROM (
SELECT ID,
CASE
WHEN DATEPART(dw, DateStart) = 7
THEN DATEADD(SECOND, 1, DATEADD(DAY, DATEDIFF(DAY, 0, DateStart), 2))
WHEN DATEPART(dw, DateStart) = 1
THEN DATEADD(SECOND, 1, DATEADD(DAY, DATEDIFF(DAY, 0, DateStart), 1))
ELSE DateStart END AS DateStart,
CASE
WHEN DATEPART(dw, DateEnd) = 7
THEN DATEADD(SECOND, -1, DATEADD(DAY, DATEDIFF(DAY, 0, DateEnd), 0))
WHEN DATEPART(dw, DateEnd) = 1
THEN DATEADD(SECOND, -1, DATEADD(DAY, DATEDIFF(DAY, 0, DateEnd), -1))
ELSE DateEnd END AS DateEnd FROM #Dates2
)Weekends
)InMinutes
)Overtime
)Calculation
select datediff(hh,#date1,#date2) - 16.5*(datediff(dd,#date1,#date2))
The only catch is that it will give you 3:30 as 3.5 hours but you can fix that easily.
Use this code : to find out weekend in between dates
(
DATEDIFF(dd, open_date, zassignment_date) + 1
- ( (DATEDIFF(dd, open_date, zassignment_date) + 1)
-(DATEDIFF(wk, open_date, zassignment_date) * 2)
-(CASE WHEN DATENAME(dw, open_date) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, zassignment_date) = 'Saturday' THEN 1 ELSE 0 END) )) wk_end
Assuming you have a reference-table of the working days (and their hours), then I would use a 3 stage approach (pseudo-sql)
(first preclude the "all in one day" trivial example, since that simplifies the logic)
-- days that are neither the start nor end (full days)
SELECT #FullDayHours = SUM(day start to day end)
FROM reference-calendar
WHERE Start >= midnight-after-start and End <= midnight-before-end
-- time after the [query start] to the end of the first working day
SELECT #FirstDayHours = [query start] to day end
FROM reference-calandar
WHERE start day
-- time from the start of the last working day to the [query end]
SELECT #LastDayHours = day start to [query end]
FROM reference-calendar
WHERE end-day
IF #FirstDayHours < 0 SET #FirstDayHours = 0 -- starts outside working time
IF #LastDayHours < 0 SET #LastDayHours = 0 -- ends outside working time
PRINT #FirstDayHours + #FullDayHours + #LastDayHours
Obviously it is a bit hard to do properly without more context...
This function will give you the difference in business hours between two given times. This will return the difference in minutes or hours based on the date part parameter.
CREATE FUNCTION [dbo].[fnBusinessHoursDateDiff] (#StartTime SmallDatetime, #EndTime SmallDateTime, #DatePart varchar(2)) RETURNS DECIMAL (10,2)
AS
BEGIN
DECLARE #Minutes bigint
, #FinalNumber Decimal(10,2)
-- // Create Minute By minute table for CTE
-- ===========================================================
;WITH cteInputHours (StartTime, EndTime, NextTime) AS (
SELECT #StartTime
, #EndTime
, dateadd(mi, 1, #StartTime)
),
cteBusinessMinutes (TimeOfDay, [isBusHour], NextTime) AS(
SELECT StartTime [TimeOfDay]
, case when datepart(dw, StartTime) between 2 and 6 and convert(time,StartTime) between '08:30' and '15:59' then 1 else 0 end [isBusHour]
, dateadd(mi, 1, #StartTime) [NextTime]
FROM cteInputHours
UNION ALL
SELECT dateadd(mi, 1, (a.TimeOfDay)) [TimeOfDay]
, case when datepart(dw, a.TimeOfDay) between 2 and 6 and convert(time,dateadd(mi, 1, (a.TimeOfDay)) ) between '08:30' and '15:59' then 1 else 0 end [isBusHour]
, dateadd(mi, 2, (a.TimeOfDay)) NextTime
FROM cteBusinessMinutes a
WHERE dateadd(mi, 1, (a.TimeOfDay)) < #EndTime
)
SELECT #Minutes = count(*)
FROM cteBusinessMinutes
WHERE isBusHour = 1
OPTION (MAXRECURSION 0);
-- // Final Select
-- ===========================================================
SELECT #FinalNumber = #Minutes / (case when #DatePart = 'hh' then 60.00 else 1 end)
RETURN #FinalNumber
END