difference between two dates without weekends and holidays Sql query ORACLE - sql

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

Related

Get all dates between two dates in SQL Server

How to get all the dates between two dates?
I have a variable #MAXDATE which is storing the maximum date from the table. Now I want to get the all dates between #Maxdate and GETDATE() and want to store these dates in a cursor.
So far I have done as follows:
;with GetDates As
(
select DATEADD(day,1,#maxDate) as TheDate
UNION ALL
select DATEADD(day,1, TheDate) from GetDates
where TheDate < GETDATE()
)
This is working perfectly but when I am trying to store these values in a cursor
SET #DateCurSor = CURSOR FOR
SELECT TheDate
FROM GetDates
Compilation Error
Incorrect syntax near the keyword 'SET'.
How to solve this?
My first suggestion would be use your calendar table, if you don't have one, then create one. They are very useful. Your query is then as simple as:
DECLARE #MinDate DATE = '20140101',
#MaxDate DATE = '20140106';
SELECT Date
FROM dbo.Calendar
WHERE Date >= #MinDate
AND Date < #MaxDate;
If you don't want to, or can't create a calendar table you can still do this on the fly without a recursive CTE:
DECLARE #MinDate DATE = '20140101',
#MaxDate DATE = '20140106';
SELECT TOP (DATEDIFF(DAY, #MinDate, #MaxDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #MinDate)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b;
For further reading on this see:
Generate a set or sequence without loops – part 1
Generate a set or sequence without loops – part 2
Generate a set or sequence without loops – part 3
With regard to then using this sequence of dates in a cursor, I would really recommend you find another way. There is usually a set based alternative that will perform much better.
So with your data:
date | it_cd | qty
24-04-14 | i-1 | 10
26-04-14 | i-1 | 20
To get the quantity on 28-04-2014 (which I gather is your requirement), you don't actually need any of the above, you can simply use:
SELECT TOP 1 date, it_cd, qty
FROM T
WHERE it_cd = 'i-1'
AND Date <= '20140428'
ORDER BY Date DESC;
If you don't want it for a particular item:
SELECT date, it_cd, qty
FROM ( SELECT date,
it_cd,
qty,
RowNumber = ROW_NUMBER() OVER(PARTITION BY ic_id
ORDER BY date DESC)
FROM T
WHERE Date <= '20140428'
) T
WHERE RowNumber = 1;
You can use this script to find dates between two dates. Reference taken from this Article:
DECLARE #StartDateTime DATETIME
DECLARE #EndDateTime DATETIME
SET #StartDateTime = '2015-01-01'
SET #EndDateTime = '2015-01-12';
WITH DateRange(DateData) AS
(
SELECT #StartDateTime as Date
UNION ALL
SELECT DATEADD(d,1,DateData)
FROM DateRange
WHERE DateData < #EndDateTime
)
SELECT DateData
FROM DateRange
OPTION (MAXRECURSION 0)
GO
Just saying...here is a more simple approach to this:
declare #sdate date = '2017-06-25'
, #edate date = '2017-07-24';
with dates_CTE (date) as (
select #sdate
Union ALL
select DATEADD(day, 1, date)
from dates_CTE
where date < #edate
)
select *
from dates_CTE;
Easily create a Table Value Function that will return a table with all dates.
Input dates as string
You can customize the date in the the format you like '01/01/2017' or '01-01-2017' in string formats (103,126 ...)
Try this
CREATE FUNCTION [dbo].[DateRange_To_Table] ( #minDate_Str NVARCHAR(30), #maxDate_Str NVARCHAR(30))
RETURNS #Result TABLE(DateString NVARCHAR(30) NOT NULL, DateNameString NVARCHAR(30) NOT NULL)
AS
begin
DECLARE #minDate DATETIME, #maxDate DATETIME
SET #minDate = CONVERT(Datetime, #minDate_Str,103)
SET #maxDate = CONVERT(Datetime, #maxDate_Str,103)
INSERT INTO #Result(DateString, DateNameString )
SELECT CONVERT(NVARCHAR(10),#minDate,103), CONVERT(NVARCHAR(30),DATENAME(dw,#minDate))
WHILE #maxDate > #minDate
BEGIN
SET #minDate = (SELECT DATEADD(dd,1,#minDate))
INSERT INTO #Result(DateString, DateNameString )
SELECT CONVERT(NVARCHAR(10),#minDate,103), CONVERT(NVARCHAR(30),DATENAME(dw,#minDate))
END
return
end
To execute the function do this:
SELECT * FROM dbo.DateRange_To_Table ('01/01/2017','31/01/2017')
The output will be
01/01/2017 Sunday
02/01/2017 Monday
03/01/2017 Tuesday
04/01/2017 Wednesday
05/01/2017 Thursday
06/01/2017 Friday
07/01/2017 Saturday
08/01/2017 Sunday
09/01/2017 Monday
10/01/2017 Tuesday
11/01/2017 Wednesday
12/01/2017 Thursday
13/01/2017 Friday
14/01/2017 Saturday
15/01/2017 Sunday
16/01/2017 Monday
17/01/2017 Tuesday
18/01/2017 Wednesday
19/01/2017 Thursday
20/01/2017 Friday
21/01/2017 Saturday
22/01/2017 Sunday
23/01/2017 Monday
24/01/2017 Tuesday
25/01/2017 Wednesday
26/01/2017 Thursday
27/01/2017 Friday
28/01/2017 Saturday
29/01/2017 Sunday
30/01/2017 Monday
31/01/2017 Tuesday
This can be considered as bit tricky way as in my situation, I can't use a CTE table, so decided to join with sys.all_objects and then created row numbers and added that to start date till it reached the end date.
See the code below where I generated all dates in Jul 2018. Replace hard coded dates with your own variables (tested in SQL Server 2016):
select top (datediff(dd, '2018-06-30', '2018-07-31')) ROW_NUMBER()
over(order by a.name) as SiNo,
Dateadd(dd, ROW_NUMBER() over(order by a.name) , '2018-06-30') as Dt from sys.all_objects a
You can try this:
SET LANGUAGE SPANISH
DECLARE #startDate DATE = GETDATE() -- Your start date
DECLARE #endDate DATE = DATEADD(MONTH, 16, GETDATE()) -- Your end date
DECLARE #years INT = YEAR(#endDate) - YEAR(#startDate)
CREATE TABLE #TMP_YEARS (
[year] INT
)
-- Get all posible years between the start and end date
WHILE #years >= 0
BEGIN
INSERT INTO #TMP_YEARS
([year])
SELECT YEAR(#startDate) + #years
SET #years = #years - 1
END
;WITH [days]([day]) AS -- Posible days at a month
(
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 UNION ALL -- days lower than 10
SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12 UNION ALL SELECT 13 UNION ALL SELECT 14 UNION ALL SELECT 15 UNION ALL SELECT 16 UNION ALL SELECT 17 UNION ALL SELECT 18 UNION ALL SELECT 19 UNION ALL -- days lower than 20
SELECT 20 UNION ALL SELECT 21 UNION ALL SELECT 22 UNION ALL SELECT 23 UNION ALL SELECT 24 UNION ALL SELECT 25 UNION ALL SELECT 26 UNION ALL SELECT 27 UNION ALL SELECT 28 UNION ALL SELECT 29 UNION ALL -- days lower than 30
SELECT 30 UNION ALL SELECT 31 -- days higher 30
),
[months]([month]) AS -- All months at a year
(
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 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
)
SELECT CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, d.[day]))) + CONVERT(VARCHAR, d.[day]) as [date]
FROM #TMP_YEARS a
CROSS JOIN [months] n -- Join all years with all months
INNER JOIN [days] d on DAY(EOMONTH(CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + CONVERT(VARCHAR, DAY(EOMONTH(CAST(CONVERT(VARCHAR, a.[year]) + '-' + CONVERT(varchar, n.[month]) + '-15' AS DATE)))))) >= d.[day] AND -- The number of the day can't be higher than the last day of the current month and the current year
CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, d.[day]))) + CONVERT(VARCHAR, d.[day]) <= ISNULL(#endDate, GETDATE()) AND -- The current date can't be higher than the end date
CONVERT(VARCHAR, a.[year]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, n.[month]))) + CONVERT(VARCHAR, n.[month]) + '-' + REPLICATE('0', 2 - LEN(CONVERT(VARCHAR, d.[day]))) + CONVERT(VARCHAR, d.[day]) >= ISNULL(#startDate, GETDATE()) -- The current date should be higher than the start date
ORDER BY a.[year] ASC, n.[month] ASC, d.[day] ASC
The output will be something like this, you can format the date as you like:
2019-01-24
2019-01-25
2019-01-26
2019-01-27
2019-01-28
2019-01-29
2019-01-30
2019-01-31
2019-02-01
2019-02-02
2019-02-03
2019-02-04
2019-02-05
2019-02-06
2019-02-07
2019-02-08
2019-02-09
...
create procedure [dbo].[p_display_dates](#startdate datetime,#enddate datetime)
as
begin
declare #mxdate datetime
declare #indate datetime
create table #daterange (dater datetime)
insert into #daterange values (#startdate)
set #mxdate = (select MAX(dater) from #daterange)
while #mxdate < #enddate
begin
set #indate = dateadd(day,1,#mxdate)
insert into #daterange values (#indate)
set #mxdate = (select MAX(dater) from #daterange)
end
select * from #daterange
end
I listed dates of 2 Weeks later. You can use variable #period OR function datediff(dd, #date_start, #date_end)
declare #period INT, #date_start datetime, #date_end datetime, #i int;
set #period = 14
set #date_start = convert(date,DATEADD(D, -#period, curent_timestamp))
set #date_end = convert(date,current_timestamp)
set #i = 1
create table #datesList(dts datetime)
insert into #datesList values (#date_start)
while #i <= #period
Begin
insert into #datesList values (dateadd(d,#i,#date_start))
set #i = #i + 1
end
select cast(dts as DATE) from #datesList
Drop Table #datesList
This is the method that I would use.
DECLARE
#DateFrom DATETIME = GETDATE(),
#DateTo DATETIME = DATEADD(HOUR, -1, GETDATE() + 2); -- Add 2 days and minus one hour
-- Dates spaced a day apart
WITH MyDates (MyDate)
AS (
SELECT #DateFrom
UNION ALL
SELECT DATEADD(DAY, 1, MyDate)
FROM MyDates
WHERE MyDate < #DateTo
)
SELECT
MyDates.MyDate
, CONVERT(DATE, MyDates.MyDate) AS [MyDate in DATE format]
FROM
MyDates;
Here is a similar example, but this time the dates are spaced one hour apart to further aid understanding of how the query works:
-- Alternative example with dates spaced an hour apart
WITH MyDates (MyDate)
AS (SELECT #DateFrom
UNION ALL
SELECT DATEADD(HOUR, 1, MyDate)
FROM MyDates
WHERE MyDate < #DateTo
)
SELECT
MyDates.MyDate
FROM
MyDates;
As you can see, the query is fast, accurate and versatile.
You can use SQL Server recursive CTE
DECLARE
#MinDate DATE = '2020-01-01',
#MaxDate DATE = '2020-02-01';
WITH Dates(day) AS
(
SELECT CAST(#MinDate as Date) as day
UNION ALL
SELECT CAST(DATEADD(day, 1, day) as Date) as day
FROM Dates
WHERE CAST(DATEADD(day, 1, day) as Date) < #MaxDate
)
SELECT* FROM dates;
declare #start_dt as date = '1/1/2021'; -- Date from which the calendar table will be created.
declare #end_dt as date = '1/1/2022'; -- Calendar table will be created up to this date (not including).
declare #dates as table (
date_id date primary key,
date_year smallint,
date_month tinyint,
date_day tinyint,
weekday_id tinyint,
weekday_nm varchar(10),
month_nm varchar(10),
day_of_year smallint,
quarter_id tinyint,
first_day_of_month date,
last_day_of_month date,
start_dts datetime,
end_dts datetime
)
while #start_dt < #end_dt
begin
insert into #dates(
date_id, date_year, date_month, date_day,
weekday_id, weekday_nm, month_nm, day_of_year, quarter_id,
first_day_of_month, last_day_of_month,
start_dts, end_dts
)
values(
#start_dt, year(#start_dt), month(#start_dt), day(#start_dt),
datepart(weekday, #start_dt), datename(weekday, #start_dt), datename(month, #start_dt), datepart(dayofyear, #start_dt), datepart(quarter, #start_dt),
dateadd(day,-(day(#start_dt)-1),#start_dt), dateadd(day,-(day(dateadd(month,1,#start_dt))),dateadd(month,1,#start_dt)),
cast(#start_dt as datetime), dateadd(second,-1,cast(dateadd(day, 1, #start_dt) as datetime))
)
set #start_dt = dateadd(day, 1, #start_dt)
end
-- sample of the data
select
top 50 *
--into master.dbo.DimDate
from #dates d
order by date_id
DECLARE #FirstDate DATE = '2018-01-01'
DECLARE #LastDate Date = '2018-12-31'
DECLARE #tbl TABLE(ID INT IDENTITY(1,1) PRIMARY KEY,CurrDate date)
INSERT #tbl VALUES( #FirstDate)
WHILE #FirstDate < #LastDate
BEGIN
SET #FirstDate = DATEADD( day,1, #FirstDate)
INSERT #tbl VALUES( #FirstDate)
END
INSERT #tbl VALUES( #LastDate)
SELECT * FROM #tbl

How can I generate Week ending dates (Saturdays) within a date range

I need to generate either a column in a query or a temp table (not sure which one is required)
so that I can have a list of dates that are on Saturday that fall within a given date range.
This list will be used in a join to associate records with weeks.
What are my options?
Sample Input:
From: 03/01/2013
To: 04/30/2013
Results:
Week Ending
- 03/02/2013
- 03/09/2013
- 03/16/2013
- 03/23/2013
- 03/30/2013
- 04/06/2013
- 04/13/2013
- 04/20/2013
- 04/27/2013
- 05/04/2013
Current code:
create table #TBL7(YEAR INT, WEEKNUMBER INT, STARTDATE DATETIME, ENDDATE DATETIME)
begin
declare #startdate datetime
, #enddate datetime
, #ctr int
SET #startdate = CAST(2013 AS VARCHAR)+ '/01/01'
SET #enddate = CAST(2013 AS VARCHAR) + '/12/31'
SET #ctr = 0
WHILE #enddate >= #startdate
BEGIN
SET #ctr = #ctr + 1
INSERT INTO #TBL7
values(year(#startdate), #ctr, #startdate, #startdate + 6)
SET #startdate = #startdate + 7
END
end
select * from #TBL7
First, create a calendar table. Then you have a very simple query:
select [Date]
from dbo.Calendar
where DayOfWeek = 'Saturday' and [Date] between '20130301' and '20130430'
A calendar table is almost always the best approach to working with dates because you're working with data, not code, so you can see it's correct and there's no cryptic code to maintain.
This is Oracle code. Sorry I do not know how to convert this to SQL SERVER. Should not be very hard. All you need is to use proper functions in place of to_date() and to_char(), and calc the difference between start and end date, e.g. (end_date-start_date)+1:
WITH data(r, some_date) AS
(
SELECT 1 r, to_date('03/01/2013', 'MM/DD/YYYY') some_date FROM dual
UNION ALL
SELECT r+1, to_date('03/01/2013', 'MM/DD/YYYY')+r FROM data WHERE r < 61 -- (end_date-start_date)+1
)
SELECT some_date
, To_Char(some_date, 'DY') wk_day
FROM data
WHERE To_Char(some_date, 'DY') = 'SAT'
/
SOME_DATE WK_DAY
--------------------
3/2/2013 SAT
3/9/2013 SAT
3/16/2013 SAT
3/23/2013 SAT
3/30/2013 SAT
4/6/2013 SAT
4/13/2013 SAT
4/20/2013 SAT
4/27/2013 SAT
This should work:
WITH cteWeeks (WeekEnding) As
(
-- Find the Saturday of the first week.
-- Need to allow for different DATEFIRST settings:
SELECT
CASE
WHEN DatePart(dw, DateAdd(day, ##datefirst, #StartDate)) = 7 THEN #StartDate
ELSE DateAdd(day, 7 - DatePart(dw, DateAdd(day, ##datefirst, #StartDate)), #StartDate)
END
UNION ALL
SELECT
DateAdd(day, 7, WeekEnding)
FROM
cteWeeks
WHERE
WeekEnding < #EndDate
)
SELECT
WeekEnding
FROM
cteWeeks
;
http://www.sqlfiddle.com/#!3/d41d8/12095

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)

Possible recursive CTE query using date ranges

Not sure how to even phrase the title on this one!
I have the following data:
IF OBJECT_ID ('tempdb..#data') IS NOT NULL DROP TABLE #data
CREATE TABLE #data
(
id UNIQUEIDENTIFIER
,reference NVARCHAR(30)
,start_date DATETIME
,end_date DATETIME
,lapse_date DATETIME
,value_received DECIMAL(18,3)
)
INSERT INTO #data VALUES ('BE91B9C1-C02F-46F7-9B63-4D0B25D9BA2F','168780','2006-05-01 00:00:00.000',NULL,'2011-09-27 00:00:00.000',537.42)
INSERT INTO #data VALUES ('B538F123-C839-447A-B300-5D16EACF4560','320858','2011-08-08 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('1922465D-2A55-434D-BAAA-8E15D681CF12','306597','2011-04-08 00:00:00.000','2011-06-22 13:14:40.083','2011-08-07 00:00:00.000',12)
INSERT INTO #data VALUES ('7DF8FBCC-B490-4892-BDC5-8FD2D73B0323','321461','2011-07-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',8.44)
INSERT INTO #data VALUES ('1EC2E754-F325-4313-BDFC-9010E255F6FE','74215','2000-10-31 00:00:00.000',NULL,'2011-08-30 00:00:00.000',258)
INSERT INTO #data VALUES ('9E59B09C-0198-48AC-8EEC-A0D76CEA9385','169194','2008-06-25 00:00:00.000',NULL,'2011-09-25 00:00:00.000',1766.4)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','288039','2010-09-01 00:00:00.000','2011-07-29 00:00:00.000','2011-08-21 00:00:00.000',55)
INSERT INTO #data VALUES ('97CF6C0F-324A-49A6-B9D8-AC848A1F821A','324423','2011-08-01 00:00:00.000',NULL,'2011-09-25 00:00:00.000',5)
INSERT INTO #data VALUES ('D5E5197A-E8E1-468C-9991-C8712224C2BF','323395','2011-08-25 00:00:00.000',NULL,NULL,0)
INSERT INTO #data VALUES ('0EC4976C-16B9-4C99-BD07-D0CBDF014D32','323741','2011-08-25 00:00:00.000',NULL,NULL,0)
And I want to be able to group all references into a category of 'active', 'lapsed' or 'new' based upon the following criteria:
Active has a start date that is less than the last date of the reference month, a lapse date after the last day of the prior month and a value_received > 0;
New has a start date which falls within the reference month;
Lapsed has a lapse date which falls within the reference month.
And to then apply these definitions for each reference for a rolling 13 months (so from Now going back as far as July 2010) so that for each month I can see how many references fall into each group.
I am able to use the following to define this for the current month:
select
id
,reference
,start_date
,end_date
,lapse_date
,value_received
,CASE WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) --last day of current month
AND value_received > 0
THEN 'Active'
WHEN lapse_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start
AND lapse_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
THEN 'lapse'
WHEN start_date < DATEADD(month,DATEPART(Month,GETDATE()) + 1,DATEADD(year,DATEPART(year,GETDATE())-1900,0)) --next month start date
AND start_date > DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE()),0)) --last day of prior month
THEN 'New'
ELSE 'Not applicable'
END AS [type]
from #data
But I can't see a nice / efficient way of doing this (other than to repeat this query 13 times and union the results, which I know is just awful)
Would this be a case for using the current month as an anchor and using recursion (if so, some pointers would be most appreciated)?
Any help most appreciated as always :)
* Edited to include actual solution *
In case it's of interest to anyone, this is the final query I used:
;WITH Months as
(
SELECT DATEADD(ms,-3,DATEADD(mm,DATEDIFF(mm,0,GETDATE())+1,0)) as month_end
,0 AS level
UNION ALL
SELECT DATEADD(month, -1, month_end)as month_end
,level + 1 FROM Months
WHERE level < 13
)
SELECT
DATENAME(Month,month_end) + ' ' + DATENAME(YEAR,month_end) as date
,SUM(CASE WHEN start_date <= month_end
AND Month(start_date) <> MONTH(Month_end)
AND lapse_date > Month_end
THEN 1 ELSE 0 END) AS Active
,SUM(CASE WHEN start_date <= Month_end
AND DATENAME(MONTH,start_date) + ' ' + DATENAME(YEAR,start_date) =
DATENAME(MONTH,month_end) + ' ' + DATENAME(YEAR,month_end)
THEN 1 ELSE 0 END) AS New
,SUM(CASE WHEN lapse_date <= Month_end
AND Month(lapse_date) = MONTH(Month_end)
THEN 1 ELSE 0 END) AS lapse
FROM #data
CROSS JOIN Months
WHERE id IS NOT NULL
AND start_date IS NOT NULL
GROUP BY DATENAME(Month,month_end) + ' ' + DATENAME(YEAR,month_end)
ORDER by MAX(level) ASC
You don't need a "real" recursive CTE here. You can use one for the month references though:
;WITH Months
as
(
SELECT DATEADD(day, -DATEPART(day, GETDATE())+1, GETDATE()) as 'MonthStart'
UNION ALL
SELECT DATEADD(month, -1, MonthStart) as 'MonthStart'
FROM Months
)
Then you can JOIN to SELECT TOP 13 * FROM Months in your above query.
I'm not going to try to parse all your CASE statements, but essentially you can use a GROUP BY on the date and the MonthStart fields, like:
GROUP BY Datepart(year, monthstart), Datepart(month, monthstart)
and aggregate by month. It will probably be easiest to have all your options (active, lapsed, etc) as columns and calculate each with a SUM(CASE WHEN ... THEN 1 ELSE 0 END) as it will be easier with a GROUP BY.
You can cross join your request with a recursive CTE, this is a good idea.
WITH thirteenMonthBack(myDate, level) as
(
SELECT GETDATE() as myDate, 0 as level
UNION ALL
SELECT DATEADD(month, -1, myDate), level + 1
FROM thirteenMonthBack
WHERE level < 13
)
SELECT xxx
FROM youQuery
CROSS JOIN thirteenMonthBack
DECLARE #date DATE = GETDATE()
;WITH MonthsCTE AS (
SELECT 1 [Month], DATEADD(DAY, -DATEPART(DAY, #date)+1, #date) as 'MonthStart'
UNION ALL
SELECT [Month] + 1, DATEADD(MONTH, 1, MonthStart)
FROM MonthsCTE
WHERE [Month] < 12 )
SELECT * FROM MonthsCTE
/*
| The below SELECT statements show TWO examples of how this can be useful.
| Example 1 SELECT: Simple example of showing how to generate 12 days ahead based on date entered
| Example 2 SELECT: This example shows how to generate 12 months ahead based on date entered
| This example tries to mimic as best it can Oracles use of LEVEL and CONNECT BY LEVEL
*/
WITH dynamicRecords(myDate, level) AS
(
SELECT GETDATE() AS myDate, 1 AS level
UNION ALL
SELECT myDate + 1, level + 1 /* 12 Days - WHERE level < 12 */
--SELECT DATEADD(month, 1, myDate), level + 1 /* 12 Months - WHERE level < 12 */
FROM dynamicRecords
WHERE level < 12
)
SELECT *
FROM dynamicRecords
Option (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */
Screenshots:
/* Original T-SQL Solution I found here: https://riptutorial.com/sql-server/example/11098/generating-date-range-with-recursive-cte
| The below provides an example of how to generate the days within a date range of the dates entered.
| The below SELECT statements show TWO examples of how this can be useful.
| Example 1 SELECT: Uses static dates to display ALL of the dates within the range for the dates entered
| Example 2 SELECT: This example uses GETDATE() and then obtains the FOM day and the EOM day of the dates
| beging entered to then show all days in the month of the dates entered.
*/
With DateCte AS
(
SELECT CAST('2021-04-21' AS DATE) AS BeginDate, CAST('2022-05-02' AS DATE) AS EndDate
--SELECT CAST( GETDATE() - Day(GETDATE()) + 1 AS DATE ) AS BeginDate, CAST(EOMONTH(GETDATE()) AS DATE) AS EndDate
UNION ALL
SELECT DateAdd(Day, 1, BeginDate), EndDate
FROM DateCte
WHERE BeginDate < EndDate
)
Select BeginDate AS Dates
From DateCte
Option (MaxRecursion 0) /* The default MaxRecursion setting is 100. Generating more than 100 dates using this method will require the Option (MaxRecursion N) segment of the query, where N is the desired MaxRecursion setting. Setting this to 0 will remove the MaxRecursion limitation altogether */
;
Screenshot:

Calculating number of full months between two dates in 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)