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
Related
I need to return count on results per day between a time range that will overlap days.
So from 8 PM to 8 AM for every day that starts on M-F return a count for that time period. And do that for the entire year.
This is what I have, and I could do a simple and between if I only wanted on day but I'm not sure how to iterate through days especially when the start is on one day and the end on the next but skip the ones that Start on a Saturday or Sunday.
SELECT TOP (50)
ClientVisit.visittype,
ClientVisit.location_id,
ClientVisit.visittype_id,
Location.location_desc,
Location.location_code,
ClientVisit.timein,
ClientVisit.timeout,
ClientVisit.visit_dateday
FROM
ClientVisit
INNER JOIN
Location ON ClientVisit.location_id = Location.location_id
WHERE
(ClientVisit.visittype Like '%Open Chart%'
OR ClientVisit.visittype LIKE '%Diag%')
AND (Location.location_code = 'Access-505'
OR Location.location_code = 'Access-hosp')
AND (ClientVisit.timein BETWEEN #param1 AND #param2)
Filtering days of week and hours of the day is easy enough. Does this group by get at what you're trying to accomplish for the counts?
SELECT CAST(ClientVisit.timein AS DATE) AS DT, COUNT(*)
FROM ClientVisit INNER JOIN Location
ON ClientVisit.location_id = Location.location_id
WHERE
(ClientVisit.visittype Like '%Open Chart%' OR ClientVisit.visittype LIKE '%Diag%')
AND (Location.location_code = 'Access-505' OR Location.location_code = 'Access-hosp')
-- Use date params rather than datetime
AND CAST(ClientVisit.timein AS DATE) BETWEEN #param1 AND #param2
-- M-F assuming ##DATEFIRST is Sunday (7)
AND DATEPART(weekday, ClientVisit.timein) BETWEEN 2 AND 6
-- time of day. won't include the instant of 8:00:00am
AND ( DATEPART(hour, ClientVisit.timein) BETWEEN 8 AND 23
OR DATEPART(hour, ClientVisit.timein) BETWEEN 0 AND 7)
GROUP BY CAST(ClientVisit.timein AS DATE);
If you need to treat the hours from 8PM to 8AM as a single shift then you can adjust the times prior to so that times after midnight are treated as part of the preceeding day:
WITH AdjustedVisit AS (
SELECT *, DATEADD(hour, -8, timein) AS adjustedin FROM ClientVisit)
-- Use date params rather than datetime
WHERE CAST(timein AS DATE) BETWEEN #param1 AND #param2
)
SELECT CAST(v.adjustedin AS DATE) AS DT, COUNT(*)
FROM AdjustedVisit AS v INNER JOIN Location AS l
ON v.location_id = l.location_id
WHERE
(v.visittype Like '%Open Chart%' OR v.visittype LIKE '%Diag%')
AND (l.location_code = 'Access-505' OR l.location_code = 'Access-hosp')
-- M-F assuming ##DATEFIRST is Sunday (7)
AND DATEPART(weekday, v.adjustedin) BETWEEN 2 AND 6
-- time of day. won't include the instant of 8:00:00am
AND DATEPART(hour, v.adjustedin) BETWEEN 12 AND 23
GROUP BY CAST(v.adjustedin AS DATE);
I have a list holidays stored in BankHolidays table for a couple of years. Now i need to compute the last working day for any given date.
CTE may help in this scenario, however i prefer to have this snippet as a function.
I have written the following (pseudo) code to get my result, but i'm not able to do a recursive call
CREATE FUNCTION dbo.Get_Previous_Working_Day(#Day date) RETURNS date
AS BEGIN
if(datename(dw,#Day) = 'Sunday')
set #Day = DATEADD(day, -1, #Day)
if(datename(dw,#Day) = 'Saturday')
set #Day = DATEADD(day, -1, #Day)
if not exists (select count(1) from BankHolidays where datepart(yyyy,HolidayDate) = datepart(yyyy,#Day))
return null
else
begin
if exists (select count(1) from BankHolidays where convert(date,HolidayDate) = convert(date,#Day))
begin
set #Day = DATEADD(day, -1, #Day)
dbo.Get_Previous_Working_Day(#Day) --This recurise call may need to be modified
end
else
return #Day
end
end
Thank you in advance
Edit 1
Error : Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32).
I guess this is due to the stack over flow on recursive calls and its not able to decide when to quit. Seems like a logical error. Unfortunately i couldn't figure where its going wrong
BankHolidays Table:
--------------------------------------------------
HolidayDate DayofWeek Description
--------------------------------------------------
2015-01-01 Thursday New year
2010-01-01 Friday New year
2015-04-03 Friday Good Friday
2015-05-04 Monday Early May bank holiday
2014-06-11 Wednesday June 14 - NEW ENTRY
2015-05-25 Monday Spring bank holiday
2015-12-28 Monday Boxing Day (substitute day)
2015-04-06 Monday Easter Monday
2015-08-31 Monday Summer bank holiday
2015-12-25 Friday Christmas Day
Expected Output
Get_Previous_Working_Day('2015-01-01') -- Result : 2014-12-31
Get_Previous_Working_Day('2015-01-02') -- Result : 2014-12-31
Get_Previous_Working_Day('2010-01-04') -- Result : 2009-12-31
Get_Previous_Working_Day('2015-04-06') -- Result : 2015-04-02
Get_Previous_Working_Day('2015-12-05') -- Result : 2015-12-04
Get_Previous_Working_Day('2015-12-06') -- Result : 2015-04-04
Get_Previous_Working_Day('2014-06-12') -- Result : 2014-06-10
CREATE TABLE dbo.BankHolidays
(
HolidayDate DATE PRIMARY KEY,
[DayofWeek] AS DATENAME(dw, HolidayDate),
[Description] VARCHAR(100)
)
INSERT INTO dbo.BankHolidays (HolidayDate, [Description])
VALUES
('2015-01-01', 'New year'),
('2010-01-01', 'New year'),
('2015-04-03', 'Good Friday'),
('2015-05-04', 'Early May bank holiday'),
('2014-06-11', 'June 14 - NEW ENTRY'),
('2015-05-25', 'Spring bank holiday'),
('2015-12-28', 'Boxing Day (substitute day)'),
('2015-04-06', 'Easter Monday'),
('2015-08-31', 'Summer bank holiday'),
('2015-12-25', 'Christmas Day')
GO
CREATE FUNCTION dbo.Get_Previous_Working_Day
(
#date DATE
)
RETURNS DATE
AS BEGIN
DECLARE #result DATE
;WITH cte AS
(
SELECT dt = DATEADD(DAY, -1, #date)
UNION ALL
SELECT DATEADD(DAY, -1, dt)
FROM cte
WHERE dt > DATEADD(DAY, -30, #date)
)
SELECT TOP(1) #result = dt
FROM cte
WHERE dt NOT IN (SELECT t.HolidayDate FROM dbo.BankHolidays t)
AND DATENAME(dw, dt) NOT IN ('Saturday', 'Sunday')
ORDER BY dt DESC
OPTION (MAXRECURSION 0)
RETURN #result
END
GO
SELECT dbo.Get_Previous_Working_Day('2015-12-29')
#Ramu,
Please find the corrected part here.
There are two issues in the function definition. One is in the calling of function. Since the function should be called by using SELECT statement so you have to use that.
Second, the function should end by return statement.
So the correct definition would be:
CREATE FUNCTION dbo.Get_Previous_Working_Day(#Day date) RETURNS date
AS BEGIN
if(datename(dw,#Day) = 'Sunday')
set #Day = DATEADD(day, -1, #Day)
if(datename(dw,#Day) = 'Saturday')
set #Day = DATEADD(day, -1, #Day)
if not exists (select count(1) from BankHolidays where datepart(yyyy,HolidayDate) = datepart(yyyy,#Day))
return null
else
begin
if exists (select count(1) from BankHolidays where convert(date,HolidayDate) = convert(date,#Day))
begin
set #Day = DATEADD(day, -1, #Day)
select #Day = dbo.Get_Previous_Working_Day(#Day) --This recurise call may need to be modified
return #Day
end
else
return #Day
end
return #Day
end
I have to get/create date from the user input of week of month (week number in that month - 1st,2nd,3rd,4th and last) and day of week (sunday,monday..) in SQL server.
Examples:
4th Sunday of every month, Last Friday of every month, First Monday etc.
I was able to do it easily in .net but SQL server does seem limited in the date functions.
I am having to use lot of logic to get the date. To calculate the date using the above two parameters I had to use lot of datepart function.
Any suggestions on how to come up with the optimal SQL query for such a function?
I created a function other day for another OP GET Month, Quarter based on Work Week number
This function takes the current year as default it can be further modified to take Year as a parameter too.
an extension to that function can produce the results you are looking for ....
WITH X AS
(
SELECT TOP (CASE WHEN YEAR(GETDATE()) % 4 = 0 THEN 366 ELSE 365 END)-- to handle leap year
DATEADD(DAY
,ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1
, CAST(YEAR(GETDATE()) AS VARCHAR(4)) + '0101' )
DayNumber
From master..spt_values
),DatesData AS(
SELECT DayNumber [Date]
,DATEPART(WEEKDAY,DayNumber) DayOfTheWeek
,DATEDIFF(WEEK,
DATEADD(WEEK,
DATEDIFF(WEEK, 0, DATEADD(MONTH,
DATEDIFF(MONTH, 0, DayNumber), 0)), 0)
, DayNumber- 1) + 1 WeekOfTheMonth
FROM X )
SELECT * FROM DatesData
WHERE DayOfTheWeek = 6 -- A function would expect these two parameters
AND WeekOfTheMonth = 4 -- #DayOfTheWeek and #WeekOfTheMonth
Here is a general formula:
declare #month as datetime --set to the first day of the month you wish to use
declare #week as int --1st, 2nd, 3rd...
declare #day as int --Day of the week (1=sunday, 2=monday...)
--Second monday in August 2015
set #month = '8/1/2015'
set #week = 2
set #day = 2
select dateadd(
day,
((7+#day) - datepart(weekday, #month)) % 7 + 7 * (#week-1),
#month
)
You can also find the last, 2nd to last... etc with this reverse formula:
--Second to last monday in August 2015
set #month = '8/1/2015'
set #week = 2
set #day = 2
select
dateadd(
day,
-((7+datepart(weekday, dateadd(month,1,#month)-1)-#day)) % 7 - 7 * (#week-1),
dateadd(month,1,#month)-1
)
I am creating a calendar table for my warehouse. I will use this as a foreign key for all the date fields.
The code shown below creates the table and populates it. I was able to figure out how to find Memorial Day (last Monday of May) and Labor Day (first Monday of September).
SET NOCOUNT ON
DROP Table dbo.Calendar
GO
Create Table dbo.Calendar
(
CalendarId Integer NOT NULL,
DateValue Date NOT NULL,
DayNumberOfWeek Integer NOT NULL,
NameOfDay VarChar (10) NOT NULL,
NameOfMonth VarChar (10) NOT NULL,
WeekOfYear Integer NOT NULL,
JulianDay Integer NOT NULL,
USAIsBankHoliday Bit NOT NULL,
USADayName VarChar (100) NULL,
)
ALTER TABLE dbo.Calendar ADD CONSTRAINT
DF_Calendar_USAIsBankHoliday DEFAULT 0 FOR USAIsBankHoliday
GO
ALTER TABLE dbo.Calendar ADD CONSTRAINT
DF_Calendar_USADayName DEFAULT '' FOR USADayName
GO
Declare #StartDate DateTime = '01/01/2000'
Declare #EndDate DateTime = '01/01/2020'
While #StartDate < #EndDate
Begin
INSERT INTO dbo.Calendar
(
CalendarId,
DateValue,
WeekOfYear,
DayNumberOfWeek,
NameOfDay,
NameOfMonth,
JulianDay
)
Values
(
YEAR (#StartDate) * 10000 + MONTH (#StartDate) * 100 + Day (#StartDate), --CalendarId
#StartDate, -- DateValue
DATEPART (ww, #StartDate), -- WeekOfYear
DATEPART (dw, #StartDate), -- DayNumberOfWeek
DATENAME (dw, #StartDate), -- NameOfDay
DATENAME (M, #StartDate), -- NameOfMonth
DATEPART (dy, #StartDate) -- JulianDay
)
Set #StartDate += 1
End
--=========================== Weekends
-- saturday and sunday
UPDATE dbo.Calendar SET USAIsBankHoliday = 1, USADayName += 'Weekend, ' WHERE DayNumberOfWeek IN (1, 7)
--=========================== Bank Holidays
-- new years day
UPDATE dbo.Calendar SET USAIsBankHoliday = 1, USADayName += 'New Year''s Day, ' WHERE (CalendarId % 2000) IN (101)
-- memorial day (last Monday in May)
UPDATE dbo.Calendar
SET USAIsBankHoliday = 1,
USADayName += 'Memorial Day, '
WHERE 1=1
AND CalendarId IN
(
SELECT MAX (CalendarId)
FROM dbo.Calendar
WHERE MONTH (DateValue) = 5
AND DATEPART (DW, DateValue)=2
GROUP BY YEAR (datevalue)
)
-- independence day
UPDATE dbo.Calendar SET USAIsBankHoliday = 1, USADayName += 'Independence Day, ' WHERE (CalendarId % 2000) IN (704)
-- labor day (first Monday in September)
UPDATE dbo.Calendar
SET USAIsBankHoliday = 1,
USADayName += 'Labor Day, '
WHERE 1=1
AND CalendarId IN
(
SELECT MIN (CalendarId)
FROM dbo.Calendar
WHERE MONTH (DateValue) = 9
AND DATEPART (DW, DateValue)=2
GROUP BY YEAR (datevalue)
)
-- thanksgiving day (fourth Thursday in November)
UPDATE dbo.Calendar
SET USAIsBankHoliday = 1,
USADayName += 'Thanksgiving Day, '
WHERE 1=1
AND CalendarId IN
(
SELECT Max (CalendarId)
FROM dbo.Calendar
WHERE MONTH (DateValue) = 11
AND DATEPART (DW, DateValue)=5
GROUP BY YEAR (datevalue)
)
-- christmas
UPDATE dbo.Calendar SET USAIsBankHoliday = 1, USADayName += 'Christmas Day, ' WHERE (CalendarId % 2000) IN (1225)
--=========================== Other named days
-- new years eve
UPDATE dbo.Calendar SET USADayName += 'New Year''s Eve, ' WHERE (CalendarId % 2000) IN (1231)
-- christmas eve
UPDATE dbo.Calendar SET USADayName += 'Christmas Eve, ' WHERE (CalendarId % 2000) IN (1224)
-- boxing day
UPDATE dbo.Calendar SET USADayName += 'Boxing Day, ' WHERE (CalendarId % 2000) IN (1226)
--=========================== Remove trailing comma
UPDATE dbo.Calendar SET USADayName = SubString (USADayName, 1, LEN (USADayName) -1) WHERE LEN (USADayName) > 2
SELECT * FROM dbo.Calendar
I am stumped on figuring out Thanksgiving day (Thursday of the last FULL week of November).
Edit:
Correction based on comment by John Sauer
Thanksgiving is the fourth Thursday of November. However, upon checking several years, I find that it has turned out to also be the Thursday of the last full week of Nov.
I am stumped on figuring out Thanksgiving day (Thursday of the last FULL week of November).
Last Saturday of November - 2
Take the last Saturday of November, and subtract two days ;)
WITH cal AS
(
SELECT CAST('2009-30-11' AS DATETIME) AS cdate
UNION ALL
SELECT DATEADD(day, -1, cdate)
FROM cal
WHERE cdate >= '2009-01-11'
)
SELECT TOP 1 DATEADD(day, -2, cdate)
FROM cal
WHERE DATEPART(weekday, cdate) = 6
For the complex algorithms, it's sometimes better to find a matching date from a set than trying to construct an enormous single-value formula.
See this article in my blog for more detail:
Checking event dates
declare #thedate datetime
set #thedate = '11/24/2011'
select 1
where datepart(dw, #thedate) = 5 and datepart(m, #thedate) = 11 AND (datepart(dd, #thedate) - 21) between 0 and 6
Is the date a Thursday in November and is there less than a week remaining.
We use a neat Scalar-valued Function in our database that we can call to determine whether or not a date is a NonWorkDay. We manually add special dates for Observed Holidays.
Create Function:
CREATE FUNCTION [dbo].[NonWorkDay](#date as DATETIME)
RETURNS bit
AS
BEGIN
if #date is not null
begin
-- Weekends
if DATEPART(weekday, #date)=1 return 1 --Sunday
if DATEPART(weekday, #date)=7 return 1 --Saturday
-- JAN
if month(#date)=1 AND day(#date)=1 return 1 -- New Years Day
-- MAY
if month(#date)=5 and DATEPART(weekday, #date)=2 and day(#date)>24 and day(#date)<=31 return 1 --Memorial Day
-- JULY
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
-- NOV
if month(#date)=11 and DATEPART(weekday, #date)=5 and day(#date)>21 and day(#date)<=28 return 1--Thanksgiving
if month(#date)=11 and DATEPART(weekday, #date)=6 and day(#date)>22 and day(#date)<=29 return 1 -- Black Friday
-- DEC
if month(#date)=12 AND day(#date)=24 return 1 -- Christmas Eve
if month(#date)=12 AND day(#date)=25 return 1 -- Christmas
if month(#date)=12 AND day(#date)=31 return 1 -- NYE
-- Office Closed for Observed Holiday Dates
if month(#date)=7 AND day(#date)=5 AND year(#date)=2021 return 1 -- 4th of July Observed for 2021
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)=7 AND day(#date)=3 AND year(#date)=2026 return 1 -- 4th of July Observed for 2026
if month(#date)=7 AND day(#date)=5 AND year(#date)=2027 return 1 -- 4th of July Observed for 2027
end
return 0
END
GO
Call Function:
IF [dbo].[NonWorkDay](Getdate())= 0 -- If (not a NonWorkDay -ie not Sat or Sun)
I have a need to create a gross requirements report that takes how much supply and demand of a item in inventory from a start date onwards and 'buckets' it into different weeks of the year so that material planners know when they will need a item and if they have enough stock in inventory at that time.
As an example, today’s date (report date) is 8/27/08. The first step is to find the date for the Monday of the week the report date falls in. In this case, Monday would be 8/25/08. This becomes the first day of the first bucket. All transactions that fall before that are assigned to week #0 and will be summarized as the beginning balance for the report. The remaining buckets are calculated from that point. For the eighth bucket, there is no ending date so any transactions after that 8th bucket start date are considered week #8.
WEEK# START DATE END DATE
0.......None..........8/24/08
1.......8/25/08.......8/31/08
2.......9/1/08.........9/7/08
3.......9/8/08.........9/14/08
4.......9/15/08.......9/21/08
5.......9/22/08.......9/28/08
6.......9/29/08.......10/5/08
7.......10/06/08.....10/12/08
8.......10/13/08......None
How do I get the week #, start date, end date for a given date?
I've always found it easiest and most efficient (for SQL Server) to construct a table with one row for every week into the future through your domain horizon; and join to that (with a "WHERE GETDATE() >= MONDATE AND NOT EXISTS (SELECT 1 FROM table WHERE MONDATE < GETDATE())".
Anything you try to do with UDF's will be much less efficient and I find more difficult to use.
You can get Monday for any given date in a week as:
DATEADD(d, 1 - DATEPART(dw, #date), #date)
and you can write a stored procedure with the following body
-- find Monday at that week
DECLARE #currentDate SMALLDATETIME
SELECT #currentDate = DATEADD(d, 1 - DATEPART(dw, #date), #date)
-- create a table and insert the first record
DECLARE #weekTable TABLE (Id INT, StartDate SMALLDATETIME, EndDate SMALLDATETIME)
INSERT INTO #weekTable VALUES (0, NULL, #currentDate)
-- increment the date
SELECT #currentDate = DATEADD(d, 1, #currentDate)
-- iterate for 7 more weeks
DECLARE #id INT
SET #id = 1
WHILE #id < 8
BEGIN
INSERT INTO #weekTable VALUES (#id, #currentDate, DATEADD(d, 6, #currentDate))
SELECT #currentDate = DATEADD(ww, 1, #currentDate)
SET #id = #id + 1
END
-- add the last record
INSERT INTO #weekTable VALUES (8, #currentDate, NULL)
-- select the values
SELECT Id 'Week #', StartDate 'Start Date', EndDate 'End Date'
FROM #weekTable
When I pass
#date = '20080827'
to this procedure, I get the following
Week # Start Date End Date
0 NULL 2008-08-24 00:00:00
1 2008-08-25 00:00:00 2008-08-31 00:00:00
2 2008-09-01 00:00:00 2008-09-07 00:00:00
3 2008-09-08 00:00:00 2008-09-14 00:00:00
4 2008-09-15 00:00:00 2008-09-21 00:00:00
5 2008-09-22 00:00:00 2008-09-28 00:00:00
6 2008-09-29 00:00:00 2008-10-05 00:00:00
7 2008-10-06 00:00:00 2008-10-12 00:00:00
8 2008-10-13 00:00:00 NULL
--SQL sets the first day of the week as sunday and for our purposes we want it to be Monday.
--This command does that.
SET DATEFIRST 1
DECLARE
#ReportDate DATETIME,
#Weekday INTEGER,
#NumDaysToMonday INTEGER,
#MondayStartPoint DATETIME,
#MondayStartPointWeek INTEGER,
#DateToProcess DATETIME,
#DateToProcessWeek INTEGER,
#Bucket VARCHAR(50),
#DaysDifference INTEGER,
#BucketNumber INTEGER,
#NumDaysToMondayOfDateToProcess INTEGER,
#WeekdayOfDateToProcess INTEGER,
#MondayOfDateToProcess DATETIME,
#SundayOfDateToProcess DATETIME
SET #ReportDate = '2009-01-01'
print #ReportDate
SET #DateToProcess = '2009-01-26'
--print #DateToProcess
SET #Weekday = (select DATEPART ( dw , #ReportDate ))
--print #Weekday
--print DATENAME(dw, #ReportDate)
SET #NumDaysToMonday =
(SELECT
CASE
WHEN #Weekday = 1 THEN 0
WHEN #Weekday = 2 THEN 1
WHEN #Weekday = 3 THEN 2
WHEN #Weekday = 4 THEN 3
WHEN #Weekday = 5 THEN 4
WHEN #Weekday = 6 THEN 5
WHEN #Weekday = 7 THEN 6
END)
--print #NumDaysToMonday
SET #MondayStartPoint = (SELECT DATEADD (d , -1*#NumDaysToMonday, #ReportDate))
--print #MondayStartPoint
SET #DaysDifference = DATEDIFF ( dd , #MondayStartPoint , #DateToProcess )
--PRINT #DaysDifference
SET #BucketNumber = #DaysDifference/7
--print #BucketNumber
----Calculate the start and end dates of this bucket------
PRINT 'Start Of New Calc'
print #DateToProcess
SET #WeekdayOfDateToProcess = (select DATEPART ( dw , #DateToProcess ))
print #WeekdayOfDateToProcess
SET #NumDaysToMondayOfDateToProcess=
(SELECT
CASE
WHEN #WeekdayOfDateToProcess = 1 THEN 0
WHEN #WeekdayOfDateToProcess = 2 THEN 1
WHEN #WeekdayOfDateToProcess = 3 THEN 2
WHEN #WeekdayOfDateToProcess = 4 THEN 3
WHEN #WeekdayOfDateToProcess = 5 THEN 4
WHEN #WeekdayOfDateToProcess = 6 THEN 5
WHEN #WeekdayOfDateToProcess = 7 THEN 6
END)
print #NumDaysToMondayOfDateToProcess
SET #MondayOfDateToProcess = (SELECT DATEADD (d , -1*#NumDaysToMondayOfDateToProcess, #DateToProcess))
print #MondayOfDateToProcess ---This is the start week
SET #SundayOfDateToProcess = (SELECT DATEADD (d , 6, #MondayOfDateToProcess))
PRINT #SundayOfDateToProcess
The problem I see with the one bucket at a time approach is that its hard to make it scale,
If you join into a user defined function you will get better performance, you could use this a a starting point
Why not use a combination of DATEPART(year, date-column) and DATEPART(week, date-column) and group by these values. This works if the week in DATEPART is aligned on Mondays as ISO 8601 requires. In outline:
SELECT DATEPART(year, date_column) AS yyyy,
DATEPART(week, date_column) AS ww,
...other material as required...
FROM SomeTableOrOther
WHERE ...appropriate filters...
GROUP BY yyyy, ww -- ...and other columns as necessary...