The title of this blog is only half as thrilling as the SQL. I can assure you.
I have a calendar table at work that is based off of the calendar table that you can find on aspfaq.com in an article titled Why should I consider using an auxiliary calendar table?
The big problem is that the holidays in the script are not actually federal holidays. They’re just a set of holidays that seemed significant to the author at the time of writing. The Federal government actually has rules about how to calculate holidays as well as a great list of federal holidays.
One of the more interesting rules is that if a holiday falls on a Saturday, the holiday is observed on a Friday. If the holiday falls on Sunday, the holiday is observed on Monday. That means New Year’s Day sometimes happens last year.
The following query is pretty self-explanatory and basically uses a few look forwards and look backs to figure out when holidays end up falling on the weekend and it makes a second, observed, holiday on the closest weekday.
I have verified this from 1999 through 2020 just to make sure that my math was correct against all posted Federal Holidays.
I hope this saves somebody some time.
UPDATE dbo.Calendar
SET IsHoliday = 0;
-- New Year's day
UPDATE Calendar
SET IsHoliday = 1
WHERE M = 1
AND D = 1;
-- New Year's day observed
-- if NYD is a Saturday
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 12
AND c1.D = 31
AND c1.DW = 6
AND EXISTS (SELECT 1
FROM Calendar AS c2
WHERE c2.M = 1
AND c2.D = 1
AND c2.[Date] > c1.[Date]
AND DW = 7);
-- if NYD is a Sunday
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 1
AND c1.D = 2
AND c1.DW = 2
AND EXISTS (SELECT 1
FROM Calendar AS c2
WHERE c2.M = 1
AND c2.D = 1
AND c2.[Date] < c1.[Date]
AND c2.DW = 1);
-- Martin Luther King Day
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 1
AND c1.DW = 2
AND (SELECT COUNT(*)
FROM Calendar AS c2
WHERE c2.M = 1
AND c2.DW = 2
AND c2.Y = c1.Y
AND c2.[Date] < c1.[Date]) = 2;
-- Washington's Birthday
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 2
AND c1.DW = 2
AND (SELECT COUNT(*)
FROM Calendar AS c2
WHERE c2.M = 2
AND c2.DW = 2
AND c2.Y = c1.Y
AND c2.[Date] < c1.[Date]) = 2;
-- Memorial Day
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE M = 5
AND DW = 2
AND NOT EXISTS (SELECT 1
FROM Calendar AS c2
WHERE M = 5 AND DW = 2
AND c2.Y = c1.Y
AND c2.[Date] > c1.[Date]);
-- Independence Day
UPDATE Calendar
SET IsHoliday = 1
WHERE M = 7
AND D = 4;
-- Independence Day, observed
-- if Independence Day is a Saturday
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 7
AND c1.D = 3
AND c1.DW = 6
AND EXISTS (SELECT 1
FROM Calendar AS c2
WHERE c2.M = 7
AND c2.D = 4
AND c2.DW = 7
AND c2.[Date] > c1.[Date]);
-- if Independence Day is a Sunday
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 7
AND c1.D = 5
AND c1.DW = 2
AND EXISTS (SELECT 1
FROM Calendar AS c2
WHERE c2.M = 7
AND c2.D = 4
AND c2.DW = 1
AND c2.[Date] < c1.[Date]);
-- Labor Day
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE M = 9
AND DW = 2
AND NOT EXISTS (SELECT 1
FROM Calendar AS c2
WHERE M = 9 AND DW = 2
AND c2.Y = c1.Y
AND c2.[Date] < c1.[Date]);
-- Columbus Day
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 10
AND c1.DW = 2
AND (SELECT COUNT(*)
FROM Calendar AS c2
WHERE c2.M = 10
AND c2.DW = 2
AND c2.Y = c1.Y
AND c1.[Date] > c2.[Date]) = 1
-- Veterans' Day
UPDATE Calendar
SET IsHoliday = 1
WHERE M = 11
AND D = 11
-- Veterans' Day, observed
-- when Veterans' Day is a Saturday
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 11
AND c1.D = 10
AND c1.DW = 6
AND EXISTS (SELECT 1
FROM Calendar AS c2
WHERE c2.M = 11
AND c2.D = 11
AND c2.DW = 7
AND c2.[Date] > c1.[Date]);
-- when Veterans' Day is a Sunday
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 11
AND c1.D = 12
AND c1.DW = 2
AND EXISTS (SELECT 1
FROM Calendar AS c2
WHERE c2.M = 11
AND c2.D = 11
AND c2.DW = 1
AND c2.[Date] < c1.[Date]);
-- Thanksgiving Day
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE M = 11
AND DW = 5
AND (SELECT COUNT(*)
FROM Calendar AS c2
WHERE M = 11
AND DW = 5
AND c2.Y = c1.Y
AND c2.[Date] < c1.[Date]) = 3;
-- Christmas Day
UPDATE Calendar
SET IsHoliday = 1
WHERE M = 12
AND D = 25;
-- Christmas Day, observed
-- when Christmas Day is a Saturday
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 12
AND c1.D = 24
AND c1.DW = 6
AND EXISTS (SELECT 1
FROM Calendar AS c2
WHERE c2.M = 12
AND c2.D = 25
AND c2.DW = 7
AND c2.[Date] > c1.[Date]);
-- when Christmas Day is a Sunday
UPDATE Calendar
SET IsHoliday = 1
FROM Calendar AS c1
WHERE c1.M = 12
AND c1.D = 26
AND c1.DW = 2
AND EXISTS (SELECT 1
FROM Calendar AS c2
WHERE c2.M = 12
AND c2.D = 25
AND c2.DW = 1
AND c2.[Date] < c1.[Date]);
Comments
I don’t find words to appreciate your hardwork.
Stupdendous!!!!!
keep it up!!!!!
Best Regards,
Ram
Very well done. Hats off to you. It works perfectly. Many Many Thanks!!!
Thanks for this query. It saved me many hours of work. I used it to update my data warehouse Calendar table and it worked perfectly!!
Thanks,
Remi.
This is a beautiful thing. I made a small change and added an IsFederalHoliday in addition to the IsHoliday column. Saved me lots and lots of work.
Thanks so much, between your article and the “Why I should..” post, I finally have a robust solution for accurately calculating package delivery schedules. Not to mention a whole slew of other useful things. I exclusively work in Firebird, so I had to tweak the code a bit, but it works great. Willing to share if anyone needs it.
How would you recommend adding production vs. non-production day for a manufacturing implementation?
How would you recommend adding shifts for the same environment? Some have 2, some 3, some a mix depending on the deparment?
I would track production days with a separate bit column, ProductionDay, since production schedules aren’t related to days of the year.
I would maintain a separate shifts table to describe the department/shift/day relationship. That one is a bit trickier and depends more on how that data will be used.
Has anyone already altered this script in a way that only creates the observed holiday and not 2 instances of a given holiday if it occurs on a weekend?
I have not, but feel free to modify it. Typically we’ve wanted to know about both the observed and actual holiday, but I can see why you’d want one or the other. It’s always possible to add an extra column for observed holidays and use that instead. I’ve usually found it helpful to know when the real holiday occurs as well as when the bank is closed.
How would you get the date for a query where you know the current date and you need the record that is 8 business days prior to that date?
There are a number of ways to do it. You could add a BusinessDayNumber column to your table and then populate that when you create the table. Your query becomes as easy as
SELECT * FROM dbo.Calendar WHERE BusinessDayNumber = (SELECT BusinessDayNumber - 8 FROM dbo.Calendar WHERE Date = @day);Using the auxiliary calendar table referred to by the scripts above.
How would I populate that?
I’d use an UPDATE statement combined with a CURSOR, but that’s just me.
Trackbacks
One Trackback