Passing in Week Day name to get nearest date in SQ

2020-06-21 06:31发布

问题:

I'm working on a query that deals with a frequency value (i.e. Mondays, Tuesdays, etc. - Think assignments).

So in my query I currently have a result of

jobId:1, personId:100, frequencyVal: 'Mondays'
jobId:2, personId:101, frequencyVal: 'Saturdays'

What I need is the next the 4 future(or current) dates for the frequencyVal.

So if today is 1/3/2015

I would need my result set to be

jobId:1, personId:100, frequencyVal: 'Mondays', futureDates: '1/5,1/12,1/19,1/26'
jobId:2, personId:102, frequencyVal: 'Saturdays', futureDates: '1/3,1/10,1/17,1/24'

I was looking at the following post: How to find the Nearest (day of the week) for a given date

But that sets it for a specific date. And I'm looking at this being a web application and I want the dates for the current date. So if I try to run this query next Tuesday the future dates for jobId:1 would remove the 1/5 and add the 2/2.

Is there a way to pass in a weekday value to get the next nearest date?

回答1:

Your sample table

create table #t
(
    jobId int,
    personId int,
    frequencyVal varchar(10)
);

insert into #t values (1,100,'Mondays'),(2,101,'Saturdays');

QUERY 1 : Select nearest 4 week of days in current month for particular week day

-- Gets first day of month
DECLARE @FIRSTDAY DATE=DATEADD(month, DATEDIFF(month, 0, GETDATE()), 0)

;WITH  CTE as
(
     -- Will find all dates in current month
     SELECT CAST(@FIRSTDAY AS DATE) as DATES
     UNION ALL
     SELECT DATEADD(DAY,1,DATES)    
     FROM    CTE
     WHERE   DATES < DATEADD(MONTH,1,@FIRSTDAY)
 )
,CTE2 AS
(
   -- Join the #t table with  CTE on the datename+'s' 
   SELECT jobId,personId,frequencyVal,DATES,
   -- Get week difference for each weekday        
   DATEDIFF(WEEK,DATES,GETDATE()) WEEKDIFF,
   -- Count the number of weekdays in a month
   COUNT(DATES) OVER(PARTITION BY DATENAME(WEEKDAY,CTE.DATES)) WEEKCOUNT
   FROM CTE
   JOIN #t ON DATENAME(WEEKDAY,CTE.DATES)+'s' = #t.frequencyVal 
   WHERE MONTH(DATES)= MONTH(GETDATE())   
)
-- Converts to CSV and make sure that only nearest 4 week of days are generated for month
SELECT  DISTINCT C2.jobId,C2.personId,frequencyVal,
         SUBSTRING(
        (SELECT ', ' + CAST(DATEPART(MONTH,DATES) AS VARCHAR(2)) + '/'  + 
                       CAST(DATEPART(DAY,DATES) AS VARCHAR(2))
        FROM CTE2 
        WHERE C2.jobId=jobId AND C2.personId=personId AND C2.frequencyVal=frequencyVal AND
                       ((WEEKDIFF<3 AND WEEKDIFF>-3 AND WEEKCOUNT = 5) OR WEEKCOUNT <= 4)
        ORDER BY CTE2.DATES
        FOR XML PATH('')),2,200000) futureDates
FROM CTE2 C2
  • SQL FIDDLE

For example, in Query2 the nearest date(here we take example as Saturday) of

2015-Jan-10 will be 01/03,01/10,01/17,01/24
2015-Jan-24 will be 01/10,01/17,01/24,01/31

QUERY 2 : Select next 4 week's dates for particular week day irrelevant of month

;WITH  CTE as
(
     -- Will find the next 4 week details
     SELECT CAST(GETDATE() AS DATE) as DATES
     UNION ALL
     SELECT DATEADD(DAY,1,DATES)    
     FROM    CTE
     WHERE   DATES < DATEADD(DAY,28,GETDATE())
 )
,CTE2 AS
(
   -- Join the #t table with  CTE on the datename+'s' 
   SELECT jobId,personId,frequencyVal, DATES,
   ROW_NUMBER() OVER(PARTITION BY DATENAME(WEEKDAY,CTE.DATES) ORDER BY CTE.DATES) DATECNT
   FROM CTE
   JOIN #t ON DATENAME(WEEKDAY,CTE.DATES)+'s' = #t.frequencyVal  
)
-- Converts to CSV and make sure that only 4 days are generated for month
SELECT  DISTINCT C2.jobId,C2.personId,frequencyVal,   
        SUBSTRING(
        (SELECT ', ' + CAST(DATEPART(MONTH,DATES) AS VARCHAR(2)) + '/'  + 
                       CAST(DATEPART(DAY,DATES) AS VARCHAR(2))
        FROM CTE2 
        WHERE C2.jobId=jobId AND C2.personId=personId AND C2.frequencyVal=frequencyVal 
              AND DATECNT < 5
        ORDER BY CTE2.DATES
        FOR XML PATH('')),2,200000) futureDates
        FROM CTE2 C2
  • SQL FIDDLE

The following would be the output if the GETDATE() (if its Saturday) is

2015-01-05 - 1/10, 1/17, 1/24, 1/31
2015-01-24 - 1/24, 1/31, 2/7, 2/14


回答2:

I prefer a calendar table for this kind of query. Actually, I prefer a calendar table over date functions for most queries. Here's a minimal one. The one I use in production has more columns and more rows. (100 years of data is only 37k rows.)

create table calendar (
  cal_date date not null primary key,
  day_of_week varchar(15)
  );

insert into calendar (cal_date) values 
('2015-01-01'), ('2015-01-02'), ('2015-01-03'), ('2015-01-04'),
('2015-01-05'), ('2015-01-06'), ('2015-01-07'), ('2015-01-08'),
('2015-01-09'), ('2015-01-10'), ('2015-01-11'), ('2015-01-12'),
('2015-01-13'), ('2015-01-14'), ('2015-01-15'), ('2015-01-16'),
('2015-01-17'), ('2015-01-18'), ('2015-01-19'), ('2015-01-20'),
('2015-01-21'), ('2015-01-22'), ('2015-01-23'), ('2015-01-24'),
('2015-01-25'), ('2015-01-26'), ('2015-01-27'), ('2015-01-28'),
('2015-01-29'), ('2015-01-30'), ('2015-01-31'),

('2015-02-01'), ('2015-02-02'), ('2015-02-03'), ('2015-02-04'),
('2015-02-05'), ('2015-02-06'), ('2015-02-07'), ('2015-02-08'),
('2015-02-09'), ('2015-02-10'), ('2015-02-11'), ('2015-02-12'),
('2015-02-13'), ('2015-02-14'), ('2015-02-15'), ('2015-02-16'),
('2015-02-17'), ('2015-02-18'), ('2015-02-19'), ('2015-02-20'),
('2015-02-21'), ('2015-02-22'), ('2015-02-23'), ('2015-02-24'),
('2015-02-25'), ('2015-02-26'), ('2015-02-27'), ('2015-02-28')
;

update calendar
set day_of_week = datename(weekday, cal_date);

alter table calendar 
alter column day_of_week varchar(15) not null;

alter table calendar
add constraint cal_date_matches_dow
check (datename(weekday, cal_date) = day_of_week);

create index day_of_week_ix on calendar (day_of_week);

Set the privileges so that

  • everyone can select, but
  • almost nobody can insert new rows, and
  • even fewer people can delete rows.

(Or write a constraint that can guarantee there are no gaps. I think you can do that in SQL Server.)

You can select the next four Mondays after today with a very simple SQL statement. (The current date is 2015-01-05, which is a Monday.)

select top 4 cal_date
from calendar
where cal_date > convert(date, getdate())
  and day_of_week = 'Monday'
order by cal_date;
CAL_DATE
--
2015-01-12
2015-01-19
2015-01-26
2015-02-02

For me, this is a huge advantage. No procedural code. Simple SQL that is obviously right. Big win.



回答3:

There's no built-in function to do it. But you can try this, you may place it inside a Scalar-Valued Function:

DECLARE @WeekDay VARCHAR(10) = 'Monday';
DECLARE @WeekDayInt INT;

SELECT @WeekDayInt = CASE @WeekDay
                     WHEN 'SUNDAY'    THEN 1 
                     WHEN 'MONDAY'    THEN 2 
                     WHEN 'TUESDAY'   THEN 3 
                     WHEN 'WEDNESDAY' THEN 4 
                     WHEN 'THURSDAY'  THEN 5 
                     WHEN 'FRIDAY'    THEN 6
                     WHEN 'SATURDAY'  THEN 7 END

SELECT CONVERT(DATE, DATEADD(DAY, (DATEPART(WEEKDAY, GETDATE()) + @WeekDayInt) % 7, GETDATE())) AS NearestDate

UPDATE:

Looks like radar was right, here's the solution:

DECLARE @WeekDay    VARCHAR(10) = 'Monday';
DECLARE @WeekDayInt INT;
DECLARE @Date       DATETIME = GETDATE();

SELECT @WeekDayInt = CASE @WeekDay
                     WHEN 'SUNDAY'    THEN 1 
                     WHEN 'MONDAY'    THEN 2 
                     WHEN 'TUESDAY'   THEN 3 
                     WHEN 'WEDNESDAY' THEN 4 
                     WHEN 'THURSDAY'  THEN 5 
                     WHEN 'FRIDAY'    THEN 6
                     WHEN 'SATURDAY'  THEN 7 END
DECLARE @Diff INT = DATEPART(WEEKDAY, @Date) - @WeekDayInt;
SELECT CONVERT(DATE, DATEADD(DAY, CASE WHEN @Diff >= 0 THEN 7 - @Diff ELSE ABS(@Diff) END, @Date)) AS NearestDate


回答4:

Try this - based on king.code's answer to get the nearest date.

create table #t
(
    jobId int,
    personId int,
    frequencyVal varchar(10)
);

insert into #t values (1,100,'Mondays'),(2,101,'Saturdays');

WITH cte(n) AS
(
    SELECT 0
    UNION ALL
    SELECT n+1 FROM cte WHERE n < 3
)

select #t.jobId, #t.personId, #t.frequencyVal, STUFF(a.d, 1, 1, '') AS FutureDates
from #t
cross apply (SELECT CASE #t.frequencyVal
                         WHEN 'SUNDAYS'    THEN 1 
                         WHEN 'MONDAYS'    THEN 2 
                         WHEN 'TUESDAYS'   THEN 3 
                         WHEN 'WEDNESDAYS' THEN 4 
                         WHEN 'THURSDAYS'  THEN 5 
                         WHEN 'FRIDAYS'    THEN 6
                         WHEN 'SATURDAYS'  THEN 7 
                    END)tranlationWeekdays(n)
cross apply (select ',' +  CONVERT(varchar(10),  CONVERT(date,dateadd(WEEK, cte.n,CONVERT(DATE, DATEADD(DAY, (DATEPART(WEEKDAY, GETDATE()) + tranlationWeekdays.n) % 7, GETDATE()))))) from cte FOR XML PATH('')) a(d);

drop table #t;


回答5:

Try this,

DECLARE @YEAR INT=2015
DECLARE @MONTH INT=1
DECLARE @DAY INT=1
DECLARE @DATE DATE = (SELECT DateFromParts(@Year, @Month, @Day))
DECLARE @TOTAL_DAYS INT =(SELECT DatePart(DY, @DATE));

WITH CTE1
     AS (SELECT T_DAY=(SELECT DateName(DW, @DATE)),
                @DATE AS T_DATE,
                @DAY  AS T_DDAY
         UNION ALL
         SELECT T_DAY=(SELECT DateName(DW, DateAdd(DAY, T_DDAY + 1, @DATE))),
                DateAdd(DAY, T_DDAY + 1, @DATE) AS T_DATE,
                T_DDAY + 1
         FROM   CTE1
         WHERE  T_DDAY + 1 <= 364)
SELECT DISTINCT T_DAY,
       Stuff((SELECT ',' + CONVERT(VARCHAR(30), T_DATE)
              FROM   CTE1 A
              WHERE  A.T_DAY=CTE1.T_DAY AND A.T_DATE > GetDate() AND A.T_DATE<(DATEADD(WEEK,4,GETDATE())) 
              FOR XML PATH('')), 1, 1, '') AS FUTURE
FROM   CTE1
ORDER  BY T_DAY
OPTION (MAXRECURSION 365) 


回答6:

This is a simpler way I think, and I think it fits your requirements.

Note that I have changed your frequency_val column to an integer that represents the day of the week from SQL servers perspective and added a calculated column to illustrate how you can easily derive the day name from that.


declare @t table
(
    jobId int,
    personId int,
    --frequencyVal varchar(10)
    frequency_val int,
    frequency_day as datename(weekday,frequency_val -1) + 's'
);

declare @num_occurances int = 4 declare @from_date date = dateadd(dd,3,getdate()) -- this will allow you to play with the date simply by changing the increment value

insert into @t values (1,100,1),--'Mondays'), (2,101,6),--'Saturdays'); (3,101,7),--'Saturdays'); (4,100,2)--'Mondays'), --select * from @t

;with r_cte (days_ahead, occurance_date) as (select 0, convert(date,@from_date,121) union all select r_cte.days_ahead +1, convert(date,dateadd(DD, r_cte.days_ahead+1, @from_date),121) from r_cte where r_cte.days_ahead < 7 * @num_occurances ) select t.*, r_cte.occurance_date from @t t inner join r_cte on DATEPART(WEEKDAY, dateadd(dd,@@DATEFIRST - 1 ,r_cte.occurance_date)) = t.frequency_val



回答7:

Having seen the use of DATENAME in some of the answers already given, I'd like to point out that return values of DATENAME might vary depending on your current language setting, but you can save the current language setting and ensure usage of us_english so you can be confident to use English weekday names.

Now here is my slightly different approach to get the 4 next dates that fall on a certain (known) weekday, using a user defined table valued function that allows to create a number sequence table (yes this is a pretty dull function, you have to pass MaxValue greater MinValue, but that could be easily enhanced, if needed, but hey, it does the job). Using that function span a table over 28 values (next 28 days should indeed include the next 4 relevant weekdays ;)), apply DATEADD on GETDATE and reduce the result set with WHERE to only those values that have the right weekday:

CREATE FUNCTION GetIntSequence(@MinValue INT, @MaxValue INT)
RETURNS @retSequence TABLE
(
    IntValue INT NOT NULL
)
BEGIN
    DECLARE @i INT = (SELECT @MinValue)

    WHILE @i <= @MaxValue
    BEGIN
        INSERT INTO @retSequence (IntValue) SELECT @i

        SELECT @i = @i + 1
    END

    RETURN
END

GO
DECLARE @weekDay NVARCHAR(MAX) = 'Monday'   --(or Tuesday, wednesday, ...)

--save current language setting
DECLARE @languageBackup NVARCHAR(MAX) = (SELECT @@LANGUAGE)
--ensure us english language setting for reliable weekday names
SET LANGUAGE us_english;


SELECT FourWeeks.SomeDay FROM
    (
        SELECT
                DATEADD(DAY, IntValue, GETDATE()) AS SomeDay
            FROM dbo.GetIntSequence(1, 28)
    ) AS FourWeeks
    WHERE DATENAME(WEEKDAY, SomeDay) = @weekDay

--restore old language setting
SET LANGUAGE @languageBackup;

GO

DROP FUNCTION dbo.GetIntSequence