Determine which of four date ranges a single date range overlaps - sql-server

I have Subscriptions that can last anywhere (as far as my output should be concerned) from 1 to 4 fiscal quarters (could also be seen as four date ranges)
I'm using Arizona Fiscal Quarters:
Q1: Jul 1st - Sep 30th
Q2: Oct 1st - Dec 31st
Q3: Jan 1st - Mar 31st
Q4: Apr 1st - Jun 30th
What I need to figure out is how many subscriptions are active in each quarter based on the subscriptions start and end dates.
For example, using (YYYY-MM-DD), I have:
A subscription that starts on 2016-07-06 and ends on 2017-02-22 I should be able to see I have a subscription active in Q1, Q2, and Q3.
Another subscription that starts on 2016-10-18 and ends on 2016-10-24 would only be seen as active for Q2
Finally, a subscription that starts on 2016-09-28 but has no end date would be seen as active for Q1, Q2, Q3, and Q4 (so whatever starting quarter all the way to Q4)
Below is my current SQL Server Script, and here it is on SQL Fiddle:
WITH SubscriptionInfo AS
(
SELECT
[Subscriptions].[Customer_Id]
,[DistributorTypes].[Name] AS [Distributor Type]
,[Customers].Zip_Id
,[Subscriptions].[UnsubscribeReason_Id]
,[Subscriptions].[Id] AS [Subscription ID]
,CONVERT(DATE, [Subscriptions].[StartDate]) AS [Subscription Start Date]
,CONVERT(DATE, [Subscriptions].[EndDate]) AS [Subscription End Date]
,[PriorityLevels].PriorityLevel AS [Priority Level]
,CONVERT(DATE, [SubscriptionPriorityLevels].StartDate) AS [Priority Level Start Date]
,CONVERT(DATE, [SubscriptionPriorityLevels].EndDate) AS [Priority Level End Date]
,[FundingSources].[Name] AS [Funding Source]
,CONVERT(DATE, [SubscriptionFundingSources].StartDate) AS [SubscriptionFundingSources Start Date]
,CONVERT(DATE, [SubscriptionFundingSources].EndDate) AS [SubscriptionFundingSources End Date]
FROM [Subscriptions]
LEFT JOIN [SubscriptionPriorityLevels]
ON [SubscriptionPriorityLevels].Subscription_Id = Subscriptions.Id
LEFT JOIN [PriorityLevels]
ON [PriorityLevels].Id = SubscriptionPriorityLevels.PriorityLevel_Id
LEFT JOIN [SubscriptionFundingSources]
ON [SubscriptionFundingSources].Subscription_Id = Subscriptions.Id
LEFT JOIN [FundingSources]
ON [FundingSources].Id = [SubscriptionFundingSources].FundingSource_Id
LEFT JOIN [Customers]
ON [Customers].Id = [Subscriptions].Customer_Id
LEFT JOIN [DistributorTypes]
ON [DistributorTypes].Id = Customers.DistributorType_Id
WHERE
([Subscriptions].StartDate >= '2016-07-01') -- Dummy dates, would later be parameters
AND ([Subscriptions].EndDate <= '2017-06-30'
OR [Subscriptions].EndDate IS NULL)
AND ([PriorityLevels].PriorityLevel IN (2, 3)) -- Only care about these two levels
AND ([Customers].DistributorType_Id = 1) -- Distributor Type: Number One Distrubition
AND ([SubscriptionFundingSources].FundingSource_Id = 2) -- Funding Source: First Bank
)
SELECT
[SubscriptionInfo].Customer_Id
,[SubscriptionInfo].[Subscription ID]
,MAX([SubscriptionInfo].[Priority Level]) AS [Highest Priority Level]
,CASE -- Determine which fiscal quarter each Subscription Start Date belongs to
WHEN MONTH([SubscriptionInfo].[Subscription Start Date]) IN (7, 8, 9) THEN 1 -- July, August, September
WHEN MONTH([SubscriptionInfo].[Subscription Start Date]) IN (10, 11, 12) THEN 2 -- October, November, December
WHEN MONTH([SubscriptionInfo].[Subscription Start Date]) IN (1, 2, 3) THEN 3 -- January, Feburary, March
ELSE 4 -- April, May, June
END AS [Fiscal Quarter Start Date]
,CASE -- Determine which fiscal quarter each Subscription Start Date belongs to
WHEN MONTH([SubscriptionInfo].[Subscription End Date]) IN (7, 8, 9) THEN 1 -- July, August, September
WHEN MONTH([SubscriptionInfo].[Subscription End Date]) IN (10, 11, 12) THEN 2 -- October, November, December
WHEN MONTH([SubscriptionInfo].[Subscription End Date]) IN (1, 2, 3) THEN 3 -- January, Feburary, March
ELSE 4 -- April, May, June
END AS [Fiscal Quarter End Date]
FROM [SubscriptionInfo]
GROUP BY
[SubscriptionInfo].Customer_Id
,[SubscriptionInfo].[Subscription ID]
,CASE -- Group Subscription Start Date's into Fiscal Quarters
WHEN MONTH([SubscriptionInfo].[Subscription Start Date]) IN (7, 8, 9) THEN 1 -- July, August, September
WHEN MONTH([SubscriptionInfo].[Subscription Start Date]) IN (10, 11, 12) THEN 2 -- October, November, December
WHEN MONTH([SubscriptionInfo].[Subscription Start Date]) IN (1, 2, 3) THEN 3 -- January, Feburary, March
ELSE 4 -- April, May, June
END
,CASE -- Group Subscription End Date's into Fiscal Quarters
WHEN MONTH([SubscriptionInfo].[Subscription End Date]) IN (7, 8, 9) THEN 1 -- July, August, September
WHEN MONTH([SubscriptionInfo].[Subscription End Date]) IN (10, 11, 12) THEN 2 -- October, November, December
WHEN MONTH([SubscriptionInfo].[Subscription End Date]) IN (1, 2, 3) THEN 3 -- January, Feburary, March
ELSE 4 -- April, May, June
END
ORDER BY
[SubscriptionInfo].Customer_Id
So far I'm able to identify in what fiscal quarter a subscription started and what fiscal quarter it ended.
I want to be able to count how many subscriptions were active in each quarter.
Desired Output:
| FirstQuarter | SecondQuarter | ThirdQuarter | FourthQuarter |
|--------------|---------------|--------------|---------------|
| 2 | 1 | 3 | 3 |

Once you have a table describing the quarters' start and end dates as #Sean Lange suggested, you just need to compare your subscriptions' start and end date to the quarters' start and end date and then count them up where they overlap. Basically, two ranges have some overlap if A.Start < B.End and A.End > B.Start.
Here's a complete example of the pattern.
-------------------------
-- construct the quarters table (taken from the SQL Fiddle in comments above)
DECLARE #FiscalDate Table
(
Id INT NOT NULL,
FiscalYear INT NOT NULL,
FiscalQuarter INT NOT NULL,
QuarterStartDate DATE NOT NULL,
QuarterEndDate DATE NOT NULL
PRIMARY KEY(Id)
);
INSERT INTO #FiscalDate
(Id, FiscalYear, FiscalQuarter, QuarterStartDate, QuarterEndDate)
VALUES
( 1, 2016, 1, '2016-07-01', '2016-09-30')
,(2, 2016, 2, '2016-10-01', '2016-12-31')
,(3, 2017, 3, '2017-01-01', '2017-03-31')
,(4, 2017, 4, '2017-04-01', '2017-06-30')
;
-------------------------
--Get some random test data to imitate subscriptions ranges
DECLARE #tempSet Table(a date, b date)
INSERT INTO #tempSet SELECT TOP 15
dateadd(dd, ROUND(365 * RAND(convert(varbinary, newid())), 0), '2016-07-01') as a
, dateadd(dd, ROUND(365 * RAND(convert(varbinary, newid())), 0), '2016-07-01') as b
FROM sysobjects
-------------------------
-- Fix our random data a little (start date needs to be before end date)
DECLARE #DateRanges Table(StartDate date, EndDate date)
INSERT INTO #DateRanges
SELECT a, b FROM #tempset WHERE a <= b UNION SELECT b, a FROM #tempset WHERE b < a
-------------------------
-- Show our Date ranges in a useful order for review
SELECT * FROM #DateRanges ORDER BY StartDate, EndDate
-------------------------
-- Show our by-quarter counts.
SELECT
fd.FiscalQuarter
, count(*) as ActiveSubsCount
FROM
#FiscalDate fd
JOIN
#DateRanges dr
on fd.QuarterStartDate < dr.EndDate
and fd.QuarterEndDate >= dr.StartDate
GROUP BY
fd.FiscalQuarter
-------------------------
-- and in your desired output (without your quarters table)
SELECT
COUNT(CASE WHEN '2016-07-01' < dr.EndDate AND '2016-09-30' >= dr.StartDate THEN 1 ELSE NULL END) AS FirstQuarter
, COUNT(CASE WHEN '2016-10-01' < dr.EndDate AND '2016-12-31' >= dr.StartDate THEN 1 ELSE NULL END) AS SecondQuarter
, COUNT(CASE WHEN '2017-01-01' < dr.EndDate AND '2017-03-31' >= dr.StartDate THEN 1 ELSE NULL END) AS ThirdQuarter
, COUNT(CASE WHEN '2017-04-01' < dr.EndDate AND '2017-06-30' >= dr.StartDate THEN 1 ELSE NULL END) AS FourthQuarter
FROM
#DateRanges dr

Related

Extract short date from a set of conditions in SQL Server

s_term
w_due
2
2
4
2
Given a table as shown above, I want to extract the corresponding date for the month of August/September (dd-mm-yyyy) as follows:
<s_term, w_due>
(2,2) means a service started on 2nd Monday of August and I need to return the date as the 4th Monday of August as indicated by w_due which indicates the stepsize from the starting point.
Similarly for (4, 2): I want to return the 2nd Monday in September as the service start date is on 4th Monday of August with a w_due of 2.
A Calendar Table would be well worth the effort.
Here is an option where we use an ad-hoc tally table to create an ad-hoc calendar table.
Example
Declare #BaseDate date = '2021-08-01'
;with cte as (
Select D
,WN = row_number() over (partition by month(d) order by D)
From ( Select Top 90 D=DateAdd(DAY,-1+Row_Number() Over (Order By (Select Null)),#BaseDate) From master..spt_values n1 ) D
Where datename(WEEKDAY,D)='Monday'
)
Select A.*
,Date1 = B.D
,Date2 = C.D
From YourTable A
Join cte B on s_term=B.WN and month(B.D)=month(#BaseDate)
Join cte C on w_due =C.WN and month(C.D)=month(dateadd(month,1,#BaseDate))
Results
s_term w_due Date1 Date2
2 2 2021-08-09 2021-09-13
4 2 2021-08-23 2021-09-13
DECLARE #referenceMonth DATE = '2021-08-01';
DECLARE #tbl TABLE (s_term int, w_due int);
INSERT INTO #tbl VALUES (2, 2), (4, 2);
SELECT a.first_monday, s_term, b.s_term_monday, w_due, c.w_due_monday
FROM #tbl
CROSS APPLY (
SELECT DATEADD(d, (9 - DATEPART(dw, #referenceMonth)) % 7, #referenceMonth) as first_monday
) a
CROSS APPLY
(
SELECT DATEADD(WEEK, s_term - 1, a.first_monday) as s_term_monday
) b
CROSS APPLY (
SELECT DATEADD(WEEK, w_due, b.s_term_monday) as w_due_monday
) c;
The first part (a) just gets the first monday of the reference month.
The second part (b) calculates the Nth monday of the reference month (s_term).
The last part (c) calculates de Nth monday from that s_term monday (w_due)
The result is as follows:
As you specified august/2021 as the reference date, I just hard-coded it as a parameter.
I also split the DATE opperations in multiple CROSS APPLY for ease of read, but you could nest the DATEADD inside one another right in the SELECT. Something like this:
SELECT s_term, w_due, DATEADD(WEEK, w_due,
DATEADD(WEEK, s_term - 1,
DATEADD(d, (9 - DATEPART(dw, #referenceMonth)) % 7, #referenceMonth)))
FROM #tbl
Edit: Explaning that misterious logic of DATEADD(d, (9 - DATEPART(dw, #referenceMonth)) % 7, #referenceMonth)
We take the weekday of the first day of that month: DATEPART(dw, #referenceMonth)
Sunday is 1, Monday is 2 and so on...
Now we find out how many days it would need to get from our reference week day to the desired week day (Monday). We do that with that formulae: (9 - X) % 7
That 9 is the number of days minus our WeekDay that would equal to 7 (the number of days in a week). 7 (days of the week) + 2 (monday) = 9.
If you where to find, say, the first Thursday of the week, that 9 would become 13. 7 (days of the week) + 5 (thursday) = 13.
When we take the modulus of 7 we are looking at the number of week days we are away from our desired week day. Let's run a few simulations:
refenceDate is a Monday (2): 9 - 2 % 7 = 0
nothing to do!
refenceDate is a Sunday (1): 9 - 1 % 7 = 1
we are 1 day away from the next monday
refenceDate is a Thursday (5): 9 - 5 % 7 = 4
we are 4 days away from the next monday
Now we finish by adding that number of days to our reference date: DATEADD(d, y, #referenceMonth)
With that, the first day of the month becomes the first Monday of said month.
maybe ?
CREATE TABLE #temptable(
s_term INTEGER
,w_due INTEGER
);
INSERT INTO #temptable(s_term,w_due) VALUES (2,2) ,(4,2);
WITH Mondays AS (
SELECT ROW_NUMBER () OVER (ORDER BY (SELECT NULL)) RN ,
DATEADD(DAY, n, M_fom) AS Mday
FROM (
SELECT fom, DATEADD(DAY, DATEDIFF(DAY, 0, fom) / 7 * 7, 0)
FROM ( VALUES ( DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()), 0)) ) d ( fom )
) e ( fom, M_fom )
CROSS JOIN ( VALUES ( 7), ( 14), ( 21), ( 28), ( 35),(42),(49),(56),(63) ) f ( n )
)
SELECT m1.Mday AS M1, m2.Mday AS M2
FROM #temptable AS t INNER JOIN
Mondays AS m1 ON t.s_term = m1.RN INNER JOIN
Mondays AS m2 ON t.s_term + t.w_due = m2.RN

Need to save ISO_WEEK and YEAR

I need to split and save ISO_WEEK and YEAR between two dates in a table.
To get that data I use below query.
DECLARE #_start DATE = '2019-01-01'
DECLARE #_end DATE = '2019-12-31'
SELECT DATEPART(YEAR,dateadd(week, number, #_start)) AS YEAR
,DATEPART(ISO_WEEK,dateadd(week, number, #_start)) AS CW
FROM (SELECT x.number FROM master..spt_values x WHERE type = 'P'
AND #_end >= dateadd(week, number, #_start)) date_part
This logic fails on last week of the year where I get CW as 1 and Year as 2019. But actually, CW is 1 and YEAR is 2020.
YEAR CW
----------- -----------
2019 1
2019 2
. .
. .
2019 52
2019 1
Any suggestions to handle this?
Try this
DECLARE #_start DATE = '2019-01-01'
DECLARE #_end DATE = '2019-12-31'
SELECT DATEPART(year, s.Sunday) AS YEAR
, DATEPART(iso_week, s.Sunday) AS CW
FROM (
SELECT DATEADD(week, x.number, DATEADD(day, 7 - (DATEPART(dw, #_start) + ##DATEFIRST - 1) % 7, #_start)) AS Sunday
FROM master..spt_values x
WHERE type = 'P'
) s
WHERE #_end >= s.Sunday
This first offsets #_start to following Sunday and then loop all Sundays from then on.
The idea is that DATEPART(YEAR, ...) for Sundays returns less surprising year for DATEPART(ISO_WEEK, ...)

Conditional scoring

I have a table that shows only the 'captured' data. For example in the below exhibit, the emp_no 17 has 2 records - for November and February (for a specified 6 month period, from July 2017). It does not have data for the other 4 months (within the 6-month date range, from previous 6 months to current date).
How can I populate these missing months (Sept, Oct, Dec) with default values for num_differences of 0 for the missing months? (for example, in this case, I want emp_no 17 to have the below (I can ignore 2018 data - only require data up to Dec 2017):
I have the script below:
declare #YMN date;
set #YMN = '20171201';
DECLARE #Emp TABLE (
[date] date,
[emp_no] int,
[num_differences] int
);
INSERT INTO #Emp VALUES
('2017-09-14', 17, 1), ('2017-12-01', 17, 1),('2017-12-18', 17, 1),('2017-12-21', 17, 1),
('2017-09-27', 17, 1), ('2017-12-04', 17, 1);
-------------------------------------------------------------------------------------------get missing dates---------------------------------------------------------------------------
;WITH cte_Emp_No AS (
SELECT DISTINCT [emp_no]
FROM #Emp
),
cte_dates AS (
SELECT [emp_no], DATEADD(month, -6, DATEADD(dd, -(DAY(dateadd(month, 1, #YMN)) - 1), dateadd(month, 1, #YMN))) AS [date]
FROM cte_Emp_No
UNION ALL
SELECT [emp_no], DATEADD(month, 1, [date]) AS [date]
FROM cte_dates
WHERE [date] < dateadd(month, 0, #YMN)
)
SELECT DISTINCT ISNULL(e.emp_no, c.emp_no) emp_no, ISNULL(e.date, c.date) date, ISNULL(e.num_differences, 0) num_differences
into ##new_table
FROM #Emp AS e
RIGHT JOIN cte_dates AS c ON YEAR(c.date) = YEAR(e.date) AND MONTH(c.date) = MONTH(e.date)
-----------------------------------------------------------------------------------------------MAIN CTE------------------------------------------------------------------------------
;with cte_RawScore as
(
select emp_no
, date YMN
,sum(case when datediff(month, convert(datetime, #YMN, 112), date) = 0 then num_differences else 0 end) as thismonth
,sum(case when datediff(month, convert(datetime, #YMN, 112), date) between -2 and 0 then num_differences else 0 end) as last3month
,sum(case when datediff(month, convert(datetime, #YMN, 112), date) between -5 and 0 then num_differences else 0 end) as last6month
from ##new_table d
group by emp_no, date
)
select
emp_no
,YMN
,case when last6month = 0 then 5
when last3month = 0 then 4
when thismonth = 0 then 3
when thismonth <= 3 then 2
else 1 end RawScore
from cte_RawScore
ORDER BY day(YMN) desc
drop table ##new_table
I want this the scoring only to be applicable for 6 months from and after July 2017. i.e. the #YMN is a variable that stores the year month number; and the score, according to the above rule applies to the 6 months from 201707.
So 201707 is 1 month,
201708 is 2 months, etc, up to 201712
I wish to have a list of employees with their associated scores, based on the rules mentioned below .
That’s, :
A score of 5 if 0 differences in 6 consecutive months ( from July to December) ;
A score of 4 if 0 differences in 3 consecutive months (from July to December);
A score of 3 if 0 differences for 1 month ( from July to December);
A score of 2 if 1 to 3 differences for 1 month (from July to December);
A score of 1 if 4 or more differences in 1 month (from July to December).
I get the number of differences from a table, but some employees do not appear for certain months; hence I want to give them a difference of 0 if they do not appear for that particular month.
Please assist.
I think I understand what you're getting at. Let me give you a simplified example. You need a table full of dates to join to. In data warehousing we use a Date dimension which has attributes about every date.
For your example your date dimension table could just have Month names or numbers:
1
2
...
12
Let's call this table Months.
Then you would do something like this, to count a zero for months with no data. Here I'm using what's called a Common Table Expression or CTE (the part with the WITH) in place of a table, since I'm not concerned with creating a permanent table right now.
WITH Months AS (
SELECT 1 AS MonthNumber UNION
SELECT 2 UNION
SELECT 3 UNION
SELECT 4 UNION
SELECT 5 UNION
SELECT 6 UNION
SELECT 7 UNION
SELECT 8 UNION
SELECT 9 UNION
SELECT 10 UNION
SELECT 11 UNION
SELECT 12
)
SELECT M.MonthNumber, COUNT(*)
FROM Months as M
LEFT JOIN MyData as D
ON MONTH(D.SomeDateValue) = M.MonthNumber
GROUP BY M.MonthNumber
This will guarantee every month appears with a count, perhaps of zero.

Identify All Holiday Dates By Year using TSQL

I did a lot of searching for an easy solution to dynamically identify U.S. federal holidays by year. I wasn't able to find much information for the trickier holidays. Holidays like New Year's Day or Independence Day are easy to program as they are static. However, some are more difficult to identify programmatically such as Presidents' Day (3rd Monday in February) or Thanksgiving (4th Thursday in November).
I know this is an old question, but we have a sweet Scalar-valued Function that returns 1 if it's a holiday and 0 if it is not. We do have to manually add Observed Dates - We use the https://www.timeanddate.com/holidays/us/ to get the observed dates.
First, create the function:
USE [DATABASE]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[IsHoliday](#date as DATETIME)
RETURNS bit
AS
BEGIN
if #date is not null
begin
-- JAN
IF Month(#date)=1 AND day(#date)=1 return 1 -- New Years Day
IF Month(#date)=1 AND DATEPART(weekday, #date)=2 and day(#date)>14 and day(#date)<=21 return 1 -- Martin Luther King, Jr. Day
-- FEB
IF Month(#date)=2 AND DATEPART(weekday, #date)=1 and day(#date)<=7 return 1 -- Super Bowl Sunday
IF Month(#date)=2 AND day(#date)=14 return 1 -- Valentine's Day
IF Month(#date)=2 AND DATEPART(weekday, #date)=2 and day(#date)>14 and day(#date)<=21 return 1 -- Presidents' Day
-- MAR
IF Month(#date)=3 AND day(#date)=17 return 1 -- St Patrick's Day
-- MAY
IF Month(#date)=5 AND DATEPART(weekday, #date)=1 and day(#date)>7 and day(#date)<=14 return 1 -- Mother's day
IF Month(#date)=5 AND DATEPART(weekday, #date)=2 and day(#date)>24 and day(#date)<=31 return 1 --Memorial Day
-- JUN
IF Month(#date)=6 AND day(#date)=19 return 1 -- Juneteenth
IF Month(#date)=6 AND DATEPART(weekday, #date)=2 and day(#date)>14 and day(#date)<=21 return 1 --Father's Day
-- JUL
IF Month(#date)=7 AND day(#date)=4 return 1 -- July 4th
-- SEP
IF Month(#date)=9 AND DATEPART(weekday, #date)=2 and day(#date)<=7 return 1--Labor Day
-- OCT
IF Month(#date)=10 AND DATEPART(weekday, #date)=2 and day(#date)>7 and day(#date)<=14 return 1 --Columbus Day
IF Month(#date)=10 AND day(#date)=31 return 1 -- Halloween
-- NOV
IF Month(#date)=11 AND day(#date)=11 return 1 -- Veteran's Day
IF Month(#date)=11 AND DATEPART(weekday, #date)=5 and day(#date)>21 and day(#date)<=28 return 1 --Thanksgiving
-- DEC
IF Month(#date)=12 AND day(#date)=24 return 1 -- Christmas Eve
IF Month(#date)=12 AND day(#date)=25 return 1 -- Christmas Day
IF Month(#date)=12 AND day(#date)=31 return 1 -- NYE
-- Observed Dates
if month(#date)=1 AND day(#date)=2 AND year(#date)=2017 return 1 -- New Years Day observed for 2017
if month(#date)=11 AND day(#date)=10 AND year(#date)=2017 return 1 -- Veteran's Day observed for 2017
if month(#date)=11 AND day(#date)=12 AND year(#date)=2018 return 1 -- Veteran's Day observed for 2018
if month(#date)=7 AND day(#date)=3 AND year(#date)=2020 return 1 -- 4th of July Observed for 2021
if month(#date)=6 AND day(#date)=18 AND year(#date)=2021 return 1 -- Juneteenth observed for 2021
if month(#date)=7 AND day(#date)=5 AND year(#date)=2021 return 1 -- 4th of July Observed for 2021
if month(#date)=6 AND day(#date)=20 AND year(#date)=2022 return 1 -- Juneteenth observed for 2022
if month(#date)=12 AND day(#date)=26 AND year(#date)=2022 return 1 -- Christmas Day observed for 2022
if month(#date)=1 AND day(#date)=2 AND year(#date)=2023 return 1 -- New Years Day observed for 2023
if month(#date)=11 AND day(#date)=10 AND year(#date)=2023 return 1 -- Veteran's Day observed for 2023
if month(#date)=7 AND day(#date)=3 AND year(#date)=2026 return 1 -- 4th of July Observed for 2026
if month(#date)=6 AND day(#date)=18 AND year(#date)=2027 return 1 -- Juneteenth observed for 2027
if month(#date)=7 AND day(#date)=5 AND year(#date)=2027 return 1 -- 4th of July Observed for 2027
if month(#date)=11 AND day(#date)=10 AND year(#date)=2028 return 1 -- Veteran's Day observed for 2028
if month(#date)=11 AND day(#date)=12 AND year(#date)=2029 return 1 -- Veteran's Day observed for 2029
end
return 0
END
GO
Then call the function:
SELECT dbo.[IsHoliday](GETDATE())
SELECT dbo.[IsHoliday]('2021-07-05 09:20:51.270')
Here is the solution I came up with.
I created a table variable to store the entire years dates:
DECLARE #DateTable TABLE
(
dtDate DATE,
dtMonth VARCHAR(10),
dtDayName VARCHAR(10),
dtDayRank INT
);
Populated first 3 columns of the #DateTable:
DECLARE #Year CHAR(4), #CurrentDate DATE
SET #Year = '2018'
SET #CurrentDate = CAST(#Year + '0101' AS DATE)
WHILE #CurrentDate <= CAST(#Year + '1231' AS DATE)
BEGIN
INSERT INTO #DateTable (dtDate, dtMonth, dtDayName)
VALUES (#CurrentDate, DATENAME(mm, #CurrentDate), DATENAME(dw, #CurrentDate))
SET #CurrentDate = DATEADD(dd, 1, #CurrentDate)
END;
Once I had the table populated, I ranked the rows and updated the table:
UPDATE #DateTable
SET dtDayRank = rankdates.DayRank
FROM #DateTable datatable
INNER JOIN (
SELECT
dtDate,
DayRank = RANK() OVER (PARTITION BY dtMonth, dtDayName ORDER BY dtDate) -- rank each DayOfWeek in order
FROM #DateTable
) rankdates ON datatable.dtDate = rankdates.dtDate;
Sample Output from #DateTable
Once I had the #DateTable populated, I could use logic to identify specific days.
SELECT
HolidayName = 'Presidents'' Day',
ObservedDayOfWeek = dtDayName,
HolidayObservedDate = dtDate
FROM #DateTable
WHERE dtMonth = 'February'
AND dtDayName = 'Monday'
AND dtDayRank = 3
SELECT
HolidayName = 'Thanksgiving Day',
ObservedDayOfWeek = dtDayName,
HolidayObservedDate = dtDate
FROM #DateTable
WHERE dtMonth = 'November'
AND dtDayName = 'Thursday'
AND dtDayRank = 4
Output
I liked how this solution worked out because I can identify any date in the year by using a predicate equal to the month, day of the week and how many times this day of the week has occurred in this month. I made this into a stored procedure and table valued function so that I can run it by passing a year and it returns all holidays for that year.
Is this a good solution...is there an easier way?
There's some math you can use to make your SQL life easier e.g. 3rd Monday in February mathematically has to be between the 15th and the 21st (the earliest 3rd Monday has 14 days before it; the latest 3rd Monday can have no more than 20 days before it). If you have a tally table, it will be pretty easy to find all the dates. Here's how you can do it for president's day
with t1 as
(SELECT 1 num
FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1)) subTable(n)
),
TallyTable as
(
SELECT TOP 10000 ROW_NUMBER() OVER (ORDER BY (SELECT 1)) n
FROM t1 a
CROSS JOIN t1 b
CROSS JOIN t1 c
CROSS JOIN t1 d
CROSS JOIN t1 e
CROSS JOIN t1 f
),
DateTable as
(
SELECT DateAdd(day,n,'1/1/2018') DateValue
FROM TallyTable
)
SELECT *
FROM DateTable DT
WHERE DatePart(month,DT.DateValue) = 2 --February
AND DatePart(dw,DT.DateValue) = 2 --Monday
AND DatePart(day,DT.DateValue) BETWEEN 15 AND 21; --Day is between 15 and 21

How to get output change according to the dates input by user?

I have 3 input tables-
day_level
Dim_type Id day_date month year
1 1 2015-01-05 January 2015
1 2 2015-01-06 January 2015
1 3 2015-01-07 January 2015
1 4 2015-01-08 January 2015
1 5 2015-01-09 January 2015
1 6 2015-01-10 January 2015
1 7 2015-01-11 January 2015
1 8 2015-01-12 January 2015
1 9 2015-01-13 January 2015
1 10 2015-01-14 January 2015
1 11 2015-01-15 January 2015
1 12 2015-01-16 January 2015
1 13 2015-01-17 January 2015
1 14 2015-01-18 January 2015
1 15 2015-01-19 January 2015
1 16 2015-01-20 January 2015
This shows the weekly basis data.
week_level
Dim_type Id week_number month year
2 101 week1 January 2015
2 102 week2 January 2015
2 103 week3 January 2015
2 104 week4 January 2015
2 105 week1 February 2015
This shows the monthly basis data.
month_level
Dim_type Id month year
3 1001 January 2015
3 1002 January 2015
3 1003 January 2015
3 1004 January 2015
3 1005 February 2015
I have a 3 tables which have data according to the day level, week level and month level. There is Dim_type column which tells us which data is from which table like
dim_type=1 is for day level
dim_type=2 is for week level
dim_type=3 is for month level
Here I am not able to write a function/procedure which on the basis of input dates given by the user can decide which of the data is to be shown-
Here I give you some example suppose the date input by the user start date- 2015-01-01 and end date- 2015-01-31. Now here data is needed for whole January month so data will come from month table.
Second like start date-2015-01-05 and end date- 2015-01-06. Now we don't have a complete month on either side so here we have to consider week data. So here output will be like-
id value
102 week2 ( January)
103 week3 ( ,, )
104 week4 ( ,, )
105 week5 (Febuaray)
Here whole week is considered because saturday and sunday are non-working day.
Third is like the start date- 2015-01-05 and end date- 2015-01-20 so it will be like
id value
102 week2 ( January)
103 week3 ( ,, )
14 day level data for 18 January
15 day level data for 19 January
12 day level data for 20 January
Each table id has unique id which has data and this data is to represented in output according to the date filter. How to writer filter code is that part I need help!
So I am not able make a stored procedure/ functions that will able to tell if there is whole month or this by week data or it should be output as day level. Can anyone help me? Thanks
This will do what you're asking.
There are some caveats: The function will get the number of days in a month based on the start date.
You should store the days in a months table in the database so you're not recreating a temp table again and again.
You'd be better off aggregating the data so you don't need to use this function at all.
CREATE FUNCTION dbo.ISFullMonth (#StartDate DATE, #EndDate DATE)
RETURNS VARCHAR(5)
BEGIN
/* variables to be used */
DECLARE #Return VARCHAR(5), #Difference INT, #DaysInMonth TINYINT;
/*
table variable to store the number of days in a month
this would be better as a fixed SQL table as it'll
be called a lot
*/
DECLARE #Months TABLE
([Month] TINYINT, [NoDays] TINYINT);
/*
month values
*/
INSERT INTO #Months
VALUES
(1, 31),
(2, 28),
(3, 31),
(4, 30),
(5, 31),
(6, 30),
(7, 31),
(8, 31),
(9, 30),
(10, 31),
(11, 30),
(12, 31);
/*
get the number of days in the month
*/
SELECT #DaysInMonth = [NoDays] FROM #Months WHERE [Month] = MONTH(#StartDate);
/*
Check if it's a leap year and alter the number of days in Febuary to 29
This was taken from https://www.mssqltips.com/sqlservertip/1527/sql-server-function-to-determine-a-leap-year/
*/
IF((SELECT CASE DATEPART(mm, DATEADD(dd, 1, CAST((CAST(#StartDate AS VARCHAR(4)) + '0228') AS DATE)))
WHEN 2 THEN 1
ELSE 0
END) = 1) AND MONTH(#StartDate) = 2
SET #DaysInMonth = 29;
/*
Get the difference between the two dates
add 1 to the value to include the first day in the count
*/
SET #Difference = DATEDIFF(day, #StartDate, #EndDate)+1;
/*
Check how many days difference there are
*/
If (#Difference >= #DaysInMonth)
BEGIN
SET #Return = 'Month';
END
ELSE IF (#Difference > 7)
BEGIN
SET #Return = 'Week';
END
ELSE
BEGIN
SET #Return = 'Day';
END
RETURN #Return;
END
GO
OK This took longer to write than I expected, but here you go. This should work for now but it doesn't traverse years very well at all.
CREATE PROCEDURE GetDateParts
(
#StartDate DATE ,
#EndDate DATE
)
AS
BEGIN
/* variables to be used */
DECLARE #Return VARCHAR(5)
/*
Get the difference between the two dates
add 1 to the value to include the first day in the count
*/
, #TotalNumberOfDays INT
, #DaysInMonth TINYINT;
/* table variable to store the number of days in a month
this would be better as a fixed SQL table as it'll
be called a lot */
DECLARE #Months TABLE
([Month] TINYINT, [NoDays] TINYINT);
/* month values */
INSERT INTO #Months
VALUES
(1, 31),
(2, 28),
(3, 31),
(4, 30),
(5, 31),
(6, 30),
(7, 31),
(8, 31),
(9, 30),
(10, 31),
(11, 30),
(12, 31);
/* Create Result table */
DECLARE #ResultTable TABLE ([MonthNumber] TINYINT, [FullMonth] BIT, [Weeks] TINYINT, [Days] TINYINT)
-- set the count as the mointh number
DECLARE #Count TINYINT = MONTH(#StartDate);
SET #TotalNumberOfDays = DATEDIFF(day, #StartDate, #EndDate)+1
WHILE #Count <= MONTH(#EndDate)
BEGIN
/* get the number of days in the month */
SELECT #DaysInMonth = [NoDays] FROM #Months WHERE [Month] = #Count;
/*
Check if it's a leap year and alter the number of days in Febuary to 29
This was taken from https://www.mssqltips.com/sqlservertip/1527/sql-server-function-to-determine-a-leap-year/
*/
IF((SELECT CASE DATEPART(mm, DATEADD(dd, 1, CAST((CAST(#StartDate AS VARCHAR(4)) + '0228') AS DATE)))
WHEN 2 THEN 1
ELSE 0
END) = 1) AND MONTH(#StartDate) = 2
SET #DaysInMonth = 29;
IF (#TotalNumberOfDays >= #DaysInMonth)
BEGIN
INSERT INTO #ResultTable ([MonthNumber], [FullMonth])
VALUES (#Count, 1)
SET #TotalNumberOfDays = #TotalNumberOfDays - (#DaysInMonth-DAY(#StartDate));
SET #StartDate = DATEADD(day, (#DaysInMonth-DAY(#StartDate)+1), #StartDate);
SET #Count = #Count + 1;
END
ELSE IF (#TotalNumberOfDays >= 7)
BEGIN
INSERT INTO #ResultTable ([MonthNumber], [Weeks])
VALUES (#Count, CAST(#TotalNumberOfDays/7 AS INT))
DECLARE #Remainder TINYINT = #TotalNumberOfDays%7;
IF (#Remainder = 0)
BEGIN
SET #Count = #Count + 1;
END
ELSE
BEGIN
SET #TotalNumberOfDays = #Remainder;
END
END
ELSE
BEGIN
INSERT INTO #ResultTable ([MonthNumber], [Days])
VALUES (#Count, #TotalNumberOfDays)
SET #Count = #Count + 1;
END
END;
-- Return Results
SELECT * FROM #ResultTable;
END

Resources