简体   繁体   中英

Get nth weekday from XDate in SQL Server

I have to get date repeatedly for every N months.

I have XDate to Start from.

I want the nth week's mth weekday's date.

N is say 2 - I have to get for every 2 month

XDate is suppose tomorrow's date. So, Starting from tomorrow

m is 7 - So, get date of every Saturday

n is 2 - of second week.

I could not even think for start point for this complex logic.

Any suggestion how should I start - pseudo code

Thanks in advance,

First, this is where a calendar table comes in handy. The following code creates a table called calendar and populates it with dates starting in 2000. It also has a column called NthWeekdayInMonth. For example, if you look at the entries for 1/29/05 through 1/31/05 you'll see that this column is set to a 5 because those were the 5th Saturday, Sunday, and Monday of the month.

CREATE TABLE Calendar
(
    [Date] date NOT NULL,
    [NthWeekdayInMonth] int,
    CONSTRAINT PK_Calendar
        PRIMARY KEY CLUSTERED ([Date])
        WITH FILLFACTOR = 100
)


;WITH cte AS 
(
    SELECT
        DATEADD(d, (a.Number * 256) + b.Number, '01/01/2000') AS [Date]
    FROM 
        (
            SELECT number
            FROM master..spt_values
            WHERE 
                type = 'P'
                AND number <= 255
        ) a (Number),
        (
            SELECT number
            FROM master..spt_values
            WHERE 
                type = 'P'
                AND number <= 255
        ) b (Number)
)

INSERT INTO Calendar
SELECT 
    [Date], 
    ROW_NUMBER() OVER (PARTITION BY YEAR([Date]), MONTH([Date]), DATEPART(dw, [Date]) ORDER BY [Date]) FROM cte
ORDER BY 
    [Date]
GO

Now that we have a calendar table the rest is fairly straightforward. I did deviate from your design in one respect but you should be able to adjust it if needed. In my implementation, the starting date is literally the first date that should be returned. So a starting date of 1/11/2014, looking every 2 months would return:

2014-01-11 
2014-03-08 
2014-05-10 
2014-07-12

By passing the first date the code can figure out what day of the week it was and what week of the month. Passing those values in is redundant. The test code is below...

DECLARE @startDate date
DECLARE @everyNMonths int 
DECLARE @numResults int 
DECLARE @nthAppearanceOfDay int 

SET @startDate = '01/11/2014'   -- First occurence is on this date
SET @everyNMonths = 2           -- Skip every n months
SET @numResults = 4             -- Max # of results to return

-- Figure out which x-day of the month this is.  For example, if the starting 
-- date is 1/11/2014 that was the second Saturday so this will be set to 2.
SELECT @nthAppearanceOfDay = NthWeekdayInMonth FROM calendar WHERE [date] = @startDate

-- Use a CTE to get all the months involved in this calculation
;WITH candidateMonths AS (
    SELECT 
        1 AS [resultnum], @startDate AS [date]
    UNION ALL 
        SELECT resultnum + 1, DATEADD(month, @everyNMonths, [date]) FROM candidateMonths
            WHERE resultnum + 1 <= @numResults
)

-- Now evaluate every date for each of the candidate months.  If the day of week matches
-- that of the start date AND it is the Nth occurrence of that day of week in the month
-- include it
SELECT 
    c.[Date]
FROM 
    candidateMonths cm
    INNER JOIN calendar c ON ( (YEAR(c.[Date]) = YEAR(cm.[Date])) AND (MONTH(c.[Date]) = MONTH(cm.[Date])))
WHERE 
    (DATEPART(dw, c.[date]) = DATEPART(dw, @startDate)) -- Same day of week
    AND 
    (c.NthWeekdayInMonth = @nthAppearanceOfDay) -- Same week of month

I've been experimenting with the following code:

SELECT * FROM dbo.NthWeekday(GETDATE(), 1, 1);
SELECT * FROM dbo.NthWeekday(GETDATE(), 1, -1);

Where 1 is Sunday and 7 is Saturday regardless of the @@DATEFIRST setting. A positive value for n (or 0) will return the Next Nth Weekday while a negative value for n returns the Previous Nth Weekday .

I don't fully understand what you want but if I gathered correctly: just getting the Nth Weekday is not enough. You want to do this repeatedly for X months as well. This is the tentative code I'd use:

DECLARE @date DATE = GETDATE();
DECLARE @numMonths INT = -5
DECLARE @weekday INT = 1;
DECLARE @n INT = 2;

SELECT C.D
FROM dbo.RangeSmallInt(0, @numMonths - SIGN(@numMonths)) A
CROSS APPLY ( -- MonthBegin
    SELECT DT = DATEADD(m, DATEDIFF(m, 0, @date) + A.N, 0)
) B
CROSS APPLY dbo.NthWeekday(B.DT, @weekday, @n) C;

Results: 2014-12-14
         2015-01-11
         2015-02-08
         2015-03-08
         2015-04-12

Which you could wrap in a table-valued function much like I have done with NthWeekday and RangeSmallInt. The RangeSmallInt function call can be replaced with a numbers table, tally CTE, or whatever terminology/style you're comfortable with.

How it works:

We start by generating a set of numbers beginning with 0 because we want the function to be inclusive. (@numMonths - SIGN(@numMonths)) handles the addition or subtraction of 1/0 from @numMonths based on the sign of @numMonths . This ensures that the proper range of integers (in the above case: 0 through -4) are generated for our next trick.

Once we have a range of integers to work with we can use them to offset the date. In this case we want to find out the beginning of the month for X months. If we had a function that could return the Nth Month Begin Date then we would simply pass the integers we already have to the function and get out the dates we want. So that's exactly what we do using CROSS APPLY.

Now that we have the beginning of the month for X months solved all we need to do is apply our NthWeekday function to these dates.

Nth Weekday:

CREATE FUNCTION dbo.NthWeekday ( 
    @date DATE = NULL  
  , @weekday INT = NULL
  , @n INT = 1
)
RETURNS TABLE   
AS   
RETURN (
    SELECT D = CASE SIGN(@n)
                    WHEN -1 THEN DATEADD(d, -(DATEPART(dw, @date) + @@DATEFIRST - @weekday) % 7 + ((@n + 1) * 7), @date)                
                    ELSE DATEADD(d, (@weekday - DATEPART(dw, @date) + @@DATEFIRST) % 7 + ((@n - SIGN(@n)) * 7), @date)
               END
);

RangeSmallInt:

-- Generate a range of up to 65,536 contiguous BIGINTS
CREATE FUNCTION dbo.RangeSmallInt (
    @num1 BIGINT = NULL
  , @num2 BIGINT = NULL
)
RETURNS TABLE
AS
RETURN (
    WITH Numbers(N) AS (
        SELECT N FROM(VALUES
            (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 16
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 32
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 48
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 64
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 80
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 96
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 112
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 128
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 144
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 160
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 176
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 192
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 208
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 224
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 240
          , (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 256
        ) V (N)
    )    
    SELECT TOP (
               CASE
                   WHEN @num1 IS NOT NULL AND @num2 IS NOT NULL THEN ABS(@num1 - @num2) + 1
                   ELSE 0
               END
           )
           N = ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) + CASE WHEN @num1 <= @num2 THEN @num1 ELSE @num2 END - 1
    FROM Numbers A
       , Numbers B
    WHERE ABS(@num1 - @num2) + 1 < 65537
);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM