T-SQL get number of working days between 2 dates - sql

I want to calculate the number of working days between 2 given dates. For example if I want to calculate the working days between 2013-01-10 and 2013-01-15, the result must be 3 working days (I don't take into consideration the last day in that interval and I subtract the Saturdays and Sundays). I have the following code that works for most of the cases, except the one in my example.
SELECT (DATEDIFF(day, '2013-01-10', '2013-01-15'))
- (CASE WHEN DATENAME(weekday, '2013-01-10') = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(weekday, DATEADD(day, -1, '2013-01-15')) = 'Saturday' THEN 1 ELSE 0 END)
How can I accomplish this? Do I have to go through all the days and check them? Or is there an easy way to do this.

Please, please, please use a calendar table. SQL Server doesn't know anything about national holidays, company events, natural disasters, etc. A calendar table is fairly easy to build, takes an extremely small amount of space, and will be in memory if it is referenced enough.
Here is an example that creates a calendar table with 30 years of dates (2000 -> 2029) but requires only 200 KB on disk (136 KB if you use page compression). That is almost guaranteed to be less than the memory grant required to process some CTE or other set at runtime.
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2029-12-31';
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 12
AND DAY(dt) = 25
AND IsWorkDay = 1;
-- continue with other holidays, known company events, etc.
Now the query you're after is quite simple to write:
SELECT COUNT(*) FROM dbo.Calendar
WHERE dt >= '20130110'
AND dt < '20130115'
AND IsWorkDay = 1;
More info on calendar tables:
http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html
More info on generating sets without loops:
http://www.sqlperformance.com/tag/date-ranges
Also beware of little things like relying on the English output of DATENAME. I've seen several applications break because some users had a different language setting, and if you're relying on WEEKDAY be sure you set your DATEFIRST setting appropriately...

For stuff like this i tend to maintain a calendar table that also includes bank holidays etc.
The script i use for this is as follows (Note that i didnt write it # i forget where i found it)
SET DATEFIRST 1
SET NOCOUNT ON
GO
--Create ISO week Function (thanks BOL)
CREATE FUNCTION ISOweek ( #DATE DATETIME )
RETURNS INT
AS
BEGIN
DECLARE #ISOweek INT
SET #ISOweek = DATEPART(wk, #DATE) + 1 - DATEPART(wk, CAST(DATEPART(yy, #DATE) AS CHAR(4)) + '0104')
--Special cases: Jan 1-3 may belong to the previous year
IF ( #ISOweek = 0 )
SET #ISOweek = dbo.ISOweek(CAST(DATEPART(yy, #DATE) - 1 AS CHAR(4)) + '12' + CAST(24 + DATEPART(DAY, #DATE) AS CHAR(2))) + 1
--Special case: Dec 29-31 may belong to the next year
IF ( ( DATEPART(mm, #DATE) = 12 )
AND ( ( DATEPART(dd, #DATE) - DATEPART(dw, #DATE) ) >= 28 )
)
SET #ISOweek = 1
RETURN(#ISOweek)
END
GO
--END ISOweek
--CREATE Easter algorithm function
--Thanks to Rockmoose (http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=45689)
CREATE FUNCTION fnDLA_GetEasterdate ( #year INT )
RETURNS CHAR(8)
AS
BEGIN
-- Easter date algorithm of Delambre
DECLARE #A INT ,
#B INT ,
#C INT ,
#D INT ,
#E INT ,
#F INT ,
#G INT ,
#H INT ,
#I INT ,
#K INT ,
#L INT ,
#M INT ,
#O INT ,
#R INT
SET #A = #YEAR % 19
SET #B = #YEAR / 100
SET #C = #YEAR % 100
SET #D = #B / 4
SET #E = #B % 4
SET #F = ( #B + 8 ) / 25
SET #G = ( #B - #F + 1 ) / 3
SET #H = ( 19 * #A + #B - #D - #G + 15 ) % 30
SET #I = #C / 4
SET #K = #C % 4
SET #L = ( 32 + 2 * #E + 2 * #I - #H - #K ) % 7
SET #M = ( #A + 11 * #H + 22 * #L ) / 451
SET #O = 22 + #H + #L - 7 * #M
IF #O > 31
BEGIN
SET #R = #O - 31 + 400 + #YEAR * 10000
END
ELSE
BEGIN
SET #R = #O + 300 + #YEAR * 10000
END
RETURN #R
END
GO
--END fnDLA_GetEasterdate
--Create the table
CREATE TABLE MyDateTable
(
FullDate DATETIME NOT NULL
CONSTRAINT PK_FullDate PRIMARY KEY CLUSTERED ,
Period INT ,
ISOWeek INT ,
WorkingDay VARCHAR(1) CONSTRAINT DF_MyDateTable_WorkDay DEFAULT 'Y'
)
GO
--End table create
--Populate table with required dates
DECLARE #DateFrom DATETIME ,
#DateTo DATETIME ,
#Period INT
SET #DateFrom = CONVERT(DATETIME, '20000101')
--yyyymmdd (1st Jan 2000) amend as required
SET #DateTo = CONVERT(DATETIME, '20991231')
--yyyymmdd (31st Dec 2099) amend as required
WHILE #DateFrom <= #DateTo
BEGIN
SET #Period = CONVERT(INT, LEFT(CONVERT(VARCHAR(10), #DateFrom, 112), 6))
INSERT MyDateTable
( FullDate ,
Period ,
ISOWeek
)
SELECT #DateFrom ,
#Period ,
dbo.ISOweek(#DateFrom)
SET #DateFrom = DATEADD(dd, +1, #DateFrom)
END
GO
--End population
/* Start of WorkingDays UPDATE */
UPDATE MyDateTable
SET WorkingDay = 'B' --B = Bank Holiday
--------------------------------EASTER---------------------------------------------
WHERE FullDate = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate)))) --Good Friday
OR FullDate = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate))))
--Easter Monday
GO
UPDATE MyDateTable
SET WorkingDay = 'B'
--------------------------------NEW YEAR-------------------------------------------
WHERE FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 1
AND DATEPART(dw, FullDate) NOT IN ( 6, 7 )
GROUP BY DATEPART(yy, FullDate) )
---------------------MAY BANK HOLIDAYS(Always Monday)------------------------------
OR FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------AUGUST BANK HOLIDAY(Always Monday)------------------------------
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 8
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------XMAS(Move to next working day if on Sat/Sun)--------------------
OR FullDate IN ( SELECT CASE WHEN DATEPART(dw, FullDate) IN ( 6, 7 ) THEN DATEADD(dd, +2, FullDate)
ELSE FullDate
END
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 12
AND DATEPART(dd, FullDate) IN ( 25, 26 ) )
GO
---------------------------------------WEEKENDS--------------------------------------
UPDATE MyDateTable
SET WorkingDay = 'N'
WHERE DATEPART(dw, FullDate) IN ( 6, 7 )
GO
/* End of WorkingDays UPDATE */
--SELECT * FROM MyDateTable ORDER BY 1
DROP FUNCTION fnDLA_GetEasterdate
DROP FUNCTION ISOweek
--DROP TABLE MyDateTable
SET NOCOUNT OFF
Once you have created the table, finding the number of working days is easy peasy:
SELECT COUNT(FullDate) AS WorkingDays
FROM dbo.tbl_WorkingDays
WHERE WorkingDay = 'Y'
AND FullDate >= CONVERT(DATETIME, '10/01/2013', 103)
AND FullDate < CONVERT(DATETIME, '15/01/2013', 103)
Note that this script includes UK bank holidays, i'm not sure what region you're in.

Here's a simple function that counts working days not including Saturday and Sunday (when counting holidays isn't necessary):
CREATE FUNCTION dbo.udf_GetBusinessDays (
#START_DATE DATE,
#END_DATE DATE
)
RETURNS INT
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #NUMBER_OF_DAYS INT = 0;
DECLARE #DAY_COUNTER INT = 0;
DECLARE #BUSINESS_DAYS INT = 0;
DECLARE #CURRENT_DATE DATE;
DECLARE #DAYNAME NVARCHAR(9)
SET #NUMBER_OF_DAYS = DATEDIFF(DAY, #START_DATE, #END_DATE);
WHILE #DAY_COUNTER <= #NUMBER_OF_DAYS
BEGIN
SET #CURRENT_DATE = DATEADD(DAY, #DAY_COUNTER, #START_DATE)
SET #DAYNAME = DATENAME(WEEKDAY, #CURRENT_DATE)
SET #DAY_COUNTER += 1
IF #DAYNAME = N'Saturday' OR #DAYNAME = N'Sunday'
BEGIN
CONTINUE
END
ELSE
BEGIN
SET #BUSINESS_DAYS += 1
END
END
RETURN #BUSINESS_DAYS
END
GO

This is the method I normally use (When not using a calendar table):
DECLARE #T TABLE (Date1 DATE, Date2 DATE);
INSERT #T VALUES ('20130110', '20130115'), ('20120101', '20130101'), ('20120611', '20120701');
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd
If like I do you have a table with holidays in you can add this in too:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
AND NOT EXISTS
( SELECT 1
FROM HolidayTable ht
WHERE ht.Date = DATEADD(DAY, s.number, t.Date1)
)
) wd
The above will only work if your dates are within 2047 days of each other, if you are likely to be calculating larger date ranges you can use this:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM ( SELECT [Number] = ROW_NUMBER() OVER(ORDER BY s.number)
FROM Master..spt_values s
CROSS JOIN Master..spt_values s2
) s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd

I did my code in SQL SERVER 2008 (MS SQL) . It works fine for me. I hope it will help you.
DECLARE #COUNTS int,
#STARTDATE date,
#ENDDATE date
SET #STARTDATE ='01/21/2013' /*Start date in mm/dd/yyy */
SET #ENDDATE ='01/26/2013' /*End date in mm/dd/yyy */
SET #COUNTS=0
WHILE (#STARTDATE<=#ENDDATE)
BEGIN
/*Check for holidays*/
IF ( DATENAME(weekday,#STARTDATE)<>'Saturday' and DATENAME(weekday,#STARTDATE)<>'Sunday')
BEGIN
SET #COUNTS=#COUNTS+1
END
SET #STARTDATE=DATEADD(day,1,#STARTDATE)
END
/* Display the no of working days */
SELECT #COUNTS

By Combining #Aaron Bertrand's answer and the Easter Calculation from #HeavenCore's and adding some code of my own, this code creates a calendar from 2000 to 2049 that includes UK (England) Bank Holidays. Usage and notes as per Aaron's answer:
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2049-12-31';
-- Insert statements for procedure here
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 12 and
(DAY(dt) in (25,26) or
(DAY(dt) in (27, 28) and DATEPART(WEEKDAY, dt) IN (1,2)) );
-- New Year
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 1 AND
( DAY(dt) = 1 or (DAY(dt) IN (2,3) AND DATEPART(WEEKDAY, dt)=1 ));
-- Easter
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE dt = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Good Friday
OR dt = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Easter Monday
-- May Day (first Monday in May)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)<8;
-- Spring Bank Holiday (last Monday in May apart from 2022 when moved to include Platinum Jubilee bank holiday)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE
(YEAR(dt)=2022 and MONTH(dt) = 6 AND DAY(dt) IN (2,3)) OR
(YEAR(dt)<>2022 and MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24);
-- Summer Bank Holiday (last Monday in August)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 8 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24;

Related

How To Get Elapsed Time Between Two Dates, Ignoring Some Time Range?

I have a table with a DATETIME column 'Start' and a DATETIME column 'End'. I want to return the number of minutes between the start and the end (End is always after than Start). Usually I'd just use 'DateDiff()' but this time I need to exclude another date range. For example - From Tuesday at 9am until Wednesday at 6pm, of each week, should be ignored.
If a row has a Start of Tuesday at 8am and an End of Wednesday at 7pm - the elapsed time should be two hours (120 minutes) - because of the ignored date range.
I'm having trouble coming up with a decent way of doing this and my searching online hasn't found quite what I'm looking for. Can someone help me along?
Try This:
--total time span to calculate difference
DECLARE #StartDate DATETIME = '2015-11-10 8:00:00AM',
#EndDate DATETIME = '2015-11-11 7:00:00PM'
--get the day of week (-1 because sunday is counted as first weekday)
DECLARE #StartDayOfWeek INT = (SELECT DATEPART(WEEKDAY, #StartDate)) -1
DECLARE #EndDayOfWeek INT = (SELECT DATEPART(WEEKDAY, #EndDate)) -1
--set the time span to exclude
DECLARE #InitialDOWToExclude TINYINT = 2
DECLARE #InitialTODToExclude VARCHAR(100) = '9:00:00 AM'
DECLARE #EndDOWToExclude TINYINT = 3
DECLARE #EndTODToExclude VARCHAR(100) = '6:00:00 PM'
--this will be the final output in hours
DECLARE #ElapsedHours INT = (SELECT DATEDIFF(HOUR, #StartDate, #EndDate))
DECLARE #WeeksBetween INT = (SELECT DATEDIFF(WEEK, #StartDate, #EndDate))
DECLARE #Iterator INT = 0
WHILE (#Iterator <= #WeeksBetween)
BEGIN
DECLARE #InitialDaysBetween INT = #StartDayOfWeek - #InitialDOWToExclude
DECLARE #StartDateToExclude DATETIME = (SELECT DATEADD(DAY, #InitialDaysBetween, DATEADD(WEEK, #Iterator, #StartDate)))
SET #StartDateToExclude =CAST(DATEPART(YEAR, #StartDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(MONTH, #StartDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(DAY, #StartDateToExclude) AS VARCHAR(100))
+ ' '
+ CAST(#InitialTODToExclude AS VARCHAR(100))
DECLARE #EndDaysBetween INT = #EndDayOfWeek - #EndDOWToExclude
DECLARE #EndDateToExclude DATETIME = (SELECT DATEADD(DAY, #EndDaysBetween, DATEADD(WEEK, #Iterator, #EndDate)))
SET #EndDateToExclude =CAST(DATEPART(YEAR, #EndDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(MONTH, #EndDateToExclude) AS VARCHAR(100))
+ CAST(DATEPART(DAY, #EndDateToExclude) AS VARCHAR(100))
+ ' '
+ CAST(#EndTODToExclude AS VARCHAR(100))
SET #ElapsedHours = #ElapsedHours - DATEDIFF(HOUR, #StartDateToExclude, #EndDateToExclude)
SET #Iterator = #Iterator + 1
END
SELECT #ElapsedHours
This might get you pretty close..
DECLARE #Table1 TABLE ([Id] INT, [Start] DATETIME, [End] DATETIME)
INSERT INTO #Table1 VALUES
(1, '2015-11-08 00:00:00', '2015-11-10 21:45:38'),
(2, '2015-11-09 00:00:00', '2015-11-11 21:45:38')
;
-- hours to exclude
WITH excludeCTE AS
(
SELECT *
FROM (VALUES('Tuesday', 9, 0), ('Wednesday', 0, 0)) AS T([Day], [Hour], [Amount])
UNION ALL
SELECT [Day], [Hour] + 1, [Amount]
FROM excludeCTE
WHERE ([Day] = 'Tuesday' AND [Hour] < 23) OR ([Day] = 'Wednesday' AND [Hour] < 18)
),
-- all hours between start and end
dateCTE AS
(
SELECT [Id],
[Start],
[End],
DATENAME(weekday, [Start])[Day],
DATENAME(hour, [Start])[Hour]
FROM #Table1 t
UNION ALL
SELECT cte.[Id],
DATEADD(HOUR, 1, cte.[Start]),
cte.[End],
DATENAME(weekday, DATEADD(HOUR, 1, cte.[Start]))[Day],
DATENAME(hour, DATEADD(HOUR, 1, cte.[Start]))[Hour]
FROM #Table1 t
JOIN dateCTE cte ON t.Id = cte.Id
WHERE DATEADD(HOUR, 1, cte.[Start]) <= t.[End]
)
SELECT t.[Id],
t.[Start],
t.[End],
SUM(COALESCE(e.[Amount], 1)) [Hours]
FROM #Table1 t
INNER JOIN dateCTE d ON t.[Id] = d.[Id]
LEFT JOIN excludeCTE e ON d.[Day] = e.[Day] AND d.[Hour] = e.[Hour]
GROUP BY t.[Id],
t.[Start],
t.[End]
OPTION (MAXRECURSION 0) -- allow more than 100 hours
Putting the additional constraint that there can only be one excluded range between any two date
CREATE TABLE worktable (
_Id INT
, _Start DATETIME
, _End DATETIME
);
INSERT INTO worktable VALUES
(1, '2015-11-09 00:00:00', '2015-11-09 00:45:00') -- Start and End before excluded range
, (2, '2015-11-09 00:00:00', '2015-11-11 21:45:00') -- Start before, End after
, (3, '2015-11-09 00:00:00', '2015-11-10 21:00:00') -- Start before, End between
, (4, '2015-11-10 10:00:00', '2015-11-11 10:00:00') -- Start between, End between
, (5, '2015-11-10 10:00:00', '2015-11-11 21:45:00') -- Start between, End after
With getDates As (
SELECT _Id
, a = _Start
, b = _End
, c = DATEADD(hh, 9
, DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
+ 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 1))
, d = DATEADD(hh, 18
, DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
+ 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 2))
FROM worktable
), getDiff As (
SELECT c_a = DATEDIFF(mi, a, c)
, c_b = DATEDIFF(mi, b, c)
, b_d = DATEDIFF(mi, d, b)
, a, b, c, d, _id
FROM getDates
)
Select _id
, (c_a + ABS(c_a)) / 2
- (c_b + ABS(c_b)) / 2
+ (b_d + ABS(b_d)) / 2
FROM getDiff;
c is the date of the first Tuesday after the start date (Find the next occurance of a day of the week in SQL) you may need to adjust the last value depending from DATEFIRST
d is the date of the first Wednesday after the start date in the same week of c
Cast(Sign(a - b) + 1 as bit) is 1 if a is more than or equal b, 0 otherwise
(x + ABS(x)) / 2 is x if not negative, otherwise 0
Given that the formula to get the elapsed time with the excluded range is:
+ (Exclusion Start - Start) If (Start < Exclusion Start)
- (Exclusion Start - End) If (End < Exclusion Start)
+ (End - Exclusion End) If (Exclusion End < End)
-- excluded range (weekday numbers run from 1 to 7)
declare #x datetime = /*ignore*/ '1900012' + /*start day # and time*/ '3 09:00am';
declare #y datetime = /*ignore*/ '1900012' + /* end day # and time*/ '4 06:00pm';
-- normalize date to 1900-01-21, which was a Sunday
declare #s datetime =
dateadd(day, 19 + datepart(weekday, #start), cast(cast(#start as time) as datetime));
declare #e datetime =
dateadd(day, 19 + datepart(weekday, #end), cast(cast(#end as time) as datetime));
-- split range into two parts, one before #x and the other after #y
-- each part collapses to zero when #s and #e respectively fall between #x and #y
select (
datediff(second, -- diff in minutes would truncate so count seconds
case when #s < #x then #s else #x end, -- minimum of #s, #x
case when #e < #x then #e else #x end -- minimum of #e, #x
) +
datediff(second,
case when #s > #y then #s else #y end, -- maximum of #s, #y
case when #e > #y then #e else #y end -- maximum of #e, #y
)
) / 60; -- convert seconds to minutes, truncating with integer division
I glanced at the earlier answers and I thought that surely there was something more straightforward and elegant. Perhaps this is easier to understand and one clear advantage over some solutions is that it's trivial to change the excluded range and that range doesn't have to be limited to a single day.
I'm assuming that your dates never span more than one regular calendar week. It wouldn't be too difficult to extend it to handle more though. One approach would be to handle starting and ending partial weeks plus the full weeks in the middle.
Imagine that your start time is 8:59:30am and your end time is 6:00:30pm. In such a case I'm figuring that you'd want to accumulate the half minutes on each side to get a full minute in total after subtracting the 9-6 block. If you use datediff(minute, ...) you would be truncating the partial minutes and never get the chance to add them together: so that's why I count seconds and then divide by sixty at the end. Of course, if you're only dealing in whole minutes then you won't need to do it that way.
I've chosen my reference date somewhat arbitrarily. At first I thought it might possibly be handy to look at a real and convenient date on the calendar but ultimately it only really matters that it falls on a Sunday. So I settled on the first Sunday falling on a date ending in the digit 1.
Note that the solution also relies on datefirst being set to Sunday. That could be tweaked or made more portable if necessary.

Finding Closest future date

Is it possible to find the closest future date (datetime) by a date varchar value?
Given,
DECLARE #DayValue VARCHAR(3)
, #DateValue DATETIME
SET #DayValue = 'Tue' -- Values could be 'Mon', 'Tue', 'Wed' and etc.
SET #DateValue = '10/15/2014' -- Format is MM/dd/yyyy
I want to get:
Oct 21 2014 12:00AM
Using Loop,
DECLARE #DayValue VARCHAR(3)
,#DateValue DATETIME
SET #DayValue = 'tue'
SET #DateValue = '10/15/2014'
declare #i int= 1 ,#day varchar(3) = null
while (#i<=7 )
begin
Select #day = left(datename (dw,#DateValue),3)
if #day = #DayValue
begin
Select #DateValue
break
end
Select #DateValue = #DateValue+ 1
Select #i = #i+1
end
You could use this function if you had a date-table:
CREATE FUNCTION [dbo].[GetNextDayOfWeek]
( #DayOfWeek VARCHAR(3),
#DateValue datetime
)
RETURNS SmallDateTime
AS
BEGIN
DECLARE #NextDayOfWeek smalldatetime
SET #NextDayOfWeek = (
SELECT
MIN(d.Date)
FROM
tDefDate d
WHERE
d.Date > #DateValue
AND LEFT(DATENAME(Weekday, d.Date), 3) = #DayOfWeek);
RETURN #NextDayOfWeek
END
Then it's simple as:
select [dbo].[GetNextDayOfWeek]('Tue', Getdate()) -- next tuesday=> 2014-10-21
Note that it takes the language of the database into account. So if it's in german:
select [dbo].[GetNextDayOfWeek]('Die', Getdate()) -- next tuesday(Dienstag)
Here's a version that works also without a date-table (but is less efficient).
CREATE FUNCTION [dbo].[GetNextDayOfWeek]
( #DayOfWeek VARCHAR(3),
#DateValue datetime
)
RETURNS SmallDateTime
AS
BEGIN
DECLARE #NextDayOfWeek smalldatetime
;WITH CTE as
(
SELECT GetDate() DateValue, DayNum=0
UNION ALL
SELECT DateValue + 1, DayNum=DayNum+1
FROM CTE
WHERE DayNum <=7
)
SELECT #NextDayOfWeek = (
SELECT
MIN(d.DateValue)
FROM
CTE d
WHERE d.DateValue > #DateValue
AND LEFT(DATENAME(Weekday, d.DateValue), 3) = #DayOfWeek
)OPTION (MAXRECURSION 8);
RETURN #NextDayOfWeek
END
If you could define DayValue as an integer, you solve this problem with more elegant way:
DECLARE #DayValue int, #DateValue DATETIME 
SET #DayValue = 3 -- Values could be 1-Sun, 2-Mon, 3-Tue, 4-Wed and etc. 
SET #DateValue = '10/15/2014' -- Format is MM/dd/yyyy
select dateadd(day,(7 + #DayValue - datepart(w,#DateValue)), #DateValue)
TRY SQL FIDDLE DEMO
No loops and will work in selects with multiple rows. :)
DECLARE #DayValue CHAR(3)
DECLARE #DateValue DATETIME
DECLARE #FutureDate DATE
SET #DayValue='MON'
SET #DateValue='10/12/2014'
DECLARE #Days TABLE
(
[DayOfWeek] TINYINT,
[DayValue] CHAR(3)
)
INSERT INTO #Days([DayOfWeek],[DayValue])
SELECT 0,'SUN' UNION
SELECT 1,'MON' UNION
SELECT 2,'TUE' UNION
SELECT 3,'WED' UNION
SELECT 4,'THU' UNION
SELECT 5,'FRI' UNION
SELECT 6,'SAT'
SET #FutureDate=
DATEADD(DAY,
--Skip to next week if we are already on the desired day or past it
+ CASE WHEN ((SELECT [DayOfWeek] FROM #Days WHERE [DayValue]=#DayValue)<DATEPART(WEEKDAY,#DateValue)) THEN 7 ELSE 0 END
--reset to start of week (add one as DATEPART is base 1, not base 0)
- DATEPART(WEEKDAY,#DateValue) + 1
--Add the desired day of the week
+ (SELECT [DayOfWeek] FROM #Days WHERE [DayValue]=#DayValue)
,#DateValue)
SELECT #FutureDate
This is a bit chunky solution, but it works. :)
SET DATEFIRST 1
DECLARE #DateValue DateTime
, #DayValue VARCHAR(3)
, #tmp INT
SET #DateValue = '09/30/2014'
SET #DayValue = 'wed'
SET #tmp = CASE #DayValue
WHEN 'Mon' THEN (1 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Tue' THEN (2 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Wed' THEN (3 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Thu' THEN (4 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Fri' THEN (5 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Sat' THEN (6 - DATEPART(dw, #DateValue) + 7) % 7
WHEN 'Sun' THEN (7 - DATEPART(dw, #DateValue) + 7) % 7
END
SELECT
CASE
WHEN #tmp = 0 THEN DATEADD (DAY, 7, #DateValue)
ELSE DATEADD (DAY, #tmp, #DateValue)
END

How can I rewrite this as a select statement using group by instead of using a loop

I am revisiting some old code I wrote for a report when I was still very new to SQL (MSSQL). It does what it is supposed to but its not the prettiest or most efficient.
The dummy code below mimics what I currently have in place. Here I am trying to get counts for the number of contracts that are open over the last 5 weeks. For this example a contract is considered open if the start date of the contract happens before of during the given week and the end date happens during or after the given week.
dbo.GetWeekStart(#Date DATETIME, #NumOfWeeks INT, #FirstDayOfWeek CHAR(3)) is a function that will return the first day of each week based on the date provided for a specified number of weeks. ie SELECT * FROM dbo.GetWeekStart('20120719', -2, 'MON') will return the 2 mondays prior to July 19, 2012.
How can I simplify this? I think there is someone to do this without a loop but I have not been able to figure it out.
DECLARE #RunDate DATETIME,
#Index INT,
#RowCount INT,
#WeekStart DATETIME,
#WeekEnd DATETIME
DECLARE #Weeks TABLE
(
WeekNum INT IDENTITY(0,1),
WeekStart DATETIME,
WeekEnd DATETIME
)
DECLARE #Output TABLE
(
WeekStart DATETIME,
OpenContractCount INT
)
SET #RunDate = GETDATE()
INSERT INTO #Weeks (WeekStart, WeekEnd)
SELECT WeekStart,
DATEADD(ss,-1,DATEADD(ww,1,WeekStart))
FROM dbo.[GetWeekStart](#RunDate, -5, 'MON')
SET #RowCount = (SELECT COUNT(*) FROM #Weeks)
SET #Index = 0
WHILE #Index < #RowCount
BEGIN
SET #WeekStart = (SELECT WeekStart FROM #Weeks WHERE WeekNum = #Idx)
SET #WeekEnd = (SELECT WeekEnd FROM #Weeks WHERE WeekNum = #Idx)
INSERT INTO #Output (WeekStart, OpenContractCount)
SELECT #WeekStart,
COUNT(*)
FROM Contracts c
WHERE c.StartDate <= #WeekEnd
AND ISNULL(c.EndDate, GETDATE()) >= #WeekStart
SET #Index = #Index + 1
END
SELECT * FROM #Output
I see no reason why this wouldn't work:
DECLARE #RunDate DATETIME = GETDATE()
SELECT WeekStart, COUNT(*)
FROM Contracts c
INNER JOIN dbo.[GetWeekStart](#RunDate, -5, 'MON')
ON c.StartDate < DATEADD(WEEK, 1, WeekStart)
AND (c.EndDate IS NULL OR c.EndDate >= #WeekStart)
GROUP BY WeekStart
I am not sure how you are generating your dates within your function, just in case you are using a loop/recursive CTE I'll include a query that doesn't use loops/cursors etc.
DECLARE #RunDate DATETIME = GETDATE()
-- SET DATEFIRST AS 1 TO ENSURE MONDAY IS THE FIRST DAY OF THE WEEK
-- CHANGE THIS TO SIMULATE CHANGING YOUR WEEKDAY INPUT TO db
SET DATEFIRST 1
-- SET RUN DATE TO BE THE START OF THE WEEK
SET #RunDate = CAST(DATEADD(DAY, 1 - DATEPART(WEEKDAY, #RunDate), #RunDate) AS DATE)
;WITH Weeks AS
( SELECT TOP 5 -- CHANGE THIS TO CHANGE THE WEEKS TO RUN
DATEADD(WEEK, 1 - ROW_NUMBER() OVER(ORDER BY Object_ID), #RunDate) [WeekStart]
FROM sys.All_Objects
)
SELECT WeekStart, COUNT(*)
FROM Contracts c
INNER JOIN Weeks
ON c.StartDate < DATEADD(WEEK, 1, WeekStart)
AND (c.EndDate IS NULL OR c.EndDate >= #WeekStart)
GROUP BY WeekStart
Did this quick but it should work
/*CTE generates Start & End Dates for 5 weeks
Start Date = Sunday of week # midnight
End Date = Sunday of next week # midnight
*/
WITH weeks
AS ( SELECT DATEADD(ww, -4,
CAST(FLOOR(CAST(GETDATE() - ( DATEPART(dw,
GETDATE()) - 1 ) AS FLOAT)) AS DATETIME)) AS StartDate
UNION ALL
SELECT DATEADD(wk, 1, StartDate)
FROM weeks
WHERE DATEADD(wk, 1, StartDate) <= GETDATE()
)
SELECT w.StartDate ,
COUNT(*) AS OpenContracts
FROM dbo.Contracts c
LEFT JOIN weeks w ON c.StartDate < DATEADD(d, 7, w.StartDate)
AND ISNULL(c.EndDate, GETDATE()) >= w.StartDate
GROUP BY w.StartDate

t-sql select get all Months within a range of years

I need a select to return Month and year Within a specified date range where I would input the start year and month and the select would return month and year from the date I input till today.
I know I can do this in a loop but I was wondering if it is possible to do this in a series selects?
Year Month
---- -----
2010 1
2010 2
2010 3
2010 4
2010 5
2010 6
2010 7
and so on.
Gosh folks... using a "counting recursive CTE" or "rCTE" is as bad or worse than using a loop. Please see the following article for why I say that.
http://www.sqlservercentral.com/articles/T-SQL/74118/
Here's one way to do it without any RBAR including the "hidden RBAR" of a counting rCTE.
--===== Declare and preset some obviously named variables
DECLARE #StartDate DATETIME,
#EndDate DATETIME
;
SELECT #StartDate = '2010-01-14', --We'll get the month for both of these
#EndDate = '2020-12-05' --dates and everything in between
;
WITH
cteDates AS
(--==== Creates a "Tally Table" structure for months to add to start date
-- calulated by the difference in months between the start and end date.
-- Then adds those numbers to the start of the month of the start date.
SELECT TOP (DATEDIFF(mm,#StartDate,#EndDate) + 1)
MonthDate = DATEADD(mm,DATEDIFF(mm,0,#StartDate)
+ (ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1),0)
FROM sys.all_columns ac1
CROSS JOIN sys.all_columns ac2
)
--===== Slice each "whole month" date into the desired display values.
SELECT [Year] = YEAR(MonthDate),
[Month] = MONTH(MonthDate)
FROM cteDates
;
I know this is an old question, but I'm mildly horrified at the complexity of some of the answers. Using a CTE is definitely the simplest way to go for selecting these values:
WITH months(dt) AS
(SELECT getdate() dt
UNION ALL
SELECT dateadd(month, -1, dt)
FROM months)
SELECT
top (datediff(month, '2017-07-01' /* start date */, getdate()) + 1)
YEAR(months.dt) yr, MONTH(months.dt) mnth
FROM months
OPTION (maxrecursion 0);
Just slap in whichever start date you'd like in place of the '2017-07-01' above and you're good to go with an efficient and easily-integrated solution.
Edit: Jeff Moden's answer quite effectively advocates against using rCTEs. However, in this case it appears to be a case of premature optimization - we're talking about 10's of records in all likelihood, and even if you span back to 1900 from today, it's still a minuscule hit. Using rCTEs to achieve code maintainability seems to be worth the trade if the expected result set is small.
You can use something like this: Link
To generate the equivalent of a numbers table using date ranges.
But could you please clarify your inputs and outputs?
Do you want to input a start date, for example, '2010-5-1' and end date, for example, '2010-8-1' and have it return every month between the two? Do you want to include the start month and end month, or exclude them?
Here's some code that I wrote that will quickly generate an inclusive result of every month between two dates.
--Inputs here:
DECLARE #StartDate datetime;
DECLARE #EndDate datetime;
SET #StartDate = '2010-1-5 5:00PM';
SET #EndDate = GETDATE();
--Procedure here:
WITH RecursiveRowGenerator (Row#, Iteration) AS (
SELECT 1, 1
UNION ALL
SELECT Row# + Iteration, Iteration * 2
FROM RecursiveRowGenerator
WHERE Iteration * 2 < CEILING(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))
UNION ALL
SELECT Row# + (Iteration * 2), Iteration * 2
FROM RecursiveRowGenerator
WHERE Iteration * 2 < CEILING(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))
)
, SqrtNRows AS (
SELECT *
FROM RecursiveRowGenerator
UNION ALL
SELECT 0, 0
)
SELECT TOP(DATEDIFF(MONTH, #StartDate, #EndDate)+1)
DATEADD(month, DATEDIFF(month, 0, #StartDate) + A.Row# * POWER(2,CEILING(LOG(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))/LOG(2))) + B.Row#, 0) Row#
FROM SqrtNRows A, SqrtNRows B
ORDER BY A.Row#, B.Row#;
Code below generates the values for the range between 21 Jul 2013 and 15 Jan 2014.
I usually use it in SSRS reports for generating lookup values for the Month parameter.
declare
#from date = '20130721',
#to date = '20140115';
with m as (
select * from (values ('Jan', '01'), ('Feb', '02'),('Mar', '03'),('Apr', '04'),('May', '05'),('Jun', '06'),('Jul', '07'),('Aug', '08'),('Sep', '09'),('Oct', '10'),('Nov', '11'),('Dec', '12')) as t(v, c)),
y as (select cast(YEAR(getdate()) as nvarchar(4)) [v] union all select cast(YEAR(getdate())-1 as nvarchar(4)))
select m.v + ' ' + y.v [value_field], y.v + m.c [label_field]
from m
cross join y
where y.v + m.c between left(convert(nvarchar, #from, 112),6) and left(convert(nvarchar, #to, 112),6)
order by y.v + m.c desc
Results:
value_field label_field
---------------------------
Jan 2014 201401
Dec 2013 201312
Nov 2013 201311
Oct 2013 201310
Sep 2013 201309
Aug 2013 201308
Jul 2013 201307
you can do the following
SELECT DISTINCT YEAR(myDate) as [Year], MONTH(myDate) as [Month]
FROM myTable
WHERE <<appropriate criteria>>
ORDER BY [Year], [Month]
---Here is a version that gets the month end dates typically used for accounting purposes
DECLARE #StartDate datetime;
DECLARE #EndDate datetime;
SET #StartDate = '2010-1-1';
SET #EndDate = '2020-12-31';
--Procedure here:
WITH RecursiveRowGenerator (Row#, Iteration)
AS ( SELECT 1, 1
UNION ALL
SELECT Row# + Iteration, Iteration * 2
FROM RecursiveRowGenerator
WHERE Iteration * 2 < CEILING(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))
UNION ALL SELECT Row# + (Iteration * 2), Iteration * 2
FROM RecursiveRowGenerator
WHERE Iteration * 2 < CEILING(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1)) )
, SqrtNRows AS ( SELECT * FROM RecursiveRowGenerator
UNION ALL SELECT 0, 0 )
SELECT TOP(DATEDIFF(MONTH, #StartDate, #EndDate)+1)
DateAdd(d,-1,DateAdd(m,1, DATEADD(month, DATEDIFF(month, 0, #StartDate) + A.Row# * POWER(2,CEILING(LOG(SQRT(DATEDIFF(MONTH, #StartDate, #EndDate)+1))/LOG(2))) + B.Row#, 0) ))
Row# FROM SqrtNRows A, SqrtNRows B ORDER BY A.Row#, B.Row#;
DECLARE #Date1 DATE
DECLARE #Date2 DATE
SET #Date1 = '20130401'
SET #Date2 = DATEADD(MONTH, 83, #Date1)
SELECT DATENAME(MONTH, #Date1) "Month", MONTH(#Date1) "Month Number", YEAR(#Date1) "Year"
INTO #Month
WHILE (#Date1 < #Date2)
BEGIN
SET #Date1 = DATEADD(MONTH, 1, #Date1)
INSERT INTO #Month
SELECT DATENAME(MONTH, #Date1) "Month", MONTH(#Date1) "Month Number", YEAR(#Date1) "Year"
END
SELECT * FROM #Month
ORDER BY [Year], [Month Number]
DROP TABLE #Month
declare #date1 datetime,
#date2 datetime,
#date datetime,
#month integer,
#nm_bulan varchar(20)
create table #month_tmp
( bulan integer null, keterangan varchar(20) null )
select #date1 = '2000-01-01',
#date2 = '2000-12-31'
select #month = month(#date1)
while (#month < 13)
Begin
IF #month = 1
Begin
SELECT #date = CAST( CONVERT(VARCHAR(25),DATEADD(dd,-(DAY(DATEADD(mm,0,#date1))-1),DATEADD(mm,0,#date1)),111) + ' 00:00:00' as DATETIME )
End
ELSE
Begin
SELECT #date = CAST( CONVERT(VARCHAR(25),DATEADD(dd,-(DAY(DATEADD(mm,#month -1,#date1))-1),DATEADD(mm,#month -1,#date1)),111) + ' 00:00:00' as DATETIME )
End
select #nm_bulan = DATENAME(MM, #date)
insert into #month_tmp
select #month as nilai, #nm_bulan as nama
select #month = #month + 1
End
select * from #month_tmp
drop table #month_tmp
go

Count work days between two dates

How can I calculate the number of work days between two dates in SQL Server?
Monday to Friday and it must be T-SQL.
For workdays, Monday to Friday, you can do it with a single SELECT, like this:
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2008/10/01'
SET #EndDate = '2008/10/31'
SELECT
(DATEDIFF(dd, #StartDate, #EndDate) + 1)
-(DATEDIFF(wk, #StartDate, #EndDate) * 2)
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
If you want to include holidays, you have to work it out a bit...
In Calculating Work Days you can find a good article about this subject, but as you can see it is not that advanced.
--Changing current database to the Master database allows function to be shared by everyone.
USE MASTER
GO
--If the function already exists, drop it.
IF EXISTS
(
SELECT *
FROM dbo.SYSOBJECTS
WHERE ID = OBJECT_ID(N'[dbo].[fn_WorkDays]')
AND XType IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION [dbo].[fn_WorkDays]
GO
CREATE FUNCTION dbo.fn_WorkDays
--Presets
--Define the input parameters (OK if reversed by mistake).
(
#StartDate DATETIME,
#EndDate DATETIME = NULL --#EndDate replaced by #StartDate when DEFAULTed
)
--Define the output data type.
RETURNS INT
AS
--Calculate the RETURN of the function.
BEGIN
--Declare local variables
--Temporarily holds #EndDate during date reversal.
DECLARE #Swap DATETIME
--If the Start Date is null, return a NULL and exit.
IF #StartDate IS NULL
RETURN NULL
--If the End Date is null, populate with Start Date value so will have two dates (required by DATEDIFF below).
IF #EndDate IS NULL
SELECT #EndDate = #StartDate
--Strip the time element from both dates (just to be safe) by converting to whole days and back to a date.
--Usually faster than CONVERT.
--0 is a date (01/01/1900 00:00:00.000)
SELECT #StartDate = DATEADD(dd,DATEDIFF(dd,0,#StartDate), 0),
#EndDate = DATEADD(dd,DATEDIFF(dd,0,#EndDate) , 0)
--If the inputs are in the wrong order, reverse them.
IF #StartDate > #EndDate
SELECT #Swap = #EndDate,
#EndDate = #StartDate,
#StartDate = #Swap
--Calculate and return the number of workdays using the input parameters.
--This is the meat of the function.
--This is really just one formula with a couple of parts that are listed on separate lines for documentation purposes.
RETURN (
SELECT
--Start with total number of days including weekends
(DATEDIFF(dd,#StartDate, #EndDate)+1)
--Subtact 2 days for each full weekend
-(DATEDIFF(wk,#StartDate, #EndDate)*2)
--If StartDate is a Sunday, Subtract 1
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday'
THEN 1
ELSE 0
END)
--If EndDate is a Saturday, Subtract 1
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday'
THEN 1
ELSE 0
END)
)
END
GO
If you need to use a custom calendar, you might need to add some checks and some parameters. Hopefully it will provide a good starting point.
All Credit to Bogdan Maxim & Peter Mortensen. This is their post, I just added holidays to the function (This assumes you have a table "tblHolidays" with a datetime field "HolDate".
--Changing current database to the Master database allows function to be shared by everyone.
USE MASTER
GO
--If the function already exists, drop it.
IF EXISTS
(
SELECT *
FROM dbo.SYSOBJECTS
WHERE ID = OBJECT_ID(N'[dbo].[fn_WorkDays]')
AND XType IN (N'FN', N'IF', N'TF')
)
DROP FUNCTION [dbo].[fn_WorkDays]
GO
CREATE FUNCTION dbo.fn_WorkDays
--Presets
--Define the input parameters (OK if reversed by mistake).
(
#StartDate DATETIME,
#EndDate DATETIME = NULL --#EndDate replaced by #StartDate when DEFAULTed
)
--Define the output data type.
RETURNS INT
AS
--Calculate the RETURN of the function.
BEGIN
--Declare local variables
--Temporarily holds #EndDate during date reversal.
DECLARE #Swap DATETIME
--If the Start Date is null, return a NULL and exit.
IF #StartDate IS NULL
RETURN NULL
--If the End Date is null, populate with Start Date value so will have two dates (required by DATEDIFF below).
IF #EndDate IS NULL
SELECT #EndDate = #StartDate
--Strip the time element from both dates (just to be safe) by converting to whole days and back to a date.
--Usually faster than CONVERT.
--0 is a date (01/01/1900 00:00:00.000)
SELECT #StartDate = DATEADD(dd,DATEDIFF(dd,0,#StartDate), 0),
#EndDate = DATEADD(dd,DATEDIFF(dd,0,#EndDate) , 0)
--If the inputs are in the wrong order, reverse them.
IF #StartDate > #EndDate
SELECT #Swap = #EndDate,
#EndDate = #StartDate,
#StartDate = #Swap
--Calculate and return the number of workdays using the input parameters.
--This is the meat of the function.
--This is really just one formula with a couple of parts that are listed on separate lines for documentation purposes.
RETURN (
SELECT
--Start with total number of days including weekends
(DATEDIFF(dd,#StartDate, #EndDate)+1)
--Subtact 2 days for each full weekend
-(DATEDIFF(wk,#StartDate, #EndDate)*2)
--If StartDate is a Sunday, Subtract 1
-(CASE WHEN DATENAME(dw, #StartDate) = 'Sunday'
THEN 1
ELSE 0
END)
--If EndDate is a Saturday, Subtract 1
-(CASE WHEN DATENAME(dw, #EndDate) = 'Saturday'
THEN 1
ELSE 0
END)
--Subtract all holidays
-(Select Count(*) from [DB04\DB04].[Gateway].[dbo].[tblHolidays]
where [HolDate] between #StartDate and #EndDate )
)
END
GO
-- Test Script
/*
declare #EndDate datetime= dateadd(m,2,getdate())
print #EndDate
select [Master].[dbo].[fn_WorkDays] (getdate(), #EndDate)
*/
My version of the accepted answer as a function using DATEPART, so I don't have to do a string comparison on the line with
DATENAME(dw, #StartDate) = 'Sunday'
Anyway, here's my business datediff function
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION BDATEDIFF
(
#startdate as DATETIME,
#enddate as DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #res int
SET #res = (DATEDIFF(dd, #startdate, #enddate) + 1)
-(DATEDIFF(wk, #startdate, #enddate) * 2)
-(CASE WHEN DATEPART(dw, #startdate) = 1 THEN 1 ELSE 0 END)
-(CASE WHEN DATEPART(dw, #enddate) = 7 THEN 1 ELSE 0 END)
RETURN #res
END
GO
Another approach to calculating working days is to use a WHILE loop which basically iterates through a date range and increment it by 1 whenever days are found to be within Monday – Friday. The complete script for calculating working days using the WHILE loop is shown below:
CREATE FUNCTION [dbo].[fn_GetTotalWorkingDaysUsingLoop]
(#DateFrom DATE,
#DateTo   DATE
)
RETURNS INT
AS
     BEGIN
         DECLARE #TotWorkingDays INT= 0;
         WHILE #DateFrom <= #DateTo
             BEGIN
                 IF DATENAME(WEEKDAY, #DateFrom) IN('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday')
                     BEGIN
                         SET #TotWorkingDays = #TotWorkingDays + 1;
                 END;
                 SET #DateFrom = DATEADD(DAY, 1, #DateFrom);
             END;
         RETURN #TotWorkingDays;
     END;
GO
Although the WHILE loop option is cleaner and uses less lines of code, it has the potential of being a performance bottleneck in your environment particularly when your date range spans across several years.
You can see more methods on how to calculate work days and hours in this article:
https://www.sqlshack.com/how-to-calculate-work-days-and-hours-in-sql-server/
DECLARE #TotalDays INT,#WorkDays INT
DECLARE #ReducedDayswithEndDate INT
DECLARE #WeekPart INT
DECLARE #DatePart INT
SET #TotalDays= DATEDIFF(day, #StartDate, #EndDate) +1
SELECT #ReducedDayswithEndDate = CASE DATENAME(weekday, #EndDate)
WHEN 'Saturday' THEN 1
WHEN 'Sunday' THEN 2
ELSE 0 END
SET #TotalDays=#TotalDays-#ReducedDayswithEndDate
SET #WeekPart=#TotalDays/7;
SET #DatePart=#TotalDays%7;
SET #WorkDays=(#WeekPart*5)+#DatePart
RETURN #WorkDays
(I'm a few points shy of commenting privileges)
If you decide to forgo the +1 day in CMS's elegant solution, note that if your start date and end date are in the same weekend, you get a negative answer. Ie., 2008/10/26 to 2008/10/26 returns -1.
my rather simplistic solution:
select #Result = (..CMS's answer..)
if (#Result < 0)
select #Result = 0
RETURN #Result
.. which also sets all erroneous posts with start date after end date to zero. Something you may or may not be looking for.
For difference between dates including holidays I went this way:
1) Table with Holidays:
CREATE TABLE [dbo].[Holiday](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NULL,
[Date] [datetime] NOT NULL)
2) I had my plannings Table like this and wanted to fill column Work_Days which was empty:
CREATE TABLE [dbo].[Plan_Phase](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Id_Plan] [int] NOT NULL,
[Id_Phase] [int] NOT NULL,
[Start_Date] [datetime] NULL,
[End_Date] [datetime] NULL,
[Work_Days] [int] NULL)
3) So in order to get "Work_Days" to later fill in my column just had to:
SELECT Start_Date, End_Date,
(DATEDIFF(dd, Start_Date, End_Date) + 1)
-(DATEDIFF(wk, Start_Date, End_Date) * 2)
-(SELECT COUNT(*) From Holiday Where Date >= Start_Date AND Date <= End_Date)
-(CASE WHEN DATENAME(dw, Start_Date) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, End_Date) = 'Saturday' THEN 1 ELSE 0 END)
-(CASE WHEN (SELECT COUNT(*) From Holiday Where Start_Date = Date) > 0 THEN 1 ELSE 0 END)
-(CASE WHEN (SELECT COUNT(*) From Holiday Where End_Date = Date) > 0 THEN 1 ELSE 0 END) AS Work_Days
from Plan_Phase
Hope that I could help.
Cheers
Here is a version that works well (I think). Holiday table contains Holiday_date columns that contains holidays your company observe.
DECLARE #RAWDAYS INT
SELECT #RAWDAYS = DATEDIFF(day, #StartDate, #EndDate )--+1
-( 2 * DATEDIFF( week, #StartDate, #EndDate ) )
+ CASE WHEN DATENAME(dw, #StartDate) = 'Saturday' THEN 1 ELSE 0 END
- CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END
SELECT #RAWDAYS - COUNT(*)
FROM HOLIDAY NumberOfBusinessDays
WHERE [Holiday_Date] BETWEEN #StartDate+1 AND #EndDate
I know this is an old question but I needed a formula for workdays excluding the start date since I have several items and need the days to accumulate correctly.
None of the non-iterative answers worked for me.
I used a defintion like
Number of times midnight to monday, tuesday, wednesday, thursday and friday is passed
(others might count midnight to saturday instead of monday)
I ended up with this formula
SELECT DATEDIFF(day, #StartDate, #EndDate) /* all midnights passed */
- DATEDIFF(week, #StartDate, #EndDate) /* remove sunday midnights */
- DATEDIFF(week, DATEADD(day, 1, #StartDate), DATEADD(day, 1, #EndDate)) /* remove saturday midnights */
This is basically CMS's answer without the reliance on a particular language setting. And since we're shooting for generic, that means it should work for all ##datefirst settings as well.
datediff(day, <start>, <end>) + 1 - datediff(week, <start>, <end>) * 2
/* if start is a Sunday, adjust by -1 */
+ case when datepart(weekday, <start>) = 8 - ##datefirst then -1 else 0 end
/* if end is a Saturday, adjust by -1 */
+ case when datepart(weekday, <end>) = (13 - ##datefirst) % 7 + 1 then -1 else 0 end
datediff(week, ...) always uses a Saturday-to-Sunday boundary for weeks, so that expression is deterministic and doesn't need to be modified (as long as our definition of weekdays is consistently Monday through Friday.) Day numbering does vary according to the ##datefirst setting and the modified calculations handle this correction with the small complication of some modular arithmetic.
A cleaner way to deal with the Saturday/Sunday thing is to translate the dates prior to extracting a day of week value. After shifting, the values will be back in line with a fixed (and probably more familiar) numbering that starts with 1 on Sunday and ends with 7 on Saturday.
datediff(day, <start>, <end>) + 1 - datediff(week, <start>, <end>) * 2
+ case when datepart(weekday, dateadd(day, ##datefirst, <start>)) = 1 then -1 else 0 end
+ case when datepart(weekday, dateadd(day, ##datefirst, <end>)) = 7 then -1 else 0 end
I've tracked this form of the solution back at least as far as 2002 and an Itzik Ben-Gan article. (https://technet.microsoft.com/en-us/library/aa175781(v=sql.80).aspx) Though it needed a small tweak since newer date types don't allow date arithmetic, it is otherwise identical.
EDIT:
I added back the +1 that had somehow been left off. It's also worth noting that this method always counts the start and end days. It also assumes that the end date is on or after the start date.
None of the functions above work for the same week or deal with holidays. I wrote this:
create FUNCTION [dbo].[ShiftHolidayToWorkday](#date date)
RETURNS date
AS
BEGIN
IF DATENAME( dw, #Date ) = 'Saturday'
SET #Date = DATEADD(day, - 1, #Date)
ELSE IF DATENAME( dw, #Date ) = 'Sunday'
SET #Date = DATEADD(day, 1, #Date)
RETURN #date
END
GO
create FUNCTION [dbo].[GetHoliday](#date date)
RETURNS varchar(50)
AS
BEGIN
declare #s varchar(50)
SELECT #s = CASE
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year] ) + '-01-01') = #date THEN 'New Year'
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year]+1) + '-01-01') = #date THEN 'New Year'
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year] ) + '-07-04') = #date THEN 'Independence Day'
WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year] ) + '-12-25') = #date THEN 'Christmas Day'
--WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year]) + '-12-31') = #date THEN 'New Years Eve'
--WHEN dbo.ShiftHolidayToWorkday(CONVERT(varchar, [Year]) + '-11-11') = #date THEN 'Veteran''s Day'
WHEN [Month] = 1 AND [DayOfMonth] BETWEEN 15 AND 21 AND [DayName] = 'Monday' THEN 'Martin Luther King Day'
WHEN [Month] = 5 AND [DayOfMonth] >= 25 AND [DayName] = 'Monday' THEN 'Memorial Day'
WHEN [Month] = 9 AND [DayOfMonth] <= 7 AND [DayName] = 'Monday' THEN 'Labor Day'
WHEN [Month] = 11 AND [DayOfMonth] BETWEEN 22 AND 28 AND [DayName] = 'Thursday' THEN 'Thanksgiving Day'
WHEN [Month] = 11 AND [DayOfMonth] BETWEEN 23 AND 29 AND [DayName] = 'Friday' THEN 'Day After Thanksgiving'
ELSE NULL END
FROM (
SELECT
[Year] = YEAR(#date),
[Month] = MONTH(#date),
[DayOfMonth] = DAY(#date),
[DayName] = DATENAME(weekday,#date)
) c
RETURN #s
END
GO
create FUNCTION [dbo].GetHolidays(#year int)
RETURNS TABLE
AS
RETURN (
select dt, dbo.GetHoliday(dt) as Holiday
from (
select dateadd(day, number, convert(varchar,#year) + '-01-01') dt
from master..spt_values
where type='p'
) d
where year(dt) = #year and dbo.GetHoliday(dt) is not null
)
create proc UpdateHolidaysTable
as
if not exists(select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_NAME = 'Holidays')
create table Holidays(dt date primary key clustered, Holiday varchar(50))
declare #year int
set #year = 1990
while #year < year(GetDate()) + 20
begin
insert into Holidays(dt, Holiday)
select a.dt, a.Holiday
from dbo.GetHolidays(#year) a
left join Holidays b on b.dt = a.dt
where b.dt is null
set #year = #year + 1
end
create FUNCTION [dbo].[GetWorkDays](#StartDate DATE = NULL, #EndDate DATE = NULL)
RETURNS INT
AS
BEGIN
IF #StartDate IS NULL OR #EndDate IS NULL
RETURN 0
IF #StartDate >= #EndDate
RETURN 0
DECLARE #Days int
SET #Days = 0
IF year(#StartDate) * 100 + datepart(week, #StartDate) = year(#EndDate) * 100 + datepart(week, #EndDate)
--same week
select #Days = (DATEDIFF(dd, #StartDate, #EndDate))
- (CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
- (select count(*) from Holidays where dt between #StartDate and #EndDate)
ELSE
--diff weeks
select #Days = (DATEDIFF(dd, #StartDate, #EndDate) + 1)
- (DATEDIFF(wk, #StartDate, #EndDate) * 2)
- (CASE WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN 1 ELSE 0 END)
- (select count(*) from Holidays where dt between #StartDate and #EndDate)
RETURN #Days
END
Using a date table:
DECLARE
#StartDate date = '2014-01-01',
#EndDate date = '2014-01-31';
SELECT
COUNT(*) As NumberOfWeekDays
FROM dbo.Calendar
WHERE CalendarDate BETWEEN #StartDate AND #EndDate
AND IsWorkDay = 1;
If you don't have that, you can use a numbers table:
DECLARE
#StartDate datetime = '2014-01-01',
#EndDate datetime = '2014-01-31';
SELECT
SUM(CASE WHEN DATEPART(dw, DATEADD(dd, Number-1, #StartDate)) BETWEEN 2 AND 6 THEN 1 ELSE 0 END) As NumberOfWeekDays
FROM dbo.Numbers
WHERE Number <= DATEDIFF(dd, #StartDate, #EndDate) + 1 -- Number table starts at 1, we want a 0 base
They should both be fast and it takes out the ambiguity/complexity. The first option is the best but if you don't have a calendar table you can allways create a numbers table with a CTE.
DECLARE #StartDate datetime,#EndDate datetime
select #StartDate='3/2/2010', #EndDate='3/7/2010'
DECLARE #TotalDays INT,#WorkDays INT
DECLARE #ReducedDayswithEndDate INT
DECLARE #WeekPart INT
DECLARE #DatePart INT
SET #TotalDays= DATEDIFF(day, #StartDate, #EndDate) +1
SELECT #ReducedDayswithEndDate = CASE DATENAME(weekday, #EndDate)
WHEN 'Saturday' THEN 1
WHEN 'Sunday' THEN 2
ELSE 0 END
SET #TotalDays=#TotalDays-#ReducedDayswithEndDate
SET #WeekPart=#TotalDays/7;
SET #DatePart=#TotalDays%7;
SET #WorkDays=(#WeekPart*5)+#DatePart
SELECT #WorkDays
CREATE FUNCTION x
(
#StartDate DATETIME,
#EndDate DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #Teller INT
SET #StartDate = DATEADD(dd,1,#StartDate)
SET #Teller = 0
IF DATEDIFF(dd,#StartDate,#EndDate) <= 0
BEGIN
SET #Teller = 0
END
ELSE
BEGIN
WHILE
DATEDIFF(dd,#StartDate,#EndDate) >= 0
BEGIN
IF DATEPART(dw,#StartDate) < 6
BEGIN
SET #Teller = #Teller + 1
END
SET #StartDate = DATEADD(dd,1,#StartDate)
END
END
RETURN #Teller
END
I took the various examples here, but in my particular situation we have a #PromisedDate for delivery and a #ReceivedDate for the actual receipt of the item. When an item was received before the "PromisedDate" the calculations were not totaling correctly unless I ordered the dates passed into the function by calendar order. Not wanting to check the dates every time, I changed the function to handle this for me.
Create FUNCTION [dbo].[fnGetBusinessDays]
(
#PromiseDate date,
#ReceivedDate date
)
RETURNS integer
AS
BEGIN
DECLARE #days integer
SELECT #days =
Case when #PromiseDate > #ReceivedDate Then
DATEDIFF(d,#PromiseDate,#ReceivedDate) +
ABS(DATEDIFF(wk,#PromiseDate,#ReceivedDate)) * 2 +
CASE
WHEN DATENAME(dw, #PromiseDate) <> 'Saturday' AND DATENAME(dw, #ReceivedDate) = 'Saturday' THEN 1
WHEN DATENAME(dw, #PromiseDate) = 'Saturday' AND DATENAME(dw, #ReceivedDate) <> 'Saturday' THEN -1
ELSE 0
END +
(Select COUNT(*) FROM CompanyHolidays
WHERE HolidayDate BETWEEN #ReceivedDate AND #PromiseDate
AND DATENAME(dw, HolidayDate) <> 'Saturday' AND DATENAME(dw, HolidayDate) <> 'Sunday')
Else
DATEDIFF(d,#PromiseDate,#ReceivedDate) -
ABS(DATEDIFF(wk,#PromiseDate,#ReceivedDate)) * 2 -
CASE
WHEN DATENAME(dw, #PromiseDate) <> 'Saturday' AND DATENAME(dw, #ReceivedDate) = 'Saturday' THEN 1
WHEN DATENAME(dw, #PromiseDate) = 'Saturday' AND DATENAME(dw, #ReceivedDate) <> 'Saturday' THEN -1
ELSE 0
END -
(Select COUNT(*) FROM CompanyHolidays
WHERE HolidayDate BETWEEN #PromiseDate and #ReceivedDate
AND DATENAME(dw, HolidayDate) <> 'Saturday' AND DATENAME(dw, HolidayDate) <> 'Sunday')
End
RETURN (#days)
END
If you need to add work days to a given date, you can create a function that depends on a calendar table, described below:
CREATE TABLE Calendar
(
dt SMALLDATETIME PRIMARY KEY,
IsWorkDay BIT
);
--fill the rows with normal days, weekends and holidays.
create function AddWorkingDays (#initialDate smalldatetime, #numberOfDays int)
returns smalldatetime as
begin
declare #result smalldatetime
set #result =
(
select t.dt from
(
select dt, ROW_NUMBER() over (order by dt) as daysAhead from calendar
where dt > #initialDate
and IsWorkDay = 1
) t
where t.daysAhead = #numberOfDays
)
return #result
end
As with DATEDIFF, I do not consider the end date to be part of the interval.
The number of (for example) Sundays between #StartDate and #EndDate is the number of Sundays between an "initial" Monday and the #EndDate minus the number of Sundays between this "initial" Monday and the #StartDate. Knowing this, we can calculate the number of workdays as follows:
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2018/01/01'
SET #EndDate = '2019/01/01'
SELECT DATEDIFF(Day, #StartDate, #EndDate) -- Total Days
- (DATEDIFF(Day, 0, #EndDate)/7 - DATEDIFF(Day, 0, #StartDate)/7) -- Sundays
- (DATEDIFF(Day, -1, #EndDate)/7 - DATEDIFF(Day, -1, #StartDate)/7) -- Saturdays
Best regards!
I borrowed some ideas from others to create my solution. I use inline code to ignore weekends and U.S. federal holidays. In my environment, EndDate may be null, but it will never precede StartDate.
CREATE FUNCTION dbo.ufn_CalculateBusinessDays(
#StartDate DATE,
#EndDate DATE = NULL)
RETURNS INT
AS
BEGIN
DECLARE #TotalBusinessDays INT = 0;
DECLARE #TestDate DATE = #StartDate;
IF #EndDate IS NULL
RETURN NULL;
WHILE #TestDate < #EndDate
BEGIN
DECLARE #Month INT = DATEPART(MM, #TestDate);
DECLARE #Day INT = DATEPART(DD, #TestDate);
DECLARE #DayOfWeek INT = DATEPART(WEEKDAY, #TestDate) - 1; --Monday = 1, Tuesday = 2, etc.
DECLARE #DayOccurrence INT = (#Day - 1) / 7 + 1; --Nth day of month (3rd Monday, for example)
--Increment business day counter if not a weekend or holiday
SELECT #TotalBusinessDays += (
SELECT CASE
--Saturday OR Sunday
WHEN #DayOfWeek IN (6,7) THEN 0
--New Year's Day
WHEN #Month = 1 AND #Day = 1 THEN 0
--MLK Jr. Day
WHEN #Month = 1 AND #DayOfWeek = 1 AND #DayOccurrence = 3 THEN 0
--G. Washington's Birthday
WHEN #Month = 2 AND #DayOfWeek = 1 AND #DayOccurrence = 3 THEN 0
--Memorial Day
WHEN #Month = 5 AND #DayOfWeek = 1 AND #Day BETWEEN 25 AND 31 THEN 0
--Independence Day
WHEN #Month = 7 AND #Day = 4 THEN 0
--Labor Day
WHEN #Month = 9 AND #DayOfWeek = 1 AND #DayOccurrence = 1 THEN 0
--Columbus Day
WHEN #Month = 10 AND #DayOfWeek = 1 AND #DayOccurrence = 2 THEN 0
--Veterans Day
WHEN #Month = 11 AND #Day = 11 THEN 0
--Thanksgiving
WHEN #Month = 11 AND #DayOfWeek = 4 AND #DayOccurrence = 4 THEN 0
--Christmas
WHEN #Month = 12 AND #Day = 25 THEN 0
ELSE 1
END AS Result);
SET #TestDate = DATEADD(dd, 1, #TestDate);
END
RETURN #TotalBusinessDays;
END
That's working for me, in my country on Saturday and Sunday are non-working days.
For me is important the time of #StartDate and #EndDate.
CREATE FUNCTION [dbo].[fnGetCountWorkingBusinessDays]
(
#StartDate as DATETIME,
#EndDate as DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #res int
SET #StartDate = CASE
WHEN DATENAME(dw, #StartDate) = 'Saturday' THEN DATEADD(dd, 2, DATEDIFF(dd, 0, #StartDate))
WHEN DATENAME(dw, #StartDate) = 'Sunday' THEN DATEADD(dd, 1, DATEDIFF(dd, 0, #StartDate))
ELSE #StartDate END
SET #EndDate = CASE
WHEN DATENAME(dw, #EndDate) = 'Saturday' THEN DATEADD(dd, 0, DATEDIFF(dd, 0, #EndDate))
WHEN DATENAME(dw, #EndDate) = 'Sunday' THEN DATEADD(dd, -1, DATEDIFF(dd, 0, #EndDate))
ELSE #EndDate END
SET #res =
(DATEDIFF(hour, #StartDate, #EndDate) / 24)
- (DATEDIFF(wk, #StartDate, #EndDate) * 2)
SET #res = CASE WHEN #res < 0 THEN 0 ELSE #res END
RETURN #res
END
GO
Create function like:
CREATE FUNCTION dbo.fn_WorkDays(#StartDate DATETIME, #EndDate DATETIME= NULL )
RETURNS INT
AS
BEGIN
DECLARE #Days int
SET #Days = 0
IF #EndDate = NULL
SET #EndDate = EOMONTH(#StartDate) --last date of the month
WHILE DATEDIFF(dd,#StartDate,#EndDate) >= 0
BEGIN
IF DATENAME(dw, #StartDate) <> 'Saturday'
and DATENAME(dw, #StartDate) <> 'Sunday'
and Not ((Day(#StartDate) = 1 And Month(#StartDate) = 1)) --New Year's Day.
and Not ((Day(#StartDate) = 4 And Month(#StartDate) = 7)) --Independence Day.
BEGIN
SET #Days = #Days + 1
END
SET #StartDate = DATEADD(dd,1,#StartDate)
END
RETURN #Days
END
You can call the function like:
select dbo.fn_WorkDays('1/1/2016', '9/25/2016')
Or like:
select dbo.fn_WorkDays(StartDate, EndDate)
from table1
Create Function dbo.DateDiff_WeekDays
(
#StartDate DateTime,
#EndDate DateTime
)
Returns Int
As
Begin
Declare #Result Int = 0
While #StartDate <= #EndDate
Begin
If DateName(DW, #StartDate) not in ('Saturday','Sunday')
Begin
Set #Result = #Result +1
End
Set #StartDate = DateAdd(Day, +1, #StartDate)
End
Return #Result
End
I found the below TSQL a fairly elegant solution (I don't have permissions to run functions). I found the DATEDIFF ignores DATEFIRST and I wanted my first day of the week to be a Monday. I also wanted the first working day to be set a zero and if it falls on a weekend Monday will be a zero. This may help someone who has a slightly different requirement :)
It does not handle bank holidays
SET DATEFIRST 1
SELECT
,(DATEDIFF(DD, [StartDate], [EndDate]))
-(DATEDIFF(wk, [StartDate], [EndDate]))
-(DATEDIFF(wk, DATEADD(dd,-##DATEFIRST,[StartDate]), DATEADD(dd,-##DATEFIRST,[EndDate]))) AS [WorkingDays]
FROM /*Your Table*/
One approach is to 'walk the dates' from start to finish in conjunction with a case expression which checks if the day is not a Saturday or a Sunday and flagging it(1 for weekday, 0 for weekend). And in the end just sum flags(it would be equal to the count of 1-flags as the other flag is 0) to give you the number of weekdays.
You can use a GetNums(startNumber,endNumber) type of utility function which generates a series of numbers for 'looping' from start date to end date. Refer http://tsql.solidq.com/SourceCodes/GetNums.txt for an implementation. The logic can also be extended to cater for holidays(say if you have a holidays table)
declare #date1 as datetime = '19900101'
declare #date2 as datetime = '19900120'
select sum(case when DATENAME(DW,currentDate) not in ('Saturday', 'Sunday') then 1 else 0 end) as noOfWorkDays
from dbo.GetNums(0,DATEDIFF(day,#date1, #date2)-1) as Num
cross apply (select DATEADD(day,n,#date1)) as Dates(currentDate)