Published on

November 9, 2017

Creating a Dynamic Holiday Calendar in SQL Server

Do you find it challenging to maintain a list of holidays and working days for businesses in your SQL Server database? Are you tired of manually updating the holiday dates every year? In this article, we will discuss a solution to this problem by creating a dynamic holiday calendar in SQL Server.

Defining Holidays

Most holidays follow certain rules. For example, New Year’s Day, Christmas, and Independence Day occur on specific days but “float” if they fall on a weekend. There are also holidays that occur on a specific day of the week and a specific week of the month. To handle these variations, we can create a HolidayDefinition table that contains the country, holiday name, month, week, day, floating status, federal status, and business status.


CREATE TABLE dbo.HolidayDefinition (
    CountryCd CHAR(2) NOT NULL,
    Holiday VARCHAR(50) NOT NULL,
    MonthNbr TINYINT NOT NULL,
    WeekNbr TINYINT NOT NULL,
    DayNbr TINYINT NOT NULL,
    IsFloating BIT NOT NULL CONSTRAINT DF_HolidayDefinition_IsFloating DEFAULT (0),
    IsFederal BIT NOT NULL CONSTRAINT DF_HolidayDefinition_IsFederal DEFAULT (1),
    IsBusiness BIT NOT NULL CONSTRAINT DF_HolidayDefinition_IsBusiness DEFAULT (0),
    CONSTRAINT PK_HolidayDefinition PRIMARY KEY NONCLUSTERED (CountryCd ASC, Holiday ASC)
);

We can enforce some rules using check constraints. For example, if a holiday occurs on a specific day, the week number must be 0. If the week number is greater than 0, the day number corresponds to the day of the week. Only holidays falling on a specific day can “float”.

Loading Holiday Definitions

Next, we need to load the holiday definitions into the HolidayDefinition table. We can use a merge statement to insert new holidays and update existing ones. Here’s an example of how to merge the new dates:


-- create a CTE containing the source data to be merged into the target table
WITH cte_HolidayDefinition AS (
    SELECT CountryCd = 'US', Holiday = 'New Years Day', MonthNbr = '1', WeekNbr = '0', DayNbr = '1', IsFloating = '1', IsFederal = '1', IsBusiness = '1'
    UNION ALL
    SELECT CountryCd = 'US', Holiday = 'Martin Luther King Day', MonthNbr = '1', WeekNbr = '3', DayNbr = '2', IsFloating = '0', IsFederal = '1', IsBusiness = '1'
    -- add more holiday definitions here
)
-- merge the source data into the target table
MERGE dbo.HolidayDefinition tgt
USING (
    SELECT CountryCd, Holiday, MonthNbr, WeekNbr, DayNbr, IsFloating, IsFederal, IsBusiness
    FROM cte_HolidayDefinition
    EXCEPT
    SELECT CountryCd, Holiday, MonthNbr, WeekNbr, DayNbr, IsFloating, IsFederal, IsBusiness
    FROM dbo.HolidayDefinition
) src
ON tgt.CountryCd = src.CountryCd
    AND tgt.MonthNbr = src.MonthNbr
    AND tgt.WeekNbr = src.WeekNbr
    AND tgt.DayNbr = src.DayNbr
WHEN MATCHED THEN
    UPDATE SET
        CountryCd = src.CountryCd,
        Holiday = src.Holiday,
        MonthNbr = src.MonthNbr,
        WeekNbr = src.WeekNbr,
        DayNbr = src.DayNbr,
        IsFloating = src.IsFloating,
        IsFederal = src.IsFederal,
        IsBusiness = src.IsBusiness
WHEN NOT MATCHED THEN
    INSERT (CountryCd, Holiday, MonthNbr, WeekNbr, DayNbr, IsFloating, IsFederal, IsBusiness)
    VALUES (src.CountryCd, src.Holiday, src.MonthNbr, src.WeekNbr, src.DayNbr, ISNULL(src.IsFloating, 0), ISNULL(src.IsFederal, 1), ISNULL(src.IsBusiness, 0));

Building a Holiday Calendar

Now that we have the holiday definitions in the HolidayDefinition table, we can start using them to generate a holiday calendar. We can create a simple inline table-valued function that takes the country code and year as parameters and returns a table of holiday dates and other information.


CREATE FUNCTION dbo.fn_HolidayCalendar (
    @CountryCd CHAR(2) = 'US',
    @Year SMALLINT
)
RETURNS TABLE
AS
RETURN
SELECT
    h.CountryCd,
    h.Holiday,
    HolidayOn = CONVERT(DATE, CASE h.WeekNbr
        WHEN 0 THEN CASE h.IsFloating
            WHEN 0 THEN r.HolidayOn
            ELSE DATEADD(DAY, CASE DATENAME(WEEKDAY, r.HolidayOn)
                WHEN 'Sunday' THEN 1
                WHEN 'Saturday' THEN -1
                ELSE 0
            END, r.HolidayOn)
        END
        ELSE r.HolidayOn
    END),
    h.MonthNbr,
    h.WeekNbr,
    h.DayNbr,
    h.IsFloating,
    h.IsFederal,
    h.IsBusiness
FROM dbo.HolidayDefinition h
CROSS APPLY (
    SELECT
        BOM = CONVERT(DATE, CONVERT(CHAR(4), @Year) + RIGHT(CONVERT(CHAR(5), (100 * h.MonthNbr) + 10001), 4)),
        EOM = CASE h.WeekNbr
            WHEN 5 THEN DATEADD(DAY, -1, DATEADD(MONTH, 1, CONVERT(DATE, CONVERT(CHAR(4), @Year) + RIGHT(CONVERT(CHAR(5), (100 * h.MonthNbr) + 10001), 4))))
        END
) m
CROSS APPLY (
    SELECT
        offset = CASE h.WeekNbr
            WHEN 0 THEN h.DayNbr - 1
            WHEN 5 THEN CASE
                WHEN DATEPART(WEEKDAY, m.EOM) >= h.DayNbr THEN -(DATEPART(WEEKDAY, m.EOM) - h.DayNbr)
                ELSE -((DATEPART(WEEKDAY, m.EOM) - h.DayNbr) + 7)
            END
            ELSE CASE
                WHEN h.DayNbr >= DATEPART(WEEKDAY, m.BOM) THEN (h.DayNbr - DATEPART(WEEKDAY, m.BOM)) + (h.WeekNbr - 1) * 7
                ELSE (h.DayNbr - DATEPART(WEEKDAY, m.BOM)) + (h.WeekNbr * 7)
            END
        END
) o
CROSS APPLY (
    SELECT
        HolidayOn = DATEADD(DAY, o.offset, CASE h.WeekNbr
            WHEN 5 THEN m.EOM
            ELSE m.BOM
        END)
) r
WHERE h.CountryCd = @CountryCd
    AND (h.IsFederal = 1 OR h.IsBusiness = 1)
    AND h.Holiday NOT IN ('Good Friday', 'Easter Sunday')
UNION ALL
SELECT
    h.CountryCd,
    h.Holiday,
    r.HolidayOn,
    r.MonthNbr,
    WeekNbr = 0,
    r.DayNbr,
    h.IsFloating,
    h.IsFederal,
    h.IsBusiness
FROM (
    SELECT
        Holiday = 'Good Friday',
        HolidayOn = CONVERT(DATE, CONVERT(CHAR(4), @Year) + CASE
            WHEN e.d > 31 THEN '04'
            ELSE '03'
        END + RIGHT(CONVERT(CHAR(3), 100 + CASE
            WHEN e.d > 31 THEN e.d - 31
            ELSE e.d
        END - 2), 2)),
        MonthNbr = CASE
            WHEN e.d > 31 THEN 4
            ELSE 3
        END,
        DayNbr = CASE
            WHEN e.d > 31 THEN e.d - 31
            ELSE e.d
        END
    FROM (
        SELECT
            d.g,
            d.c,
            d.h,
            d.i,
            d.d,
            IsFalse = CONVERT(BIT, 0)
        FROM (
            SELECT
                c.g,
                c.c,
                c.h,
                c.i,
                d = c.i - ((@Year + (@Year / 4) + c.i + 2 - c.c + (c.c / 4)) % 7) + 28
            FROM (
                SELECT
                    b.g,
                    b.c,
                    b.h,
                    i = (b.c - (b.c / 4) - ((8 * b.c + 13) / 25) + 19 * b.g + 15) % 30
                FROM (
                    SELECT
                        a.g,
                        a.c,
                        h = (a.c - (a.c / 4) - ((8 * a.c + 13) / 25) + 19 * a.g + 15) % 30
                    FROM (
                        SELECT
                            g = @Year % 19,
                            c = @Year / 100
                    ) a
                ) b
            ) c
        ) d
    ) e
) r
CROSS APPLY (
    SELECT *
    FROM dbo.HolidayDefinition d
    WHERE d.CountryCd = @CountryCd
        AND d.Holiday = r.Holiday
        AND (d.IsFederal = 1 OR d.IsBusiness = 1)
) h;

With this function, you can generate a custom calendar with holidays and business days for any country and year.

By creating a dynamic holiday calendar in SQL Server, you can simplify the process of maintaining holiday and working day information. This solution can be customized to fit the specific rules and requirements of different countries and regions. Now you can say goodbye to the headache of manually updating holiday dates every year!

Click to rate this post!
[Total: 0 Average: 0]

Let's work together

Send us a message or book free introductory meeting with us using button below.