Date split-up based on Fiscal Year - sql

Given a From Date, To Date and a Fiscal Year system, I want to get all the split-up duration within the
given From & To Date based on the Fiscal Year system. Explained below with examples:
Example 1:
Fiscal Year system: Apr to Mar
From Date: Jan-05-2008
To Date: May-15-2008
Based on Fiscal Year system, duration should be splitted into:
Jan-05-2008 to Mar-31-2008
Apr-01-2008 to May-15-2008
Example 2:
Fiscal Year system: Apr to Mar
From Date: Jan-17-2008
To Date: May-20-2009
Based on Fiscal Year system, duration should be splitted into:
Jan-17-2008 to Mar-31-2008
Apr-01-2008 to Mar-31-2009
Apr-01-2009 to May-20-2009
Am looking for approach/algorithm to solve this in PostgreSQL 8.2.
Regards,
Gnanam

I actually favor Andomar's solution (with the addition of a processes that automatically fills the Periods table), but for fun here's a solution that doesn't require it.
CREATE TABLE your_table (start_date date, end_date date);
INSERT INTO your_table VALUES ('Jan-17-2008', 'May-20-2009');
SELECT
GREATEST(start_date, ('04-01-'||series.year)::date) AS year_start,
LEAST(end_date, ('03-31-'||series.year + 1)::date) AS year_end
FROM
(SELECT
start_date,
end_date,
generate_series(
date_part('year', your_table.start_date - INTERVAL '3 months')::int,
date_part('year', your_table.end_date - INTERVAL '3 months')::int)
FROM your_table) AS series(start_date, end_date, year)
ORDER BY
start_date;

You could create a table containing the start and end of all fiscal years, f.e.
Periods (PeriodStartDt, PeriodEndDt)
Then you can join the tables together if they at least partly overlap. Use a case statement to select the end of the period or the end of the row, depending on which is later. For example (not tested):
select case when yt.StartDt < p.PeriodStartDt then p.PeriodStartDt
else yt.StartDt
end as SplitStart
, case when yt.EndDt > p.PeriodEndDt then p.PeriodEndDt
else yt.EndDt
end as SplitEnd
, yt.*
from YourTable yt
inner join Periods p
on yt.StartDt < p.PeriodEndDate
and yt.EndDt >= p.PeriodStartDate

CREATE TABLE your_table (start_date date, end_date date);
INSERT INTO your_table VALUES (CONVERT (date, GETDATE()),CONVERT (date, DATEADD(year, -1, GETDATE())) );
WITH mycte AS
(
SELECT 1 as id
UNION ALL
SELECT id + 1
FROM mycte
WHERE id + 1 < = 12
),
cte_distribution as
(
SELECT *,
DATEPART (month,DATEADD(month, mycte.id - 1, your_table.start_date)) as month_number ,
DATEPART (YEAR,DATEADD(month, mycte.id - 1, your_table.start_date)) as cal_year,
12000/12 as cash
FROM your_table
CROSS JOIN mycte
)
select
*,
(CASE WHEN month_number between 1 and 3 THEN '1st quarter' WHEN month_number between 4 and 6 THEN '2nd quarter' WHEN month_number between 7 and 9 THEN '3rd quarter' WHEN month_number between 9 and 12 THEN '4th quarter' END) as Quarter,
CASE WHEN month_number between 1 and 6 THEN CAST(CAST((cal_year - 1) as CHAR(4)) + '-' + CAST(cal_year as CHAR(4)) AS CHAR(9)) WHEN month_number between 6 and 12 THEN CAST(CAST((cal_year) as CHAR(4)) + '-' + CAST((cal_year + 1) as CHAR(4)) AS CHAR(9)) ELSE NULL END as fin_year
from cte_distribution;

Related

PostgreSQL generate month and year series based on table field and fill with nulls if no data for a given month

I want to generate series of month and year from the next month of current year(say, start_month) to 12 months from start_month along with the corresponding data (if any, else return nulls) from another table in PostgreSQL.
SELECT ( ( DATE '2019-03-01' + ( interval '1' month * generate_series(0, 11) ) )
:: DATE ) dd,
extract(year FROM ( DATE '2019-03-01' + ( interval '1' month *
generate_series(0, 11) )
)),
coalesce(SUM(price), 0)
FROM items
WHERE s.date_added >= '2019-03-01'
AND s.date_added < '2020-03-01'
AND item_type_id = 3
GROUP BY 1,
2
ORDER BY 2;
The problem with the above query is that it is giving me the same value for price for all the months. The requirement is that the price column be filled with nulls or zeros if no price data is available for a given month.
Put the generate_series() in the FROM clause. You are summarizing the data -- i.e. calculating the price over the entire range -- and then projecting this on all months. Instead:
SELECT gs.yyyymm,
coalesce(SUM(i.price), 0)
FROM generate_series('2019-03-01'::date, '2020-02-01', INTERVAL '1 MONTH'
) gs(yyyymm) LEFT JOIN
items i
ON gs.yyyymm = DATE_TRUNC('month', s.date_added) AND
i.item_type_id = 3
GROUP BY gs.yyyymm
ORDER BY gs.yyyymm;
You want generate_series in the FROM clause and join with it, somewhat like
SELECT months.m::date, ...
FROM generate_series(
start_month,
start_month + INTERVAL '11 months',
INTERVAL '1 month'
) AS months(m)
LEFT JOIN items
ON months.m::date = items.date_added

Current status based on week number ORACLE

I got this query on order to get all the days from the first day of the year (01/01/2018) to the end of next year (31/12/2019).
SELECT MYDATE,
TO_CHAR(NR_OF_SUNDAYS + 1,'FM09') WEEK_NUM,
FROM
(
SELECT MYDATE,
( (TRUNC(MYDATE,'DAY') - TRUNC(TRUNC(MYDATE,'YYYY'),'DAY')) / 7 ) +
CASE WHEN TO_CHAR(TRUNC(MYDATE,'YYYY'),'DAY') = 'SUN' THEN 1 ELSE 0 END AS NR_OF_SUNDAYS
FROM
( SELECT TRUNC (SYSDATE, 'YY') - 1 + LEVEL AS MYDATE
FROM DUAL
CONNECT BY LEVEL <= TRUNC (ADD_MONTHS (SYSDATE, 24), 'YY') -
TRUNC (SYSDATE, 'YY')
)
)
I need a column that specifies the following cases:
1) CASE WHEN MYDATE < TO_CHAR(SYSDATE, 'DD/MM/YYYY') THEN 'PAST DUE'
(this works its easy and no problem)
2) if my current =< mydate
week_num then 'CURRENT WEEK'(Excluding PAST DUE)
3) if my current week + one week then
'NEXT WEEK' (Excluding PAST DUE)
4) else FUTURE
Thanks a lot for your help.
So, in my answer I tried retain the logic behind your week number calculation.
However keep in mind that you could calculate week number using oracle to_char(date,'WW'), to_char(date,'IW'), to_char(date,'W') functions and then your life would be easier.
WW Week of year (1-53) where week 1 starts on the first day of the year and continues to the seventh day of the year.
W Week of month (1-5) where week 1 starts on the first day of the month and ends on the seventh.
IW Week of year (1-52 or 1-53) based on the ISO standard.
Having said all that here is my solution that uses only sql (note that defining and using a function would be a lot easier), based on your calculation method.
with date_table as (
SELECT MYDATE, to_number(TO_CHAR(NR_OF_SUNDAYS + 1,'FM09')) WEEK_NUM, to_number(to_char(MYDATE+1,'IW')) as nu
FROM
(
SELECT MYDATE,
( (TRUNC(MYDATE,'DAY') - TRUNC(TRUNC(MYDATE,'YYYY'),'DAY')) / 7 ) +
CASE WHEN TO_CHAR(TRUNC(MYDATE,'YYYY'),'DY', 'NLS_DATE_LANGUAGE = american') = 'SUN' THEN 1 ELSE 0 END AS NR_OF_SUNDAYS
FROM
( SELECT TRUNC (SYSDATE, 'YY') - 1 + LEVEL AS MYDATE
FROM DUAL
CONNECT BY LEVEL <= TRUNC (ADD_MONTHS (SYSDATE, 24), 'YY') -TRUNC (SYSDATE,'YY')
)
)
),
todays_week as
(
select distinct WEEK_NUM from date_table
where trunc(sysdate)=trunc(mydate)
),
pre_final as (
select MYDATE,WEEK_NUM, (select WEEK_NUM from todays_week) as todaysweek from date_table)
select MYDATE,sysdate,WEEK_NUM,todaysweek,
case when trunc(MYDATE) < trunc(sysdate) then 'PAST DUE'
when todaysweek = WEEK_NUM and abs(MYDATE-sysdate)<=7 then 'CURRENT WEEK'
when todaysweek +1 = WEEK_NUM and abs(MYDATE-sysdate)<=14 then 'Next Week'
else 'Future' end as description
from pre_final;
The main idea is to find today's week number and then use case when.
Here is my fiddle link with the results.
http://sqlfiddle.com/#!4/3149e4/148
EDIT 1:
Now, similar results one could achive with something like this:
select res.*,
case when trunc(MYDATE) < trunc(sysdate) then 'PAST DUE'
when todaysweek = WEEK_NUM and abs(MYDATE-sysdate)<=7 then 'CURRENT WEEK'
when todaysweek +1 = WEEK_NUM and abs(MYDATE-sysdate)<=14 then 'Next Week'
else 'Future' end as description
from (
SELECT MYDATE, to_number(to_char(MYDATE,'IW')) as WEEK_NUM,to_number(to_char(sysdate,'IW')) as todaysweek
FROM
( SELECT TRUNC (SYSDATE, 'YY') - 1 + LEVEL AS MYDATE
FROM DUAL
CONNECT BY LEVEL <= TRUNC (ADD_MONTHS (SYSDATE, 24), 'YY') -TRUNC (SYSDATE,'YY')
)) res

How to get the week start and end dates?

I have variable called WeekBeginDate and I want only to pull data for that week. For example, if the beginning of the week date is 07/21/2014 which is Monday in this case, then I want only to pull the data from 07/21/2014 to 7/27/2014.
The variable will always contain the date for the beginning of the week only but I don’t have the date for the end of the week.
The week begins on Monday and ends on Sunday. I can’t figure out how to calculate or sum the number of hours if I only have the date for the beginning of week.
SELECT DT, sum (TOT_HOURS)as TOT_HOURS FROM MYTABLE where DT >= #WeekBeginDate and <=#WeekEndDate group by DT
Note, that I only have the variable for the WeekBeginDate.
just modify your table columns in this CTE it may works :
;WITH workhours AS
(
SELECT DATEADD(DAY
, -(DATEPART(dw, DT) -1)
, DT) AS week_start
, DATEADD(DAY
, 7 - (DATEPART(dw, DT))
, DT) AS week_end
FROM MYTABLE
)
SELECT week_start
, week_end
, SUM(TOT_HOURS) total_hrs_per_week
FROM workhours
GROUP BY week_start
, week_end
You may need to add 6 days to the beginning of the week
and group by something else if you need total weekly hours, i'm calling it "id".
not by dt (or don't group at all if it is a total for the whole table):
SELECT id, DT, sum (TOT_HOURS)as TOT_HOURS FROM MYTABLE
where DT BETWEEN #WeekBeginDate and DATEADD(d,6,#WeekBeginDate)
GROUP BY id
This should be of some use to you. I am casting to date so the 24 hrs of day is considered.
DECLARE #WeekBeginDate DATETIME
SET #WeekBeginDate = '2014-07-28 12:08:31.633';
WITH MYTABLE (DT,TOT_HOURS)
AS (
SELECT '2014-06-27 00:08:31.633',5 UNION ALL
SELECT '2014-07-27 00:08:31.633',5 UNION ALL
SELECT '2014-07-28 00:08:31.633',1 UNION ALL
SELECT '2014-07-29 00:08:31.633',1 UNION ALL
SELECT '2014-07-30 00:08:31.633',1 UNION ALL
SELECT '2014-07-31 00:08:31.633',1 UNION ALL
SELECT '2014-08-01 00:08:31.633',1 UNION ALL
SELECT '2014-08-02 00:08:31.633',1 UNION ALL
SELECT '2014-08-03 00:08:31.633',1
)
SELECT CAST(#WeekBeginDate AS DATE) AS StartDate,
DATEADD(d, 6, CAST(#WeekBeginDate AS DATE)) AS EndDate,
SUM (TOT_HOURS)AS TOT_HOURS
FROM MYTABLE
WHERE CAST(DT AS DATE) BETWEEN CAST(#WeekBeginDate AS DATE) AND DATEADD(d, 6, CAST(#WeekBeginDate AS DATE))
Just add 6 (or 7) days...
SELECT DT, sum (TOT_HOURS)as TOT_HOURS FROM MYTABLE
where DT BETWEEN #WeekBeginDate and #WeekBeginDate + 6 group by DT
select #weekBeginDate = DATEADD(wk, DATEDIFF(wk,0,GETDATE()), 0)
select #WeekEndDate = DATEADD(dd, 6, DATEADD(wk, DATEDIFF(wk,0,GETDATE()), 0))
SELECT DT, sum (TOT_HOURS)as TOT_HOURS FROM MYTABLE where DT >= #WeekBeginDate and <=#WeekEndDate group by DT
Here is where having a calendar table would be very useful,
especially if your logic needs to change if Monday is a holiday.
Basically create a table with pre-calculated values for weeks and just join to it.
http://www.made2mentor.com/2011/04/calendar-tables-why-you-need-one

Grouping by year and month on date stored as a decimal

I have a data set with the following sample information:
ID DTE CNTR
1 20110102.0 2
1 20110204.0 3
1 20110103.0 5
2 20110205.0 6
2 20110301.0 7
2 20110302.0 3
If I want to group the information by month and sum the counter, the code I'm guessing would be this:
SELECT t.ID,
,SUM CASE(WHEN t.DTE between 20110101 and 20110131 then t.CNTR else 0) as Jan
,SUM CASE(WHEN t.DTE between 20110201 and 20110228 then t.CNTR else 0) as Feb
,SUM CASE(WHEN t.DTE between 20110301 and 20110331 then t.CNTR else 0) as Mar
FROM table t
GROUP BY t.ID
But, is there a way to aggregate that information into another two columns called "month" and "year" and group it that way, leaving me flexibility to preform select queries over many different time periods?
Edit, since your datatype is a decimal, you can use the following:
select ID,
left(dte, 4) year,
SUBSTRING(cast(dte as varchar(8)), 5, 2) mth,
sum(CNTR) Total
from yt
group by id, left(dte, 4), SUBSTRING(cast(dte as varchar(8)), 5, 2)
My suggestion would be to use the correct datatype for this which would be datetime. Then if you want the data for each month in columns, then you can use:
SELECT t.ID
, SUM(CASE WHEN t.DTE between '20110101' and '20110131' then t.CNTR else 0 end) as Jan
, SUM(CASE WHEN t.DTE between '20110201' and '20110228' then t.CNTR else 0 end) as Feb
, SUM(CASE WHEN t.DTE between '20110301' and '20110331' then t.CNTR else 0 end) as Mar
, year(dte)
FROM yt t
GROUP BY t.ID, year(dte);
This query includes a column to get the year for each DTE value.
If you want the result in multiple rows instead of columns, then you can use:
select ID,
YEAR(dte) year,
datename(m, dte) month,
sum(CNTR) Total
from yt
group by id, year(dte), datename(m, dte);
Note, this assumes that the DTE is a date datatype.
Another idea - using div/modulo operators:
select (dte / 10000) dte_year, ((dte % 10000) / 100) dte_month,sum(cntr)
from tablename
group by (dte / 10000) , ((dte % 10000) / 100)
select id,
datepart(yy, convert(datetime, dte, 112)),
datepart(mm, convert(datetime, dte, 112)),
sum(cntr)
from table
group by id,
datepart(yy, convert(datetime, dte, 112)),
datepart(mm, convert(datetime, dte, 112))

Split data between 2 dates in SQL

I have 2 date columns called Start_date and End_date in my table. I need to first find out how many weeks are in between those 2 dates and split the data.
--For e.g. if data is as given below,
ID Start_date End_date No_Of_Weeks
1 25-Apr-11 8-May-11 2
2 23-Apr-11 27-May-11 6
--I need the result like this:
ID Start_date End_date
1 25-Apr-2011 01-May-2011
1 02-May-2011 08-May-2011
2 23-Apr-2011 24-Apr-2011
2 25-Apr-2011 01-Apr-2011
2 02-May-2011 08-May-2011
2 09-May-2011 15-May-2011
2 16-May-2011 22-May-2011
2 23-May-2011 27-May-2011
Please help me out with the query. My week start date is Monday.
You can use a Calendar table defining then weeks and join it to your data.
I've created a sql fiddle for the following:
CREATE TABLE Calendar_Weeks (
week_start_date date,
week_end_date date )
CREATE TABLE Sample_Data (
id int,
start_date date,
end_date date )
INSERT Calendar_Weeks (week_start_date, week_end_date) VALUES ('2011-04-18','2011-04-24')
INSERT Calendar_Weeks (week_start_date, week_end_date) VALUES ('2011-04-25','2011-05-01')
INSERT Calendar_Weeks (week_start_date, week_end_date) VALUES ('2011-05-02','2011-05-08')
INSERT Calendar_Weeks (week_start_date, week_end_date) VALUES ('2011-05-09','2011-05-15')
INSERT Calendar_Weeks (week_start_date, week_end_date) VALUES ('2011-05-16','2011-05-22')
INSERT Calendar_Weeks (week_start_date, week_end_date) VALUES ('2011-05-23','2011-05-29')
INSERT Sample_Data (id, start_date, end_date) VALUES (1, '2011-04-25','2011-05-08')
INSERT Sample_Data (id, start_date, end_date) VALUES (2, '2011-04-23','2011-05-27')
SELECT id, week_start_date, week_end_date
FROM Sample_Data CROSS JOIN Calendar_Weeks
WHERE week_start_date BETWEEN start_date AND end_date
UNION
SELECT id, week_start_date, week_end_date
FROM Sample_Data CROSS JOIN Calendar_Weeks
WHERE week_end_date BETWEEN start_date AND end_date
I have to admit the UNION of the queries feels a bit of a hack to include rows at the start or end of the set, so you might prefer to use Ravi Singh's solution.
You can also use INNER JOIN if you like:
SELECT id, week_start_date, week_end_date
FROM Sample_Data INNER JOIN Calendar_Weeks
ON week_start_date BETWEEN start_date AND end_date
UNION
SELECT id, week_start_date, week_end_date
FROM Sample_Data INNER JOIN Calendar_Weeks
ON week_end_date BETWEEN start_date AND end_date
As per the last understanding, this will work :
with demo_cte as
(select id,
start_date,
dateadd(day,6,DATEADD(wk, DATEDIFF(wk,0,start_date), 0)) end_date,
end_date last_end_date,
no_of_weeks no_of_weeks from demo
union all
select id,dateadd(day,1,end_date),
dateadd(day,7,end_date),
last_end_date
,no_of_weeks-1 from demo_cte
where no_of_weeks-1>0)
select id, start_date,
case
when end_date<=last_end_date then end_date
else
last_end_date
end
end_date
from demo_cte order by id,no_of_weeks desc
SQL Fiddle
And if number of weeks is not available use this :
with demo_cte as
(select id,
start_date,
dateadd(day,6,DATEADD(wk, DATEDIFF(wk,0,start_date), 0)) end_date,
end_date last_end_date
--,no_of_weeks no_of_weeks
from demo
union all
select id,dateadd(day,1,end_date),
dateadd(day,7,end_date),
last_end_date
--,no_of_weeks-1
from demo_cte
where --no_of_weeks-1>0
dateadd(day,7,end_date)<=last_end_date
)
select id, start_date,
case
when end_date<=last_end_date then end_date
else
last_end_date
end
end_date
from demo_cte order by id,start_date
--,no_of_weeks desc
setting ambient test
declare #dt table (ID int,Start_date datetime,
End_date datetime,No_Of_Weeks int)
insert into #dt (ID,Start_date,End_date,No_Of_Weeks)
select 1, '25-Apr-11', '8-May-11', 2
union all
select 2, '23-Apr-11' , '27-MAy-11' , 6;
try this...
with cte as (select d.ID
,d.Start_date
,(select MIN([end]) from (values(d.End_date),(DATEADD(day,-1,DATEADD(week,DATEDIFF(week,0,d.Start_date)+1,0))))V([end])) as End_date
,d.End_date as end_of_period
from #dt d
union all select d.ID
,DATEADD(day,1,d.End_date) as Start_date
, case when d.end_of_period < DATEADD(week,1,d.End_date) then d.end_of_period else DATEADD(week,1,d.End_date) end as End_date
,d.end_of_period as end_of_period
from cte d
where end_of_period <> End_date
)
select ID
,cast(Start_date as DATE) Start_date
,cast(End_date as date) End_date
from cte
order by cte.ID,cte.Start_date
option(maxrecursion 0)
the resultset achieved...
ID Start_date End_date
1 2011-04-25 2011-05-01
1 2011-05-02 2011-05-08
2 2011-04-23 2011-04-24
2 2011-04-30 2011-05-01
2 2011-05-07 2011-05-08
2 2011-05-14 2011-05-15
2 2011-05-21 2011-05-22
2 2011-05-28 2011-05-27
try this query, hope it will work
If your week starts on Sunday, use below
set datefirst 7
declare #FromDate datetime = '20130110'
declare #ToDate datetime = '20130206'
select datepart(week, #ToDate) - datepart(week, #FromDate) + 1
If your week starts on Monday, use below
set datefirst 1
declare #FromDate datetime = '20100201'
declare #ToDate datetime = '20100228'
select datepart(week, #ToDate) - datepart(week, #FromDate) + 1
note: both query will yield to diffrent results, since their starting dates differ.
You should look at using the DATEDIFF function.
I'm not sure what you're asking for in the second part of your question, but once you've got the difference between the dates, you may want to have a look at using CASE on the result of your DATEDIFF.
You should use a date tally table similar to the type that Jeff Moden suggests in The "Numbers" or "Tally" Table: What it is and how it replaces a loop (required login).
A Tally table is nothing more than a table with a single column of very well indexed sequential numbers starting at 0 or 1 (mine start at 1) and going up to some number. The largest number in the Tally table should not be just some arbitrary choice. It should be based on what you think you'll use it for. I split VARCHAR(8000)'s with mine, so it has to be at least 8000 numbers. Since I occasionally need to generate 30 years of dates, I keep most of my production Tally tables at 11,000 or more which is more than 365.25 days times 30 years.
I started with Tony's SQL Fiddler but implemented a DateInformation table to be a little more generic. This could be something that you could reuse.
--Build Test Data, For production set the date
--range large enough to handle all cases.
CREATE TABLE DateInformation (
[Date] date,
WeekDayNumber int,
)
--From Tony
CREATE TABLE Sample_Data (
id int,
start_date date,
end_date date )
DECLARE #CurrentDate Date = '2010-12-27'
While #CurrentDate < '2014-12-31'
BEGIN
INSERT DateInformation VALUES (#CurrentDate,DatePart(dw,#CurrentDate))
SET #CurrentDate = DATEADD(DAY,1,#CurrentDate)
END
--From Tony
INSERT Sample_Data VALUES (1, '2011-04-25','2011-05-08')
INSERT Sample_Data VALUES (2, '2011-04-23','2011-05-27')
Here's the solution using CTE to join the sample data to the DateInformation table.
--Solution using CTE
with Week (WeekStart,WeekEnd) as
(
select d.Date
,dateadd(day,6,d.date) as WeekEnd
from DateInformation d
where d.WeekDayNumber = 2
)
select
s.ID
,case when s.Start_date > w.WeekStart then s.Start_Date
else w.WeekStart end as Start_Date
,case when s.End_Date < w.WeekEnd then s.End_Date
else w.WeekEnd end as End_Date
from Sample_Data s
join Week w on w.WeekStart > dateadd(day,-6,s.start_date)
and w.WeekEnd <= dateadd(day,6,s.end_date);
See solution in SQL Fiddle
set datefirst 1
GO
with cte as (
select ID, Start_date, End_date, Start_date as Week_Start_Date, (case when datepart(weekday, Start_date) = 7 then Start_Date else cast(null as datetime) end) as Week_End_Date, datepart(weekday, Start_date) as start_weekday, cast(0 as int) as week_id
from (
values (1, cast('25-Apr-2011' as datetime), cast('8-May-2011' as datetime)),
(2, cast('23-Apr-2011' as datetime), cast('27-May-2011' as datetime))
) t(ID, Start_date, End_date)
union all
select ID, Start_date, End_date, dateadd(day, 1, Week_Start_date) as Week_Start_Date, (case when start_weekday + 1 = 7 then dateadd(day, 1, Week_Start_date) else null end) as Week_End_date, (case when start_weekday = 7 then 1 else start_weekday + 1 end) as start_weekday, (case when start_weekday = 7 then week_id + 1 else week_id end) as week_id
from cte
where Week_Start_Date != End_date
)
select ID, min(Week_Start_Date), isnull(max(Week_End_Date), max(End_Date))
from cte
group by ID, Week_id
order by ID, 2
option (maxrecursion 0)
If you wanted to get the number of weeks, you could change the select after the cte to be:
select ID, Start_date, End_date, count(distinct week_id) as Number_Of_Weeks
from cte
group by ID, Start_date, End_date
option (maxrecursion 0)
Obviously to change the data used, the anchor (first part) of the cte where values() is being used would be changed.
This uses Monday as the first day of the week. To us a different day, change the set datefirst statement at the top - -- http://msdn.microsoft.com/en-gb/library/ms181598.aspx
Enjoy!
WITH D AS (
SELECT id
, start_date
, end_date
, start_date AS WEEK_START
, start_date + 7 - DATEPART(weekday,start_date) + 1
AS week_end
FROM DATA
), W AS (
SELECT id
, start_date
, end_date
, WEEK_START
, WEEK_END
FROM D
UNION ALL
SELECT id
, start_date
, end_date
, WEEK_END + 1 AS WEEK_START
, WEEK_END + 7 AS WEEK_END
FROM W
WHERE WEEK_END < END_DATE
)
SELECT ID
, WEEK_START AS START_DATE
, WEEK_END AS END_DATE
FROM W
ORDER BY 1, 2;
Here's a solution that uses the datepart function to account for the fact that your weeks start on Mondays:
with demo_normalized as
(
select id,
start_date,
(datepart(dw,start_date) + 5) % 7 as test,
dateadd(d,
0 - ((datepart(dw,start_date) + 5) % 7),
start_date
) as start_date_firstofweek,
dateadd(d,
6 - ((datepart(dw,start_date) + 5) % 7),
start_date
) as start_date_lastofweek,
end_date,
dateadd(d,
0 - ((datepart(dw,end_date) + 5) % 7),
end_date
) as end_date_firstofweek,
dateadd(d,
6 - ((datepart(dw,end_date) + 5) % 7),
end_date
) as end_date_lastofweek,
datediff(week,
dateadd(d,
0 - ((datepart(dw,start_date) + 5) % 7),
start_date
),
dateadd(d,
6 - ((datepart(dw,end_date) + 5) % 7),
end_date
)
) as no_of_weeks
from demo
),
demo_cte as
(
select
id,
dateadd(day,7,start_date_firstofweek) as start_date,
dateadd(day,7,start_date_lastofweek) as end_date,
end_date_firstofweek,
no_of_weeks
from demo_normalized
where no_of_weeks >= 3
UNION ALL select
id,
dateadd(day,7,start_date) as start_date,
dateadd(day,7,end_date) as end_date,
end_date_firstofweek,
no_of_weeks
from demo_cte
where
(dateadd(day,8,start_date) < end_date_firstofweek)
),
demo_union as
(
select id, start_date, end_date, no_of_weeks from demo_normalized where no_of_weeks = 1
union all
select id, start_date, start_date_lastofweek as end_date, no_of_weeks
from demo_normalized where no_of_weeks >= 2
union all
select id, start_date, end_date, no_of_weeks from demo_cte
union all
select id, end_date_firstofweek as start_date, end_date, no_of_weeks
from demo_normalized where no_of_weeks >= 2
)
select
d0.id,
d0.no_of_weeks,
convert(varchar, d0.start_date, 106) as start_date,
convert(varchar, d0.end_date, 106) as end_date
from demo_union d0
order by d0.id, d0.start_date
EDIT (added CTE for the weeks in between):
Here is the link to sqlfiddle.
Note: This solution requires no additional DDL - no additional entities have to be created and maintained. In short, it doesn't reinvent the Calendar.
All the calendar logic is contained in the query.