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
Related
I'm working on SQL-Server 2012 and have the following code example to get workdays between two dates
DECLARE #StartDate AS date
DECLARE #EndDate AS date
SET #StartDate = '2019/02/18' -- this is a monday
SET #EndDate = '2019/02/23' -- this is a saturday
SELECT
DATEDIFF(DD, #StartDate, #EndDate)
- (DATEDIFF(WK, #StartDate,#EndDate) * 2)
- CASE WHEN DATEPART(DW, #StartDate) = 1 THEN 1 ELSE 0 END
- CASE WHEN DATEPART(DW, #EndDate) = 1 THEN 1 ELSE 0 END
the result is 4, which is correct...
But If I put 2019/02/24 (sunday) for the EndDate I'm getting 3... ????
I'm getting crazy here...
You're validating your Enddate to be Sunday instead of Saturday. I had a similar function available that is independent of date settings.
SELECT ISNULL((((DATEDIFF(dd,#StartDate,#EndDate)) --Start with total number of days including weekends
- (DATEDIFF(wk,#StartDate,#EndDate)*2) --Subtact 2 days for each full weekend
- (1-SIGN(DATEDIFF(dd,6,#StartDate)%7)) --If StartDate is a Sunday, Subtract 1
- (1-SIGN(DATEDIFF(dd,5,#EndDate) %7)))) , 0) --If StartDate is a Saturday, Subtract 1
WHERE #StartDate <= #EndDate
The answer by #Luis Cazares works great except that when the start date or end date fall on a Saturday or Sunday, then the number is 1 off. Also, holidays are not accounted for (if that falls within the discussion).
Here's my way to construct a function.
CREATE FUNCTION [dbo].[getWorkDays] (#startDate date, #endDate date)
RETURNS int
AS
BEGIN
DECLARE #daysct int = 0;
DECLARE #currentDate date = #startDate;
IF #endDate>#startDate
GOTO COUNTIT;
ELSE
GOTO ZERO;
COUNTIT:
BEGIN
--Pre-process: if the startDate is holiday or weekend, then step in until the next business day
WHILE datepart(weekday, #currentDate) in (1, 7) OR EXISTS (SELECT holiday_date FROM dbo.holidays WHERE holiday_date = #currentDate) --if weekend or holiday
BEGIN
SET #currentDate = dateadd(day, 1, #currentDate);
SET #daysct=1;
END
WHILE #currentDate <= #endDate
BEGIN
IF #currentDate=#endDate
BREAK;
ELSE
IF datepart(weekday, #currentDate) not in (1, 7) AND NOT EXISTS (SELECT holiday_date FROM dbo.holidays WHERE holiday_date = #currentDate) --if not weekend and not holiday
BEGIN
SET #daysct=#daysct + 1;
END
SET #currentDate=dateadd(day, 1, #currentDate);
END
--Post-process: if the end date DOES NOT fall on a weekend or holiday, then #daysct is systematically 1 below. Add 1 to straighten it.
IF datepart(weekday, #endDate) in (1, 7) OR EXISTS (SELECT holiday_date FROM dbo.holidays WHERE holiday_date = #endDate)
SET #daysct = #daysct - 1;
RETURN(#daysct);
END
ZERO:
BEGIN
SET #daysct=0;
RETURN(#daysct);
END
END;
It's not the most efficient code but it's naturally the process how I would hand-count it. The catch is you need to declare a variable or constant, otherwise it will significantly slow down your queries.
DECLARE #days_worked int = dbo.getWorkDays(dateadd(day, 1, eomonth(getdate() AT TIME ZONE 'Pacific Standard Time', -1)),getdate() AT TIME ZONE 'Pacific Standard Time');
Also, before you can use it, make sure you have a table for the holidays. My table is constructed this way.
enter image description here
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
SET #StartDate = '2019/02/18'
SET #EndDate = '2019/02/23'
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)
I have This function
ALTER FUNCTION [General].[GetWeekEnding]
(
#Date DATETIME
)
RETURNS DATETIME
AS
BEGIN
-- Return the result of the function
RETURN (DATEADD(day, -1 - (DATEPART(dw, #Date) + ##DATEFIRST - 2) % 7, #Date) + 7)
END
I need to also set the time to 00:00:00.000 as well as finding the week ending of a provided date any thoughts?
I use a UDF for adding time components to dates e.g.
CREATE FUNCTION [dbo].[DateTimeAdd]
(
#datepart date,
#timepart time
)
RETURNS datetime2
AS
BEGIN
RETURN DATEADD(dd, DATEDIFF(dd, 0, #datepart), CAST(#timepart AS datetime2));
END
Then in your case you can use it like this:
SELECT dbo.DateTimeAdd(DATEADD (D, -1 * DatePart (DW, GetDate()) + 7, GetDate()), DATEADD(hh, 0, CAST(DATEADD(DAY, DATEDIFF(DAY, -1, GETDATE()), -1) AS TIME)))
Since SQL Server 2005, the best way to remove time is to cast to the date data type:
select cast(#Date as date)
I would suggest that you have your function return a date instead of a datetime.
To get to the week ending, you need a lookup table to map days of the week to integers. You can do this with datepart. However, that is subject to system settings. Here is a function:
Create FUNCTION [GetWeekEnding] (
#Date DATETIME,
#WeekEndingDOW varchar(10)
)
RETURNS DATE
AS
BEGIN
-- Return the result of the function
declare #newdate datetime;
with lookup as (
select 'Sunday' as dow, 0 as daynum union all
select 'Monday' as dow, 1 as daynum union all
select 'Tuesday' as dow, 2 as daynum union all
select 'Wednesday' as dow, 3 as daynum union all
select 'Thursday' as dow, 4 as daynum union all
select 'Friday' as dow, 5 as daynum union all
select 'Saturday' as dow, 6 as daynum
)
select #newdate = #Date - (select daynum from lookup where datename(dw, #date) = dow) + (select daynum from lookup where #WeekEndingDOW = dow);
select #newdate = (case when #newdate < #date then #newdate + 7 else #newdate end)
RETURN cast(#newdate as date)
END;
Note that this function does use datetime internally. For some reason that I cannot fathom, you can say "#date + 1" to mean "add one day to the date value" when #date is a datetime. However, this does not work when #date is a date. (This is also true of the dateadd function.)
For example:
#dtBegin = '2012-06-29'
#input = 20
I want the output to be '2012-07-27'.
I had to tackle the same problem in my project. Gordon Lionoff's solution got me on the right track but did not always produce the right result. I also have to take in account dates that start in a weekend. I.e. adding 1 business day to a saturday or sunday should result in a monday. This is the most common approach to handling business day calculation.
I've created my own solution based on Gordon Linoff's function and Patrick McDonald's excellent C# equivalent
NOTE: My solution only works if DATEFIRST is set to the default value of 7. If you use a different DATEFIRST value, you will have to change the 1, 7 and (1,7,8,9,10) bits.
My solution consists of two functions. An "outer" function that handles edge cases and an "inner" function that performs the actual calculation. Both functions are table-valued functions so they will actually be expanded into the implementing query and fed through the query optimizer.
CREATE FUNCTION [dbo].[UTL_DateAddWorkingDays]
(
#date datetime,
#days int
)
RETURNS TABLE AS RETURN
(
SELECT
CASE
WHEN #days = 0 THEN #date
WHEN DATEPART(dw, #date) = 1 THEN (SELECT Date FROM [dbo].[UTL_DateAddWorkingDays_Inner](DATEADD(d, 1, #date), #days - 1))
WHEN DATEPART(dw, #date) = 7 THEN (SELECT Date FROM [dbo].[UTL_DateAddWorkingDays_Inner](DATEADD(d, 2, #date), #days - 1))
ELSE (SELECT Date FROM [dbo].[UTL_DateAddWorkingDays_Inner](#date, #days))
END AS Date
)
As you can see, the "outer" function handles:
When adding no days, return the original date. This will keep saturday and sunday dates intact.
When adding days to a sunday, start counting from monday. This consumes 1 day.
When adding days to a saturday, start counting from monday. This consumes 1 day.
In all other cases, perform the usual calculation
_
CREATE FUNCTION [dbo].[UTL_DateAddWorkingDays_Inner]
(
#date datetime,
#days int
)
RETURNS TABLE AS RETURN
(
SELECT
DATEADD(d
, (#days / 5) * 7
+ (#days % 5)
+ (CASE WHEN ((#days%5) + DATEPART(dw, #date)) IN (1,7,8,9,10) THEN 2 ELSE 0 END)
, #date) AS Date
)
The "inner" function is similar to Gordon Linoff's solution, except it accounts for dates crossing weekend boundaries but without crossing a full week boundary.
Finally, I created a test script to test my function. The expected values were generated using Patrick McDonald's excellent C# equivalent and I randomly cross-referenced this data with this popular calculator.
You can do this without resorting to a calendar table or user defined function:
dateadd(d,
(#input / 5) * 7 + -- get complete weeks out of the way
mod(#input, 5) + -- extra days
(case when ((#input%5) + datepart(dw, #dtbegin)%7) in (7, 1, 8) or
((#input%5) + datepart(dw, #dtbegin)%7) < (#input%5)
then 2
else 0
end),
#dtbegin
)
I'm not saying this is pretty. But sometimes arithmetic is preferable to a join or a loop.
This is what I've tried:
CREATE function [dbo].[DateAddWorkDay]
(#days int,#FromDate Date)
returns Date
as
begin
declare #result date
set #result = (
select b
from
(
SELECT
b,
(DATEDIFF(dd, a, b))
-(DATEDIFF(wk, a, b) * 2)
-(CASE WHEN DATENAME(dw, a) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, b) = 'Saturday' THEN 1 ELSE 0 END)
-COUNT(o.Holiday_Date)
as workday
from
(
select
#FromDate as a,
dateadd(DAY,num +#days,#FromDate) as b
from (select row_number() over (order by (select NULL)) as num
from Information_Schema.columns
) t
where num <= 100
) dt
left join Holiday o on o.Holiday_Date between a and b and DATENAME(dw, o.Holiday_Date) not in('Saturday','Sunday')
where DATENAME(dw, b) not in('Saturday','Sunday')
and b not in (select Holiday_Date from OP_Holiday where Holiday_Date between a and b)
group by a,b
) du
where workday =#days
)
return #result
end
Where Holiday is a table with holiday_date as a reference for holiday.
I realize I'm about 8 years late to this party... But I took Martin's answer and updated it to:
1. a single scalar function instead of 2 nested table-valued functions
I tested using his original test script and my function passes too. Also, it seems the rebuild to a scalar function has a slight positive impact on performance. Both versions seem to benefit equally from 'buffer caching', the scalar version performing up to 25% better non-cached and up to 40% better cached. Disclaimer: I just ran both versions a bunch of times and recorded the times, I did not do any decent performance testing.
2. include support for DATEFIRST is either Monday, Saturday or Sunday
I feel a UDF should be agnostic to the datefirst setting. I'm in Europe and Monday is the default here. The original function would not work without adaptation.
According to wikipedia Monday, Saturday and Sunday are the only real world first days of the week. Support for other could easily be added, but would make the code more bulky and I have a hard time envisioning a real world use case.
CREATE FUNCTION dbo.fn_addWorkDays
(
#date datetime,
#days int
)
RETURNS DATETIME
AS
BEGIN
IF NOT ##DATEFIRST IN (1,6,7) BEGIN --UNSUPPORTED DATE FIRST
RETURN NULL
/* MONDAY = FRIST DAY */
END ELSE IF #days = 0 BEGIN
RETURN #date
END ELSE IF ##DATEFIRST = 1 AND DATEPART(dw, #date) = 7 BEGIN --SUNDAY
SET #date = DATEADD(d, 1, #date)
SET #days = #days - 1
END ELSE IF ##DATEFIRST = 1 AND DATEPART(dw, #date) = 6 BEGIN --SATURDAY
SET #date = DATEADD(d, 2, #date)
SET #days = #days - 1
/* SATURDAY = FRIST DAY */
END ELSE IF ##DATEFIRST = 7 AND DATEPART(dw, #date) = 2 BEGIN --SUNDAY
SET #date = DATEADD(d, 1, #date)
SET #days = #days - 1
END ELSE IF ##DATEFIRST = 7 AND DATEPART(dw, #date) = 1 BEGIN --SATURDAY
SET #date = DATEADD(d, 2, #date)
SET #days = #days - 1
/* SUNDAY = FRIST DAY */
END ELSE IF ##DATEFIRST = 7 AND DATEPART(dw, #date) = 1 BEGIN --SUNDAY
SET #date = DATEADD(d, 1, #date)
SET #days = #days - 1
END ELSE IF ##DATEFIRST = 7 AND DATEPART(dw, #date) = 7 BEGIN --SATURDAY
SET #date = DATEADD(d, 2, #date)
SET #days = #days - 1
END
DECLARE #return AS dateTime
SELECT #return =
DATEADD(d
, (#days / 5) * 7
+ (#days % 5)
+ (CASE
/* MONDAY = FRIST DAY */
WHEN ##DATEFIRST = 1 AND ((#days%5) + DATEPART(dw, #date)) IN (6,7,8,9) THEN 2
/* SATURDAY = FRIST DAY */
WHEN ##DATEFIRST = 7 AND ((#days%5) + DATEPART(dw, #date)) IN (1,2,8,9,10) THEN 2
/* SUNDAY = FRIST DAY */
WHEN ##DATEFIRST = 7 AND ((#days%5) + DATEPART(dw, #date)) IN (1,7,8,9,10,11) THEN 2
ELSE 0
END)
, #date)
RETURN #return
END
I hope this might benefit someone!
you can use the below mentioned code for exclude weekends
go
if object_id('UTL_DateAddWorkingDays') is not null
drop function UTL_DateAddWorkingDays
go
create FUNCTION [dbo].[UTL_DateAddWorkingDays]
(
#date datetime,
#daysToAdd int
)
RETURNS date
as
begin
declare #daysToAddCnt int=0,
#Dt datetime
declare #OutTable table
(
id int identity(1,1),
WeekDate date,
DayId int
)
while #daysToAdd>0
begin
set #Dt=dateadd(day,#daysToAddCnt,#date)
--select #daysToAddCnt cnt,#Dt date,DATEPART(weekday,#Dt) dayId,#daysToAdd daystoAdd
if(DATEPART(weekday,#Dt) <>7 and DATEPART(weekday,#Dt)<>1)
begin
insert into #outTable (WeekDate,DayId)
select #Dt,DATEPART(weekday,DATEADD(day,#daysToAddCnt,#Dt))
set #daysToAdd=#daysToAdd-1
end
set #daysToAddCnt=#daysToAddCnt+1
end
select #Dt=max(WeekDate) from #outTable
return #Dt
end
what about this?
declare #d1 date='2012-06-29'
declare #days int=20
select dateadd(dd,#days,#d1)
select dateadd(dd,case DATEPART(dw,t1.d) when 6 then +2 when 7 then +1 else +0 end,t1.d)
from
(
select dateadd(dd,CEILING((convert(float,#days)/5)*2)+#days,#d1)d
)t1
i found how many we in the range by CEILING((convert(float,#days)/5)*2)
then i added them to date and at the end i check if is a saturday or sunday and i add 1 or 2 days.
I need to calculate the number of FULL month in SQL, i.e.
2009-04-16 to 2009-05-15 => 0 full month
2009-04-16 to 2009-05-16 => 1 full month
2009-04-16 to 2009-06-16 => 2 full months
I tried to use DATEDIFF, i.e.
SELECT DATEDIFF(MONTH, '2009-04-16', '2009-05-15')
but instead of giving me full months between the two date, it gives me the difference of the month part, i.e.
1
anyone know how to calculate the number of full months in SQL Server?
The original post had some bugs... so I re-wrote and packaged it as a UDF.
CREATE FUNCTION FullMonthsSeparation
(
#DateA DATETIME,
#DateB DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #Result INT
DECLARE #DateX DATETIME
DECLARE #DateY DATETIME
IF(#DateA < #DateB)
BEGIN
SET #DateX = #DateA
SET #DateY = #DateB
END
ELSE
BEGIN
SET #DateX = #DateB
SET #DateY = #DateA
END
SET #Result = (
SELECT
CASE
WHEN DATEPART(DAY, #DateX) > DATEPART(DAY, #DateY)
THEN DATEDIFF(MONTH, #DateX, #DateY) - 1
ELSE DATEDIFF(MONTH, #DateX, #DateY)
END
)
RETURN #Result
END
GO
SELECT dbo.FullMonthsSeparation('2009-04-16', '2009-05-15') as MonthSep -- =0
SELECT dbo.FullMonthsSeparation('2009-04-16', '2009-05-16') as MonthSep -- =1
SELECT dbo.FullMonthsSeparation('2009-04-16', '2009-06-16') as MonthSep -- =2
select case when DATEPART(D,End_dATE) >=DATEPART(D,sTAR_dATE)
THEN ( case when DATEPART(M,End_dATE) = DATEPART(M,sTAR_dATE) AND DATEPART(YYYY,End_dATE) = DATEPART(YYYY,sTAR_dATE)
THEN 0 ELSE DATEDIFF(M,sTAR_dATE,End_dATE)END )
ELSE DATEDIFF(M,sTAR_dATE,End_dATE)-1 END
What's your definition of a month? Technically a month can be 28,29,30 or 31 days depending on the month and leap years.
It seems you're considering a month to be 30 days since in your example you disregarded that May has 31 days, so why not just do the following?
SELECT DATEDIFF(DAY, '2009-04-16', '2009-05-15')/30
, DATEDIFF(DAY, '2009-04-16', '2009-05-16')/30
, DATEDIFF(DAY, '2009-04-16', '2009-06-16')/30
The dateadd function can be used to offset to the beginning of the month. If the endDate has a day part less than startDate, it will get pushed to the previous month, thus datediff will give the correct number of months.
DATEDIFF(MONTH, DATEADD(DAY,-DAY(startDate)+1,startDate),DATEADD(DAY,-DAY(startDate)+1,endDate))
This is for ORACLE only and not for SQL-Server:
months_between(to_date ('2009/05/15', 'yyyy/mm/dd'),
to_date ('2009/04/16', 'yyyy/mm/dd'))
And for full month:
round(months_between(to_date ('2009/05/15', 'yyyy/mm/dd'),
to_date ('2009/04/16', 'yyyy/mm/dd')))
Can be used in Oracle 8i and above.
I know this is an old question, but as long as the dates are >= 01-Jan-1753 I use:
DATEDIFF(MONTH, DATEADD(DAY,-DAY(#Start)+1,#Start),DATEADD(DAY,-DAY(#Start)+1,#End))
DATEDIFF() is designed to return the number boundaries crossed between the two dates for the span specified. To get it to do what you want, you need to make an additional adjustment to account for when the dates cross a boundary but don't complete the full span.
WITH
-- Count how many months must be added to #StartDate to exceed #DueDate
MONTHS_SINCE(n, [Month_hence], [IsFull], [RemainingDays] ) AS (
SELECT
1 as n,
DATEADD(Day, -1, DATEADD(Month, 1, #StartDate)) AS Month_hence
,CASE WHEN (DATEADD(Day, -1, DATEADD(Month, 1, #StartDate)) <= #LastDueDate)
THEN 1
ELSE 0
END AS [IsFull]
,DATEDIFF(day, #StartDate, #LastDueDate) as [RemainingDays]
UNION ALL
SELECT
n+1,
--DateAdd(Month, 1, Month_hence) as Month_hence -- No, causes propagation of short month discounted days
DATEADD(Day, -1, DATEADD(Month, n+1, #StartDate)) as Month_hence
,CASE WHEN (DATEADD(Day, -1, DATEADD(Month, n+1, #StartDate)) <= #LastDueDate)
THEN 1
ELSE 0
END AS [IsFull]
,DATEDIFF(day, DATEADD(Day, -1, DATEADD(Month, n, #StartDate)), #LastDueDate)
FROM MONTHS_SINCE
WHERE Month_hence<( #LastDueDate --WHERE Period= 1
)
), --SELECT * FROM MONTHS_SINCE
MONTH_TALLY (full_months_over_all_terms, months_over_all_terms, days_in_incomplete_month ) AS (
SELECT
COALESCE((SELECT MAX(n) FROM MONTHS_SINCE WHERE isFull = 1),1) as full_months_over_all_terms,
(SELECT MAX(n) FROM MONTHS_SINCE ) as months_over_all_terms,
COALESCE((SELECT [RemainingDays] FROM MONTHS_SINCE WHERE isFull = 0),0) as days_in_incomplete_month
) SELECT * FROM MONTH_TALLY;
Is not necesary to create the function only the #result part. For example:
Select Name,
(SELECT CASE WHEN
DATEPART(DAY, '2016-08-28') > DATEPART(DAY, '2016-09-29')
THEN DATEDIFF(MONTH, '2016-08-28', '2016-09-29') - 1
ELSE DATEDIFF(MONTH, '2016-08-28', '2016-09-29') END) as NumberOfMonths
FROM
tableExample;
This answer follows T-SQL format. I conceptualize this problem as one of a linear-time distance between two date points in datetime format, call them Time1 and Time2; Time1 should be aligned to the 'older in time' value you are dealing with (say a Birth date or a widget Creation date or a journey Start date) and Time2 should be aligned with the 'newer in time' value (say a snapshot date or a widget completion date or a journey checkpoint-reached date).
DECLARE #Time1 DATETIME
SET #Time1 = '12/14/2015'
DECLARE #Time2 DATETIME
SET #Time2 = '12/15/2016'
The solution leverages simple measurement, conversion and calculations of the serial intersections of multiple cycles of different lengths; here: Century,Decade,Year,Month,Day (Thanks Mayan Calendar for the concept!). A quick note of thanks: I thank other contributors to Stack Overflow for showing me some of the component functions in this process that I've stitched together. I've positively rated these in my time on this forum.
First, construct a horizon that is the linear set of the intersections of the Century,Decade,Year,Month cycles, incremental by month. Use the cross join Cartesian function for this. (Think of this as creating the cloth from which we will cut a length between two 'yyyy-mm' points in order to measure distance):
SELECT
Linear_YearMonths = (centuries.century + decades.decade + years.[year] + months.[Month]),
1 AS value
INTO #linear_months
FROM
(SELECT '18' [century] UNION ALL
SELECT '19' UNION ALL
SELECT '20') centuries
CROSS JOIN
(SELECT '0' [decade] UNION ALL
SELECT '1' UNION ALL
SELECT '2' UNION ALL
SELECT '3' UNION ALL
SELECT '4' UNION ALL
SELECT '5' UNION ALL
SELECT '6' UNION ALL
SELECT '7' UNION ALL
SELECT '8' UNION ALL
SELECT '9') decades
CROSS JOIN
(SELECT '1' [year] UNION ALL
SELECT '2' UNION ALL
SELECT '3' UNION ALL
SELECT '4' UNION ALL
SELECT '5' UNION ALL
SELECT '6' UNION ALL
SELECT '7' UNION ALL
SELECT '8' UNION ALL
SELECT '9' UNION ALL
SELECT '0') years
CROSS JOIN
(SELECT '-01' [month] UNION ALL
SELECT '-02' UNION ALL
SELECT '-03' UNION ALL
SELECT '-04' UNION ALL
SELECT '-05' UNION ALL
SELECT '-06' UNION ALL
SELECT '-07' UNION ALL
SELECT '-08' UNION ALL
SELECT '-09' UNION ALL
SELECT '-10' UNION ALL
SELECT '-11' UNION ALL
SELECT '-12') [months]
ORDER BY 1
Then, convert your Time1 and Time2 date points into the 'yyyy-mm' format (Think of these as the coordinate cut points on the whole cloth). Retain the original datetime versions of the points as well:
SELECT
Time1 = #Time1,
[YYYY-MM of Time1] = CASE
WHEN LEFT(MONTH(#Time1),1) <> '1' OR MONTH(#Time1) = '1'
THEN (CAST(YEAR(#Time1) AS VARCHAR) + '-' + '0' + CAST(MONTH(#Time1) AS VARCHAR))
ELSE (CAST(YEAR(#Time1) AS VARCHAR) + '-' + CAST(MONTH(#Time1) AS VARCHAR))
END,
Time2 = #Time2,
[YYYY-MM of Time2] = CASE
WHEN LEFT(MONTH(#Time2),1) <> '1' OR MONTH(#Time2) = '1'
THEN (CAST(YEAR(#Time2) AS VARCHAR) + '-' + '0' + CAST(MONTH(#Time2) AS VARCHAR))
ELSE (CAST(YEAR(#Time2) AS VARCHAR) + '-' + CAST(MONTH(#Time2) AS VARCHAR))
END
INTO #datepoints
Then, Select the ordinal distance of 'yyyy-mm' units, less one to convert to cardinal distance (i.e. cut a piece of cloth from the whole cloth at the identified cut points and get its raw measurement):
SELECT
d.*,
Months_Between = (SELECT (SUM(l.value) - 1) FROM #linear_months l
WHERE l.[Linear_YearMonths] BETWEEN d.[YYYY-MM of Time1] AND d.[YYYY-MM of Time2])
FROM #datepoints d
Raw Output:
I call this a 'raw distance' because the month component of the 'yyyy-mm' cardinal distance may be one too many; the day cycle components within the month need to be compared to see if this last month value should count. In this example specifically, the raw output distance is '12'. But this wrong as 12/14 is before 12/15, so therefore only 11 full months have lapsed--its just one day shy of lapsing through the 12th month. We therefore have to bring in the intra-month day cycle to get to a final answer. Insert a 'month,day' position comparison between the to determine if the latest date point month counts nominally, or not:
SELECT
d.*,
Months_Between = (SELECT (SUM(l.value) - 1) FROM AZ_VBP.[MY].[edg_Linear_YearMonths] l
WHERE l.[Linear_YearMonths] BETWEEN d.[YYYY-MM of Time1] AND d.[YYYY-MM of Time2])
+ (CASE WHEN DAY(Time1) < DAY(Time2)
THEN -1
ELSE 0
END)
FROM #datepoints d
Final Output:
The correct answer of '11' is now our output. And so, I hope this helps. Thanks!
select CAST(DATEDIFF(MONTH, StartDate, EndDate) AS float) -
(DATEPART(dd,StartDate) - 1.0) / DATEDIFF(DAY, StartDate, DATEADD(MONTH, 1, StartDate)) +
(DATEPART(dd,EndDate)*1.0 ) / DATEDIFF(DAY, EndDate, DATEADD(MONTH, 1, EndDate))
I realize this is an old post, but I created this interesting solution that I think is easy to implement using a CASE statement.
Estimate the difference using DATEDIFF, and then test the months before and after using DATEADD to find the best date. This assumes Jan 31 to Feb 28 is 1 month (because it is).
DECLARE #First date = '2015-08-31'
DECLARE #Last date = '2016-02-28'
SELECT
#First as [First],
#Last as [Last],
DateDiff(Month, #First, #Last) as [DateDiff Thinks],
CASE
WHEN DATEADD(Month, DATEDIFF(Month, #First, #Last) +1, #First) <= #Last Then DATEDIFF(Month, #First, #Last) +1
WHEN DATEADD(Month, DATEDIFF(Month, #First, #Last) , #First) <= #Last Then DATEDIFF(Month, #First, #Last)
WHEN DATEADD(Month, DATEDIFF(Month, #First, #Last) -1, #First) <= #Last Then DATEDIFF(Month, #First, #Last) -1
END as [Actual Months Apart]
SIMPLE AND EASY WAY, Just Copy and Paste this FULL code to MS SQL and Execute :
declare #StartDate date='2019-01-31'
declare #EndDate date='2019-02-28'
SELECT
DATEDIFF(MONTH, #StartDate, #EndDate)+
(
case
when format(#StartDate,'yyyy-MM') != format(#EndDate,'yyyy-MM') AND DATEPART(DAY,#StartDate) > DATEPART(DAY,#EndDate) AND DATEPART(DAY,#EndDate) = DATEPART(DAY,EOMONTH(#EndDate)) then 0
when format(#StartDate,'yyyy-MM') != format(#EndDate,'yyyy-MM') AND DATEPART(DAY,#StartDate) > DATEPART(DAY,#EndDate) then -1
else 0
end
)
as NumberOfMonths
All you need to do is deduct the additional month if the end date has not yet passed the day of the month in the start date.
DECLARE #StartDate AS DATE = '2019-07-17'
DECLARE #EndDate AS DATE = '2019-09-15'
DECLARE #MonthDiff AS INT = DATEDIFF(MONTH,#StartDate,#EndDate)
SELECT #MonthDiff -
CASE
WHEN FORMAT(#StartDate,'dd') > FORMAT(#EndDate,'dd') THEN 1
ELSE 0
END
You can create this function to calculate absolute difference between two dates.
As I found using DATEDIFF inbuilt system function we will get the difference only in months, days and years. For example : Let say there are two dates 18-Jan-2018 and 15-Jan-2019. So the difference between those dates will be given by DATEDIFF in month as 12 months where as it is actually 11 Months 28 Days. So using the function given below, we can find absolute difference between two dates.
CREATE FUNCTION GetDurationInMonthAndDays(#First_Date DateTime,#Second_Date DateTime)
RETURNS VARCHAR(500)
AS
BEGIN
DECLARE #RESULT VARCHAR(500)=''
DECLARE #MONTHS TABLE(MONTH_ID INT,MONTH_NAME VARCHAR(100),MONTH_DAYS INT)
INSERT INTO #MONTHS
SELECT 1,'Jan',31
union SELECT 2,'Feb',28
union SELECT 3,'Mar',31
union SELECT 4,'Apr',30
union SELECT 5,'May',31
union SELECT 6,'Jun',30
union SELECT 7,'Jul',31
union SELECT 8,'Aug',31
union SELECT 9,'Sep',30
union SELECT 10,'Oct',31
union SELECT 11,'Nov',30
union SELECT 12,'Jan',31
IF(#Second_Date>#First_Date)
BEGIN
declare #month int=0
declare #days int=0
declare #first_year int
declare #second_year int
SELECT #first_year=Year(#First_Date)
SELECT #second_year=Year(#Second_Date)+1
declare #first_month int
declare #second_month int
SELECT #first_month=Month(#First_Date)
SELECT #second_month=Month(#Second_Date)
if(#first_month=2)
begin
IF((#first_year%100<>0) AND (#first_year%4=0) OR (#first_year%400=0))
BEGIN
SELECT #days=29-day(#First_Date)
END
else
begin
SELECT #days=28-day(#First_Date)
end
end
else
begin
SELECT #days=(SELECT MONTH_DAYS FROM #MONTHS WHERE MONTH_ID=#first_month)-day(#First_Date)
end
SELECT #first_month=#first_month+1
WHILE #first_year<#second_year
BEGIN
if(#first_month=13)
begin
set #first_month=1
end
WHILE #first_month<13
BEGIN
if(#first_year=Year(#Second_Date))
begin
if(#first_month=#second_month)
begin
SELECT #days=#days+DAY(#Second_Date)
break;
end
else
begin
SELECT #month=#month+1
end
end
ELSE
BEGIN
SELECT #month=#month+1
END
SET #first_month=#first_month+1
END
SET #first_year = #first_year + 1
END
select #month=#month+(#days/30)
select #days=#days%30
if(#days>0)
begin
SELECT #RESULT=CAST(#month AS VARCHAR)+' Month '+CAST(#days AS VARCHAR)+' Days '
end
else
begin
SELECT #RESULT=CAST(#month AS VARCHAR)+' Month '
end
END
ELSE
BEGIN
SELECT #RESULT='ERROR'
END
RETURN #RESULT
END
SELECT dateadd(dd,number,DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) AS gun FROM master..spt_values
WHERE type = 'p'
AND year(dateadd(dd,number,DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)))=year(DATEADD(yy, DATEDIFF(yy,0,getdate()), 0))
CREATE FUNCTION ufFullMonthDif (#dStart DATE, #dEnd DATE)
RETURNS INT
AS
BEGIN
DECLARE #dif INT,
#dEnd2 DATE
SET #dif = DATEDIFF(MONTH, #dStart, #dEnd)
SET #dEnd2 = DATEADD (MONTH, #dif, #dStart)
IF #dEnd2 > #dEnd
SET #dif = #dif - 1
RETURN #dif
END
GO
SELECT dbo.ufFullMonthDif ('2009-04-30', '2009-05-01')
SELECT dbo.ufFullMonthDif ('2009-04-30', '2009-05-29')
SELECT dbo.ufFullMonthDif ('2009-04-30', '2009-05-30')
SELECT dbo.ufFullMonthDif ('2009-04-16', '2009-05-15')
SELECT dbo.ufFullMonthDif ('2009-04-16', '2009-05-16')
SELECT dbo.ufFullMonthDif ('2009-04-16', '2009-06-16')
SELECT dbo.ufFullMonthDif ('2019-01-31', '2019-02-28')
Making Some changes to the Above function worked for me.
CREATE FUNCTION [dbo].[FullMonthsSeparation]
(
#DateA DATETIME,
#DateB DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #Result INT
DECLARE #DateX DATETIME
DECLARE #DateY DATETIME
IF(#DateA < #DateB)
BEGIN
SET #DateX = #DateA
SET #DateY = #DateB
END
ELSE
BEGIN
SET #DateX = #DateB
SET #DateY = #DateA
END
SET #Result = (
SELECT
CASE
WHEN DATEPART(DAY, #DateX) > DATEPART(DAY, #DateY)
THEN DATEDIFF(MONTH, #DateX, #DateY) - iif(EOMONTH(#DateY) = #DateY, 0, 1)
ELSE DATEDIFF(MONTH, #DateX, #DateY)
END
)
RETURN #Result
END
Declare #FromDate datetime, #ToDate datetime,
#TotalMonth int ='2021-10-01', #TotalDay='2021-12-31' int,
#Month int = 0
WHILE #ToDate > DATEADD(MONTH,#Month,#FromDate)
BEGIN
SET #Month = #Month +1
END
SET #TotalMonth = #Month -1
SET #TotalDay = DATEDIFF(DAY, DATEADD(MONTH,#TotalMonth, #FromDate),#ToDate) +1
IF(#TotalDay = DAY(EOMONTH(#ToDate)))
BEGIN
SET #TotalMonth = #TotalMonth +1
SET #TotalDay =0
END
Result #TotalMonth = 3, #TotalDay=0
if you are using PostGres only --
SELECT (DATE_PART('year', '2012-01-01'::date) - DATE_PART('year', '2011-10-02'::date)) * 12 +
(DATE_PART('month', '2012-01-01'::date) - DATE_PART('month', '2011-10-02'::date));
There are a lot of answers here that did not satisfy all the corner cases so I set about to fix them. This handles:
01/05/2021 - 02/04/2021 = 0 months
01/31/2021 - 02/28/2021 = 1 months
09/01/2021 - 10/31/2021 = 2 months
I think this generally handles all the cases needed.
declare #dateX date = '01/1/2022'
declare #datey date = '02/28/2022'
-- select datediff(month, #dateX, #datey) --Here for comparison
SELECT
CASE
WHEN DATEPART(DAY, #DateX) = 1 and DATEPART(DAY, #DateY) = DATEPART(DAY, eomonth(#DateY))
THEN DATEDIFF(MONTH, #DateX, #DateY) + 1
WHEN DATEPART(DAY, #DateX) > DATEPART(DAY, #DateY) and DATEPART(DAY, #DateY) != DATEPART(DAY, eomonth(#DateY))
THEN DATEDIFF(MONTH, #DateX, #DateY) - 1
ELSE DATEDIFF(MONTH, #DateX, #DateY)
END
I believe it is important to note that the question specifically asks for "full months between" AND that in the examples given each date is treated as "the START point of that date". This latter item is important because some comments state that year-01-31 to year-02-28 is a result of zero. This is correct. 1 complete day in January, plus 27 complete days in February (02-28 is the start of that day, so incomplete) is zero "full" months.
With that in mind I believe the following would meet the requirements IF StartDate is <= EndDate
(DATEPART(YEAR, EndDate) - DATEPART(YEAR, StartDate)) * 12
+ (DATEPART(MONTH, EndDate) - DATEPART(MONTH, StartDate))
- CASE WHEN DATEPART(DAY,EndDate) < DATEPART(DAY,StartDate) THEN 1 ELSE 0 END
To accommodate the possibility that the dates may be in any order then:
, CASE WHEN StartDate <= EndDate THEN
(DATEPART(YEAR, EndDate) - DATEPART(YEAR, StartDate)) * 12
+ (DATEPART(MONTH, EndDate) - DATEPART(MONTH, StartDate))
- CASE WHEN DATEPART(DAY,EndDate) < DATEPART(DAY,StartDate) THEN 1 ELSE 0 END
ELSE
(DATEPART(YEAR, StartDate) - DATEPART(YEAR, EndDate)) * 12
+ (DATEPART(MONTH, StartDate) - DATEPART(MONTH, EndDate))
- CASE WHEN DATEPART(DAY,StartDate) < DATEPART(DAY,EndDate) THEN 1 ELSE 0 END
END AS FullMnthsBtwn
For this sample:
select
StartDate, EndDate
into mytable
from (
values
(cast(getdate() as date),cast(getdate() as date)) -- both same date
-- original
,('2009-04-16','2009-05-15') -- > 0 full month
,('2009-04-16','2009-05-16') -- > 1 full month
,('2009-04-16','2009-06-16') -- > 2 full months
-- '1/31/2018' and endDate of '3/1/2018', I get a 0 – Eugene
, ('2018-01-31','2018-03-01')
-- some extras mentioned in comments, both of these should return 0 (in my opinion)
,('2009-01-31','2009-02-28')
,('2012-12-31','2013-02-28')
,('2022-05-15','2022-04-16') -- > 0 full month
,('2022-05-16','2022-04-16') -- > 1 full month
,('2021-06-16','2022-04-16') -- > 10 full months
) d (StartDate, EndDate)
query
select
StartDate
, EndDate
, CASE WHEN StartDate <= EndDate THEN
(DATEPART(YEAR, EndDate) - DATEPART(YEAR, StartDate)) * 12
+ (DATEPART(MONTH, EndDate) - DATEPART(MONTH, StartDate))
- CASE WHEN DATEPART(DAY,EndDate) < DATEPART(DAY,StartDate) THEN 1 ELSE 0 END
ELSE
(DATEPART(YEAR, StartDate) - DATEPART(YEAR, EndDate)) * 12
+ (DATEPART(MONTH, StartDate) - DATEPART(MONTH, EndDate))
- CASE WHEN DATEPART(DAY,StartDate) < DATEPART(DAY,EndDate) THEN 1 ELSE 0 END
END AS FullMnthsBtwn
from mytable
order by 1
result
+------------+------------+---------------+
| StartDate | EndDate | FullMnthsBtwn |
+------------+------------+---------------+
| 2009-01-31 | 2009-02-28 | 0 |
| 2009-04-16 | 2009-05-15 | 0 |
| 2009-04-16 | 2009-05-16 | 1 |
| 2009-04-16 | 2009-06-16 | 2 |
| 2012-12-31 | 2013-02-28 | 1 |
| 2018-01-31 | 2018-03-01 | 1 |
| 2021-06-16 | 2022-04-16 | 10 |
| 2022-05-15 | 2022-04-16 | 0 |
| 2022-05-16 | 2022-04-16 | 1 |
| 2022-07-09 | 2022-07-09 | 0 |
+------------+------------+---------------+
See db<>fiddle here (compares some other responses as well)
I got some ideas from the other answers, but none of them gave me exactly what I wanted.
The problem boils down to what I perceive a "month between" to be, which may be what others are also looking for also.
For example 25th February to 25th March would be one month to me, even though it is only 28 days. I would also consider 25th March to 25th April as one month at 31 days.
Also, I would consider 31st January to 2nd March as 1 month and 2 days even though it is 30 days between.
Also, fractions of a month are a bit meaningless as it depends on the length of a month and which month in the range do you choose to take a fraction of.
So, with that in mind, I came up with this function. It returns a decimal, the integer part is the number of months and the decimal part is the number of days, so a return value of 3.07 would mean 3 months and 7 days.
CREATE FUNCTION MonthsAndDaysBetween (#fromDt date, #toDt date)
RETURNS decimal(10,2)
AS
BEGIN
DECLARE #d1 date, #d2 date, #numM int, #numD int, #trc varchar(10);
IF(#fromDt < #toDt)
BEGIN
SET #d1 = #fromDt;
SET #d2 = #toDt;
END
ELSE
BEGIN
SET #d1 = #toDt;
SET #d2 = #fromDt;
END
IF DAY(#d1)>DAY(#d2)
SET #numM = year(#d2)*12+month(#d2)-year(#d1)*12-month(#d1)-1;
ELSE
SET #numM = year(#d2)*12+month(#d2)-year(#d1)*12-month(#d1);
IF YEAR(#d1) < YEAR(#d2) OR (YEAR(#d1) = YEAR(#d2) AND MONTH(#d1) < MONTH(#d2))
BEGIN
IF DAY(#d2) < DAY(#d1)
SET #numD = DAY(#d2) + DAY(EOMONTH(DATEADD(month,-1,#d2))) - DAY(#d1);
ELSE
SET #numD = DAY(#d2)-DAY(#d1);
END
ELSE
SET #numD = DAY(#d2)-DAY(#d1);
RETURN #numM + ABS(#numD) / 100.0;
END
In sql server, this formula works for going backward and forward in time.
DATEDIFF(month,#startdate, #enddate) + iif(#startdate <=#enddate,IIF(DAY(#startdate) > DAY(#enddate),-1,0),IIF(DAY(#startdate) < DAY(#enddate),+1, 0)))
SELECT 12 * (YEAR(end_date) - YEAR(start_date)) +
((MONTH(end_date) - MONTH(start_date))) +
SIGN(DAY(end_date) / DAY(start_date));
This works fine for me on SQL SERVER 2000.
Try:
trunc(Months_Between(date2, date1))
UPDATED
Right now, I just use
SELECT DATEDIFF(MONTH, '2019-01-31', '2019-02-28')
and SQL server returns the exact result (1).
I googled over internet.
And suggestion I found is to add +1 to the end.
Try do it like this:
Declare #Start DateTime
Declare #End DateTime
Set #Start = '11/1/07'
Set #End = '2/29/08'
Select DateDiff(Month, #Start, #End + 1)
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)