Calculating number of full months between two dates in SQL - sql

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)

Related

difference between two dates without weekends and holidays Sql query ORACLE

I have 2 tables: the 1st one contains the start date and the end date of a purchase order,
and the 2nd table contains year hollidays
-purchase order
-Holidays
I'm tryign to calculate the number of business days between 2 dates without the weekends and the holidays.
the output should be like this:
Start Date | End Date | Business Days
Could you please help me
You can remove the non-weekend holidays with a query like this:
select (t.end_date - t.start_date) - count(c.date)
from table1 t left join
calendar c
on c.date between t1.start_date and t1.end_date and
to_char(c.date, 'D') not in ('1', '7')
group by t.end_date, t.start_date;
Removing the weekend days is then more complication. Full weeks have two weekend days, so that is easy. So a good approximation is:
select (t.end_date - t.start_date) - (count(c.date) +
2 * floor((t.end_date - t.start_date) / 7))
from table1 t left join
calendar c
on c.date between t1.start_date and t1.end_date and
to_char(c.date, 'D') not in ('1', '7')
group by t.end_date, t.start_date;
This doesn't get the day of week, which is essentially if the end date is before the start date, then it is in the following week. However, this logic gets rather complicated the way that Oracle handles day of the week, so perhaps the above approximation is sufficient.
EDIT: I ignored the presence of the Oracle tag and jumped into scripting this for SQL Server. The concept doesn't change though.
To be super accurate, I would create a table whit the following format.
Year int, month int, DaysInMonth int, firstOccuranceOfSunday int
Create a Procedure to extract the weekends from a specific year and month on that table.
CREATE FUNCTION [dbo].[GetWeekendsForMonthYear]
(
#year int,
#month int
)
RETURNS #weekends TABLE
(
[Weekend] date
)
AS
BEGIN
declare #firstsunday int = 0
Declare #DaysInMonth int = 0
Select #DaysInMonth = DaysInMonth, #firstsunday = FirstSunday from Months
Where [Year] = #year and [month] = #month
Declare #FirstSaterday int = #firstsunday - 1
declare #CurrentDay int = 0
Declare #CurrentDayIsSunday bit = 0
if #FirstSaterday !< 1
Begin
insert into #Weekends values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #Firstsaterday -1, 0))))
insert into #Weekends values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #FirstSunday -1, 0))))
set #CurrentDayIsSunday = 1
set #CurrentDay = #firstsunday
END
else
begin
insert into #Weekends values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #FirstSunday -1, 0))))
set #FirstSaterday = #firstsunday + 6
insert into #Weekends values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #Firstsaterday -1, 0))))
set #CurrentDayIsSunday = 0
set #CurrentDay = #FirstSaterday
end
declare #done bit = 0
while #done = 0
Begin
if #CurrentDay <= #DaysInMonth
Begin
If #CurrentDayIsSunday = 1
begin
set #CurrentDay = #CurrentDay + 6
set #CurrentDayIsSunday = 0
if #CurrentDay <= #DaysInMonth
begin
insert into #Weekends Values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #CurrentDay -1, 0))))
end
end
else
begin
set #CurrentDay = #CurrentDay + 1
set #CurrentDayIsSunday = 1
if #CurrentDay <= #DaysInMonth
begin
insert into #Weekends Values(DATEADD(year, #year -1900, DATEADD(month, #month -1, DATEADD(day, #CurrentDay -1, 0))))
end
end
end
ELSE
begin
Set #done = 1
end
end
RETURN
END
When called and provided with a year and month this will return a list of dates that represent weekends.
Now, using that function, create a procedure to call this function once for every applicable row in a specific date rang and return the values in a temptable.
Note, I'm posting this now so you can see what's going on but I am continuing to work on the code. I will post updates as they arise.
More to come: Get list of weekends(formatted) for a specific daterange, remove any dates from that list which can be found on your holidays table.
Unfortunately, I have to work tomorrow and am off to bed.
This query should produce exact number of business days for each range in purchase table:
with days as (
select rn, sd + level - 1 dt, sd, ed
from (select row_number() over (order by start_date) rn,
start_date sd, end_date ed from purchase_order)
connect by prior rn = rn and sd + level - 1 <= ed
and prior dbms_random.value is not null)
select sd start_date, ed end_date, count(1) business_days
from days d left join holidays h on holiday_date = d.dt
where dt - trunc(dt, 'iw') not in (5, 6) and h.holiday_date is null
group by rn, sd, ed
SQLFiddle demo
For each row in purchase_orders query generates dates from this range (this is done by subquery dates).
Main query checks if this is weekend day or holiday day and counts rest of dates.
Hierarchical query used to generate dates may cause slowdowns if there is big number of data in purchase_orders
or periods are long. In this case preferred way is to create calendar table, as already suggested in comments.
Since you already have a table of holidays you can count the holidays between the starting and ending date and subtract that from the difference in days between your ending and starting date. For the weekends, you either need a table containing weekend days similar to your table of holidays, or you can generate them as below.
with sample_data(id, start_date, end_date) as (
select 1, date '2015-03-06', date '2015-03-7' from dual union all
select 2, date '2015-03-07', date '2015-03-8' from dual union all
select 3, date '2015-03-08', date '2015-03-9' from dual union all
select 4, date '2015-02-07', date '2015-06-26' from dual union all
select 5, date '2015-04-17', date '2015-08-16' from dual
)
, holidays(holiday) as (
select date '2015-01-01' from dual union all -- New Years
select date '2015-01-19' from dual union all -- MLK Day
select date '2015-02-16' from dual union all -- Presidents Day
select date '2015-05-25' from dual union all -- Memorial Day
select date '2015-04-03' from dual union all -- Independence Day (Observed)
select date '2015-09-07' from dual union all -- Labor Day
select date '2015-11-11' from dual union all -- Veterans Day
select date '2015-11-26' from dual union all -- Thanks Giving
select date '2015-11-27' from dual union all -- Black Friday
select date '2015-12-25' from dual -- Christmas
)
-- If your calendar table doesn't already hold weekends you can generate
-- the weekends with these next two subfactored queries (common table Expressions)
, firstweekend(weekend, end_date) as (
select next_day(min(start_date),'saturday'), max(end_date) from sample_data
union all
select next_day(min(start_date),'sunday'), max(end_date) from sample_data
)
, weekends(weekend, last_end_date) as (
select weekend, end_date from firstweekend
union all
select weekend + 7, last_end_date from weekends where weekend+7 <= last_end_date
)
-- if not already in the same table combine distinct weekend an holiday days
-- to prevent double counting (in case a holiday is also a weekend).
, days_off(day_off) as (
select weekend from weekends
union
select holiday from holidays
)
select id
, start_date
, end_date
, end_date - start_date + 1
- (select count(*) from days_off where day_off between start_date and end_date) business_days
from sample_data;
ID START_DATE END_DATE BUSINESS_DAYS
---------- ----------- ----------- -------------
1 06-MAR-2015 07-MAR-2015 1
2 07-MAR-2015 08-MAR-2015 0
3 08-MAR-2015 09-MAR-2015 1
4 07-FEB-2015 26-JUN-2015 98
5 17-APR-2015 16-AUG-2015 85

How to get all the weekend dates of the current year in SQL?

I tried but could not get the right solution. I want an SQL query that lists all the weekend dates of the current year.
I tried this SQL query:
WITH hier(num, lvl) AS (
SELECT 0, 1
UNION ALL
SELECT 100, 1
UNION ALL
SELECT num + 1, lvl + 1
FROM hier
WHERE lvl < 100
)
SELECT lvl [Week],
convert(date,DATEADD(dw, -DATEPART(dw, DATEADD(wk,DATEDIFF(wk,0,'12/31/'+convert(nvarchar,YEAR(getdate()))), 0)+6 ),
DATEADD(wk, DATEDIFF(wk,0,'12/31/'+convert(nvarchar,YEAR(getdate()))), 0)+6 ) - num * 7,101) [End Date]
FROM hier a
where num < 52
ORDER BY [End Date] asc
Its output is like this:
Week End date
52 2012-01-14
51 2012-01-21
50 2012-01-28
49 2012-02-04
I want the dates to start from the beginning – so, the above is missing one weekend, which is 2012-07-01. Also, I want the week numbers to show as 1, 2, 3... instead of 52, 51....
Check out this blog post.
Your question is explained in detail.
DECLARE #Year AS INT,
#FirstDateOfYear DATETIME,
#LastDateOfYear DATETIME
-- You can change #year to any year you desire
SELECT #year = 2010
SELECT #FirstDateOfYear = DATEADD(yyyy, #Year - 1900, 0)
SELECT #LastDateOfYear = DATEADD(yyyy, #Year - 1900 + 1, 0)
-- Creating Query to Prepare Year Data
;WITH cte AS (
SELECT 1 AS DayID,
#FirstDateOfYear AS FromDate,
DATENAME(dw, #FirstDateOfYear) AS Dayname
UNION ALL
SELECT cte.DayID + 1 AS DayID,
DATEADD(d, 1 ,cte.FromDate),
DATENAME(dw, DATEADD(d, 1 ,cte.FromDate)) AS Dayname
FROM cte
WHERE DATEADD(d,1,cte.FromDate) < #LastDateOfYear
)
SELECT FromDate AS Date, Dayname
FROM CTE
WHERE DayName IN ('Saturday','Sunday') -- For Weekend
/*
WHERE DayName LIKE 'Sunday'
WHERE DayName NOT IN ('Saturday','Sunday') -- For Weekday
WHERE DayName LIKE 'Monday' -- For Monday
WHERE DayName LIKE 'Sunday' -- For Sunday
*/
OPTION (MaxRecursion 370)
Will this help
DECLARE #startDate DATETIME, #endDate DATETIME
SELECT #startDate = '2012-01-01', #endDate = '2012-12-31'
;WITH Calender AS (
SELECT #startDate AS dt
UNION ALL
SELECT dt + 1 FROM Calender
WHERE dt + 1 <= #endDate
)
SELECT
dt
,NameMonth = DATENAME(Month, dt)
,NameDay = DATENAME (Weekday,dt)
,WeekofYr = DATEPART(WEEK, dt) FROM Calender
WHERE DATENAME (Weekday,dt) IN ('Sunday')
Option(MaxRecursion 0)
Result(Partial)
dt NameMonth NameDay WeekofYr
2012-01-01 00:00:00.000 January Sunday 1
2012-01-08 00:00:00.000 January Sunday 2
...............................................
...............................................
2012-12-30 00:00:00.000 December Sunday 53
you can try this
DECLARE #FirstDateOfYear DATETIME
SET #FirstDateOfYear = ’2010-01-01′
SELECT DISTINCT DATEADD(d, number, #FirstDateOfYear),
CASE DATEPART(dw, DATEADD(d, number, #FirstDateOfYear))
WHEN 7 THEN ‘Saturday’
WHEN 1 THEN ‘Sunday’
ELSE ‘Work Day’
END
FROM master..spt_values
WHERE number BETWEEN 0 AND 364
AND (DATEPART(dw, DATEADD(d, number, #FirstDateOfYear)) = 1 OR DATEPART(dw, DATEADD(d, number, #FirstDateOfYear)) = 7)
ORDER BY DATEADD(d, number, #FirstDateOfYear)
Try to find the first Saturday by doing this:
Start on 2012-01-01
If it's not a Saturday, add a day
Goto 2
Then, into a temporary table, add that date and the following date (Sunday).
After that, loop the following:
Add 7 and 8 days to the last Saturday you found (you get the following Saturday and Sunday)
Check whether they are still in 2012
If they are, store them in temp table and goto 1
There may be more elegant ways, but that's my quick & dirty solution. As you didn't post any code of what you've tried, I'll leave the implementation up to you.
this also works
declare #dat datetime, #add int
set #dat = '20120101'
set #add = datepart(w,#dat)
set #add = 5 - #add -- friday
set #dat = dateadd(d,#add,#dat)
while #dat <= '20121231'
begin
print #dat
set #dat = dateadd(d,7,#dat)
end
;with AllDaysOfYear (Day) as (
select DATEADD(year,DATEDIFF(year,0,CURRENT_TIMESTAMP),0) --Jan 1st
union all
select DATEADD(day,1,Day) from AllDaysOfYear
where DATEPART(year,DATEADD(day,1,Day)) = DATEPART(year,CURRENT_TIMESTAMP)
)
select
ROW_NUMBER() OVER (ORDER BY Day) as WeekNo,
Day
from
AllDaysOfYear
where
DATEPART(weekday,Day) = DATEPART(weekday,'20120714')
option (maxrecursion 0)
First, generate a set of all of the days in the current year (AllDaysInYear). Then, select those whose weekday is a saturday. The value I've used ('20120714') isn't terribly important - it just has to be any saturday, from any year. I'm just using it to avoid needing to have particular DATEFIRST or language settings.
This query shows how to get the first day of this year and the first day of the next year in the first part. The first day of the next year is calculated once so as not to keep getting and comparing the year parts.
;WITH cte(TheDate,NextYear) AS
(
SELECT CAST(CONVERT(CHAR(4),GETDATE(),112)+'0101' AS DATETIME),
CAST(YEAR(GETDATE())*10000+10101 AS CHAR(8))
UNION ALL
SELECT DateAdd(d,1,TheDate),NextYear
FROM cte
WHERE DateAdd(d,1,TheDate)<NextYear
)
SELECT Week = DatePart(wk,TheDate),
TheDate
FROM cte
WHERE DateName(dw,TheDate) in ('Saturday')
ORDER BY TheDate
OPTION (MAXRECURSION 366)
with t as
(
select 1 b
union all
select 1 b
union all
select 1 b
union all
select 1 b
union all
select 1 b
union all
select 1 b
union all
select 1 b
union all
select 1 b
)
select * from
(
select
current_timestamp
-datepart(dy,current_timestamp)
+row_number() over (order by t.b) d
from t, t t1, t t2
) tmp
where datepart(yyyy,d)=datepart(yyyy,current_timestamp)
and
DATENAME(dw,d)='sunday'
DECLARE #Year AS INT
SELECT #Year = 2020
;WITH weekends AS (
SELECT DATEFROMPARTS(#Year, 1, 1) AS dt
UNION ALL
SELECT DATEADD(DAY, 1, dt)
FROM weekends
WHERE dt < DATEFROMPARTS(#Year, 12, 31)
)
SELECT dt, DATENAME(MONTH, dt), DATENAME(DW, dt)
FROM weekends
WHERE DATEPART(DW, dt) IN (1, 7)
OPTION(MaxRecursion 366)

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

How to determine the number of days in a month in SQL Server?

I need to determine the number of days in a month for a given date in SQL Server.
Is there a built-in function? If not, what should I use as the user-defined function?
In SQL Server 2012 you can use EOMONTH (Transact-SQL) to get the last day of the month and then you can use DAY (Transact-SQL) to get the number of days in the month.
DECLARE #ADate DATETIME
SET #ADate = GETDATE()
SELECT DAY(EOMONTH(#ADate)) AS DaysInMonth
You can use the following with the first day of the specified month:
datediff(day, #date, dateadd(month, 1, #date))
To make it work for every date:
datediff(day, dateadd(day, 1-day(#date), #date),
dateadd(month, 1, dateadd(day, 1-day(#date), #date)))
Most elegant solution: works for any #DATE
DAY(DATEADD(DD,-1,DATEADD(MM,DATEDIFF(MM,-1,#DATE),0)))
Throw it in a function or just use it inline. This answers the original question without all the extra junk in the other answers.
examples for dates from other answers:
SELECT DAY(DATEADD(DD,-1,DATEADD(MM,DATEDIFF(MM,-1,'1/31/2009'),0))) Returns 31
SELECT DAY(DATEADD(DD,-1,DATEADD(MM,DATEDIFF(MM,-1,'2404-feb-15'),0))) Returns 29
SELECT DAY(DATEADD(DD,-1,DATEADD(MM,DATEDIFF(MM,-1,'2011-12-22'),0))) Returns 31
--Last Day of Previous Month
SELECT DATEPART(day, DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE()),0)))
--Last Day of Current Month
SELECT DATEPART(day, DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE())+1,0)))
--Last Day of Next Month
SELECT DATEPART(day, DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE())+2,0)))
Personally though, I would make a UDF for it if there is not a built in function...
I would suggest:
SELECT DAY(EOMONTH(GETDATE()))
This code gets you the number of days in current month:
SELECT datediff(dd,getdate(),dateadd(mm,1,getdate())) as datas
Change getdate() to the date you need to count days for.
--- sql server below 2012---
select day( dateadd(day,-1,dateadd(month, 1, convert(date,'2019-03-01'))))
-- this for sql server 2012--
select day(EOMONTH(getdate()))
Solution 1: Find the number of days in whatever month we're currently in
DECLARE #dt datetime
SET #dt = getdate()
SELECT #dt AS [DateTime],
DAY(DATEADD(mm, DATEDIFF(mm, -1, #dt), -1)) AS [Days in Month]
Solution 2: Find the number of days in a given month-year combo
DECLARE #y int, #m int
SET #y = 2012
SET #m = 2
SELECT #y AS [Year],
#m AS [Month],
DATEDIFF(DAY,
DATEADD(DAY, 0, DATEADD(m, ((#y - 1900) * 12) + #m - 1, 0)),
DATEADD(DAY, 0, DATEADD(m, ((#y - 1900) * 12) + #m, 0))
) AS [Days in Month]
You do need to add a function, but it's a simple one. I use this:
CREATE FUNCTION [dbo].[ufn_GetDaysInMonth] ( #pDate DATETIME )
RETURNS INT
AS
BEGIN
SET #pDate = CONVERT(VARCHAR(10), #pDate, 101)
SET #pDate = #pDate - DAY(#pDate) + 1
RETURN DATEDIFF(DD, #pDate, DATEADD(MM, 1, #pDate))
END
GO
SELECT Datediff(day,
(Convert(DateTime,Convert(varchar(2),Month(getdate()))+'/01/'+Convert(varchar(4),Year(getdate())))),
(Convert(DateTime,Convert(varchar(2),Month(getdate())+1)+'/01/'+Convert(varchar(4),Year(getdate()))))) as [No.of Days in a Month]
select datediff(day,
dateadd(day, 0, dateadd(month, ((2013 - 1900) * 12) + 3 - 1, 0)),
dateadd(day, 0, dateadd(month, ((2013 - 1900) * 12) + 3, 0))
)
Nice Simple and does not require creating any functions Work Fine
You need to create a function, but it is for your own convenience. It works perfect and I never encountered any faulty computations using this function.
CREATE FUNCTION [dbo].[get_days](#date datetime)
RETURNS int
AS
BEGIN
SET #date = DATEADD(MONTH, 1, #date)
DECLARE #result int = (select DAY(DATEADD(DAY, -DAY(#date), #date)))
RETURN #result
END
How it works: subtracting the date's day number from the date itself gives you the last day of previous month. So, you need to add one month to the given date, subtract the day number and get the day component of the result.
select add_months(trunc(sysdate,'MM'),1) - trunc(sysdate,'MM') from dual;
I upvoted Mehrdad, but this works as well. :)
CREATE function dbo.IsLeapYear
(
#TestYear int
)
RETURNS bit
AS
BEGIN
declare #Result bit
set #Result =
cast(
case when ((#TestYear % 4 = 0) and (#testYear % 100 != 0)) or (#TestYear % 400 = 0)
then 1
else 0
end
as bit )
return #Result
END
GO
CREATE FUNCTION dbo.GetDaysInMonth
(
#TestDT datetime
)
RETURNS INT
AS
BEGIN
DECLARE #Result int
DECLARE #MonthNo int
Set #MonthNo = datepart(m,#TestDT)
Set #Result =
case #MonthNo
when 1 then 31
when 2 then
case
when dbo.IsLeapYear(datepart(yyyy,#TestDT)) = 0
then 28
else 29
end
when 3 then 31
when 4 then 30
when 5 then 31
when 6 then 30
when 7 then 31
when 8 then 31
when 9 then 30
when 10 then 31
when 11 then 30
when 12 then 31
end
RETURN #Result
END
GO
To Test
declare #testDT datetime;
set #testDT = '2404-feb-15';
select dbo.GetDaysInMonth(#testDT)
here's another one...
Select Day(DateAdd(day, -Day(DateAdd(month, 1, getdate())),
DateAdd(month, 1, getdate())))
I know this question is old but I thought I would share what I'm using.
DECLARE #date date = '2011-12-22'
/* FindFirstDayOfMonth - Find the first date of any month */
-- Replace the day part with -01
DECLARE #firstDayOfMonth date = CAST( CAST(YEAR(#date) AS varchar(4)) + '-' +
CAST(MONTH(#date) AS varchar(2)) + '-01' AS date)
SELECT #firstDayOfMonth
and
DECLARE #date date = '2011-12-22'
/* FindLastDayOfMonth - Find what is the last day of a month - Leap year is handled by DATEADD */
-- Get the first day of next month and remove a day from it using DATEADD
DECLARE #lastDayOfMonth date = CAST( DATEADD(dd, -1, DATEADD(mm, 1, FindFirstDayOfMonth(#date))) AS date)
SELECT #lastDayOfMonth
Those could be combine to create a single function to retrieve the number of days in a month if needed.
SELECT DAY(SUBDATE(ADDDATE(CONCAT(YEAR(NOW()), '-', MONTH(NOW()), '-1'), INTERVAL 1 MONTH), INTERVAL 1 DAY))
Nice 'n' Simple and does not require creating any functions
Mehrdad Afshari reply is most accurate one, apart from usual this answer is based on formal mathematical approach given by Curtis McEnroe in his blog https://cmcenroe.me/2014/12/05/days-in-month-formula.html
DECLARE #date DATE= '2015-02-01'
DECLARE #monthNumber TINYINT
DECLARE #dayCount TINYINT
SET #monthNumber = DATEPART(MONTH,#date )
SET #dayCount = 28 + (#monthNumber + floor(#monthNumber/8)) % 2 + 2 % #monthNumber + 2 * floor(1/#monthNumber)
SELECT #dayCount + CASE WHEN #dayCount = 28 AND DATEPART(YEAR,#date)%4 =0 THEN 1 ELSE 0 END -- leap year adjustment
To get the no. of days in a month we can directly use Day() available in SQL.
Follow the link posted at the end of my answer for SQL Server 2005 / 2008.
The following example and the result are from SQL 2012
alter function dbo.[daysinm]
(
#dates nvarchar(12)
)
returns int
as
begin
Declare #dates2 nvarchar(12)
Declare #days int
begin
select #dates2 = (select DAY(EOMONTH(convert(datetime,#dates,103))))
set #days = convert(int,#dates2)
end
return #days
end
--select dbo.daysinm('08/12/2016')
Result in SQL Server SSMS
(no column name)
1 31
Process:
When EOMONTH is used, whichever the date format we use it is converted into DateTime format of SQL-server. Then the date output of EOMONTH() will be 2016-12-31 having 2016 as Year, 12 as Month and 31 as Days.
This output when passed into Day() it gives you the total days count in the month.
If we want to get the instant result for checking we can directly run the below code,
select DAY(EOMONTH(convert(datetime,'08/12/2016',103)))
or
select DAY(EOMONTH(convert(datetime,getdate(),103)))
for reference to work in SQL Server 2005/2008/2012, please follow the following external link ...
Find No. of Days in a Month in SQL
DECLARE #date DATETIME = GETDATE(); --or '12/1/2018' (month/day/year)
SELECT DAY(EOMONTH ( #date )) AS 'This Month';
SELECT DAY(EOMONTH ( #date, 1 )) AS 'Next Month';
result:
This Month
31
Next Month
30
DECLARE #m int
SET #m = 2
SELECT
#m AS [Month],
DATEDIFF(DAY,
DATEADD(DAY, 0, DATEADD(m, +#m -1, 0)),
DATEADD(DAY, 0, DATEADD(m,+ #m, 0))
) AS [Days in Month]
RETURN day(dateadd(month, 12 * #year + #month - 22800, -1))
select day(dateadd(month, 12 * year(date) + month(date) - 22800, -1))
A cleaner way of implementing this is using the datefromparts function to construct the first day of the month, and calculate the days from there.
CREATE FUNCTION [dbo].[fn_DaysInMonth]
(
#year INT,
#month INT
)
RETURNS INT
AS
BEGIN
IF #month < 1 OR #month > 12 RETURN NULL;
IF #year < 1753 OR #year > 9998 RETURN NULL;
DECLARE #firstDay DATE = datefromparts(#year, #month, 1);
DECLARE #lastDay DATE = dateadd(month, 1, #firstDay);
RETURN datediff(day, #firstDay, #lastDay);
END
GO
Similarily, you can calculate the days in a year:
CREATE FUNCTION [dbo].[fn_DaysInYear]
(
#year INT
)
RETURNS INT
AS
BEGIN
IF #year < 1753 OR #year > 9998 RETURN NULL;
DECLARE #firstDay DATE = datefromparts(#year, 1, 1);
DECLARE #lastDay DATE = dateadd(year, 1, #firstDay);
RETURN datediff(day, #firstDay, #lastDay);
END
GO
use SQL Server EOMONTH Function nested with day to get last day of month
select Day(EOMONTH('2020-02-1')) -- Leap Year returns 29
select Day(EOMONTH('2021-02-1')) -- returns 28
select Day(EOMONTH('2021-03-1')) -- returns 31
For any date
select DateDiff(Day,#date,DateAdd(month,1,#date))
select first_day=dateadd(dd,-1*datepart(dd,getdate())+1,getdate()),
last_day=dateadd(dd,-1*datepart(dd,dateadd(mm,1,getdate())),dateadd(mm,1,getdate())),
no_of_days = 1+datediff(dd,dateadd(dd,-1*datepart(dd,getdate())+1,getdate()),dateadd(dd,-1*datepart(dd,dateadd(mm,1,getdate())),dateadd(mm,1,getdate())))
replace any date with getdate to get the no of months in that particular date
DECLARE #Month INT=2,
#Year INT=1989
DECLARE #date DateTime=null
SET #date=CAST(CAST(#Year AS nvarchar) + '-' + CAST(#Month AS nvarchar) + '-' + '1' AS DATETIME);
DECLARE #noofDays TINYINT
DECLARE #CountForDate TINYINT
SET #noofDays = DATEPART(MONTH,#date )
SET #CountForDate = 28 + (#noofDays + floor(#noofDays/8)) % 2 + 2 % #noofDays + 2 * floor(1/#noofDays)
SET #noofDays= #CountForDate + CASE WHEN #CountForDate = 28 AND DATEPART(YEAR,#date)%4 =0 THEN 1 ELSE 0 END
PRINT #noofDays
DECLARE #date nvarchar(20)
SET #date ='2012-02-09 00:00:00'
SELECT DATEDIFF(day,cast(replace(cast(YEAR(#date) as char)+'-'+cast(MONTH(#date) as char)+'-01',' ','')+' 00:00:00' as datetime),dateadd(month,1,cast(replace(cast(YEAR(#date) as char)+'-'+cast(MONTH(#date) as char)+'-01',' ','')+' 00:00:00' as datetime)))
simple query in SQLServer2012 :
select day(('20-05-1951 22:00:00'))
i tested for many dates and it return always a correct result

SQL DateDiff advanced usage?

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