Problems such as calculating third Wednesday of the month or the last day of the month are very common. Recently Peter Larsson posted a scalar UDF that solves problems such as "third Wednesday of the month", and Uri Dimant added a comment with a solution by Steve Kass. Unfortunately, all these solutions use scalar UDFs and as such are very slow, so I decided to post some very fast inline UDFs that I find very useful.
Composing a date from year, month, and day without checking if the parameters are valid.
The following inline UDF composes a date. I deliberately intend to use it with invalid parameters, such as month equal to zero, because it dramatically simplifies my logic.
CREATE FUNCTION Readers.ComposeDate(@year INT, @month INT, @day INT)
-- I deliberately do not check if the parameters are valid
RETURNS TABLE AS RETURN(
SELECT DATEADD(DAY, @day - 1, DATEADD(MONTH, @month - 1, DATEADD(YEAR, @year - 1901, '19010101')))
AS ComposedDate
)
GO
Calculating the last days of previous and this months.
Because my ComposeDate function handles out of range parameters, I don't need to handle January and December differently. Here you go, create them and see how they work:
CREATE FUNCTION Readers.LastDayOfPreviousMonth(@year INT, @month INT)
RETURNS TABLE AS RETURN(
SELECT ComposedDate AS LastDayOfPreviousMonth
FROM Readers.ComposeDate(@year, @month, 0)
)
GO
SELECT LastDayOfPreviousMonth, '20081231' FROM Readers.LastDayOfPreviousMonth(2009,1)
UNION ALL
SELECT LastDayOfPreviousMonth, '20090131' FROM Readers.LastDayOfPreviousMonth(2009,2)
GO
CREATE FUNCTION Readers.LastDayOfMonth(@year INT, @month INT)
RETURNS TABLE AS RETURN(
SELECT ComposedDate AS LastDayOfMonth
FROM Readers.ComposeDate(@year, @month+1, 0)
)
GO
SELECT LastDayOfMonth FROM Readers.LastDayOfMonth(2009,11)
UNION ALL
SELECT LastDayOfMonth FROM Readers.LastDayOfMonth(2009,12)
GO
Calculating third Wednesday and other similar problems
For simplicity (this is a blog post, not production code), this function does not determine if the returned date is actually in the required month or in another one. If you need such validation, use the approach demonstrated in the next example.
CREATE FUNCTION Readers.MthDayOfNthWeek(@year INT, @month INT, @week INT, @day INT)
-- @day 1 Sun, 2 Mon etc.
--
this function works in US but might need adjustments in other countries
RETURNS TABLE AS RETURN(
SELECT t.ComposedDate AS MthDayOfNthWeek
FROM (SELECT ComposedDate
FROM Readers.ComposeDate(@year, @month, 1)) AS d
CROSS APPLY Readers.ComposeDate(@year, @month,
@day - DATEPART(dw,ComposedDate) + 1 +
7*(@week + CASE WHEN DATEPART(dw,ComposedDate)>@day THEN 0 ELSE -1 END) ) AS t
)
GO
-- third Wednesday of June 2009
SELECT MthDayOfNthWeek, '20090617' AS ExpectedDate
FROM Readers.MthDayOfNthWeek(2009, 6, 3, 4)
UNION ALL
SELECT MthDayOfNthWeek, '20090715' AS ExpectedDate
FROM Readers.MthDayOfNthWeek(2009, 7, 3, 4)
UNION ALL
SELECT MthDayOfNthWeek, '20090601' AS ExpectedDate
FROM Readers.MthDayOfNthWeek(2009, 6, 1, 2)
UNION ALL
SELECT MthDayOfNthWeek, '20090605' AS ExpectedDate
FROM Readers.MthDayOfNthWeek(2009, 6, 1, 6)
UNION ALL
SELECT MthDayOfNthWeek, '20090606' AS ExpectedDate
FROM Readers.MthDayOfNthWeek(2009, 6, 1, 7)
UNION ALL
SELECT MthDayOfNthWeek, '20090608' AS ExpectedDate
FROM Readers.MthDayOfNthWeek(2009, 6, 2, 2)
This function is not completely trivial, so I used test driven development to come up with it.
Validating dates
If you need to validate dates, use the following approach:
CREATE FUNCTION Readers.ComposeValidDate(@year INT, @month INT, @day INT)
RETURNS TABLE AS RETURN(
SELECT CASE WHEN YEAR(ComposedDate) = @year AND MONTH(ComposedDate) = @month
THEN ComposedDate END AS ComposedDate
FROM Readers.ComposeDate(@year, @month, @day)
)
GO
SELECT ComposedDate, '20090228' AS ExpectedResult FROM Readers.ComposeValidDate(2009, 2, 28)
UNION ALL
SELECT ComposedDate, NULL FROM Readers.ComposeValidDate(2009, 2, 29)
UNION ALL
SELECT ComposedDate, NULL FROM Readers.ComposeValidDate(2009, 2, 0)
UNION ALL
SELECT ComposedDate, NULL FROM Readers.ComposeValidDate(2009, 0, 12)
Only the first result will validate.
Read more about performance of inline UDFs in "Reuse Your Code with Table-Valued UDFs" and "Many nested inline UDFs are very fast"