SQL Nth Day of Nth Week of a Month

时间:2011-08-20 19:25:34

标签: sql tsql date

我想使用以下功能来安排基于每月的周和周的月度发生的俱乐部会议。在下面的示例中,我有一个(将)函数返回该月的第三个星期三。如果那一天发生在过去,那么它将返回下个月的第3个星期三。

我想摆脱循环,我觉得有更好的计算方法。是否有更多的OO流程?你的意见?

--CREATE FUNCTION NextWeekDayofMonth

DECLARE 
--(
        @WEEK INT,
        @WEEKDAY INT,
        @REFERENCEDATE DATETIME
--) 
--RETURNS DATETIME
--AS

-------------------------------
--Values for testing - Third Wednesday of the Month
set @WEEK = 3   --Third Week
set @WEEKDAY = 4    --Wednesday
set @REFERENCEDATE = '08/20/2011'
-------------------------------

BEGIN

    DECLARE @WEEKSEARCH INT
    DECLARE @FDOM DATETIME
    DECLARE @RETURNDATE DATETIME
    SET @FDOM = DATEADD(M,DATEDIFF(M,0,@REFERENCEDATE),0)
    SET @RETURNDATE = DATEADD(M,0,@FDOM)


    WHILE (@RETURNDATE < @REFERENCEDATE)
    --If the calculated date occurs in the past then it 
    --finds the appropriate date in the next month
    BEGIN

    SET @WEEKSEARCH = 1
    SET @RETURNDATE = @FDOM

    --Finds the first weekday of the month that matches the provided weekday value
        WHILE ( DATEPART(DW,@RETURNDATE) <> @WEEKDAY)
            BEGIN
            SET @RETURNDATE = DATEADD(D,1,@RETURNDATE)
            END

    --Iterates through the weeks without going into next month
        WHILE @WEEKSEARCH < @WEEK
            BEGIN
            IF MONTH(DATEADD(WK,1,@RETURNDATE)) = MONTH(@FDOM) 
                BEGIN
                    SET @RETURNDATE = DATEADD(WK,1,@RETURNDATE)
                    SET @WEEKSEARCH = @WEEKSEARCH+1
                END 
                ELSE
                    BREAK
            END
        SET @FDOM = DATEADD(M,1,@FDOM)
    END

    --RETURN @RETURNDATE
    select @ReturnDate
    END

3 个答案:

答案 0 :(得分:2)

IMO,最好的流程是将重要的业务信息存储为数据库中表中的行。如果您构建日历表,则可以通过简单查询获取所有第三个星期三。查询不仅简单,而且可以看出它们显然是正确的

select cal_date 
from calendar
where day_of_week_ordinal = 3
  and day_of_week = 'Wed';

今天或之后的第三个星期三也很简单。

select min(cal_date)
from calendar
where day_of_week_ordinal = 3
  and day_of_week = 'Wed'
  and cal_date >= CURRENT_DATE;

创建日历表非常简单。这是为PostgreSQL编写的,但它完全是标准的SQL(我认为),除了与ISO年份和ISO周相关的列。

create table calendar (
  cal_date date primary key,
  year_of_date integer not null 
    check (year_of_date = extract(year from cal_date)),
  month_of_year integer not null 
    check (month_of_year = extract(month from cal_date)),
  day_of_month integer not null 
    check (day_of_month = extract(day from cal_date)),
  day_of_week char(3) not null 
    check (day_of_week = 
    case when extract(dow from cal_date) = 0 then 'Sun'
         when extract(dow from cal_date) = 1 then 'Mon'
         when extract(dow from cal_date) = 2 then 'Tue'
         when extract(dow from cal_date) = 3 then 'Wed'
         when extract(dow from cal_date) = 4 then 'Thu'
         when extract(dow from cal_date) = 5 then 'Fri'
         when extract(dow from cal_date) = 6 then 'Sat'
    end),
  day_of_week_ordinal integer not null
    check (day_of_week_ordinal = 
      case
        when day_of_month >= 1 and day_of_month <= 7 then 1
        when day_of_month >= 8 and day_of_month <= 14 then 2
        when day_of_month >= 15 and day_of_month <= 21 then 3
        when day_of_month >= 22 and day_of_month <= 28 then 4
        else 5
      end),
  iso_year integer not null 
    check (iso_year = extract(isoyear from cal_date)),
  iso_week integer not null 
    check (iso_week = extract(week from cal_date))
);

您可以使用电子表格或UDF填充该表格。电子表格通常具有相当不错的日期和时间功能。我有一个UDF,但它是为PostgreSQL(PL / PGSQL)编写的,所以我不确定它对你有多大帮助。但如果你愿意,我会稍后发布。

答案 1 :(得分:0)

这是一种日期数学方法,可以在不循环的情况下完成您想要的任务:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Description: Gets the nth occurrence of a given weekday in the month containing the specified date.
-- For @dayOfWeek, 1 = Sunday, 2 = Monday, 3 = Tuesday, 4 = Wednesday, 5 = Thursday, 6 = Friday, 7 = Saturday
-- =============================================
CREATE FUNCTION GetWeekdayInMonth 
(
    @date datetime,
    @dayOfWeek int,
    @nthWeekdayInMonth int
)
RETURNS datetime
AS
BEGIN
    DECLARE @beginMonth datetime
    DECLARE @offSet int
    DECLARE @firstWeekdayOfMonth datetime
    DECLARE @result datetime

    SET @beginMonth = DATEADD(DAY, -DATEPART(DAY, @date) + 1, @date)
    SET @offSet = @dayOfWeek - DATEPART(dw, @beginMonth)

    IF (@offSet < 0)
    BEGIN
        SET @firstWeekdayOfMonth = DATEADD(d, 7 + @offSet, @beginMonth)
    END
    ELSE
    BEGIN
        SET @firstWeekdayOfMonth = DATEADD(d, @offSet, @beginMonth)
    END

    SET @result = DATEADD(WEEK, @nthWeekdayInMonth - 1, @firstWeekdayOfMonth)

    IF (NOT(MONTH(@beginMonth) = MONTH(@result)))
    BEGIN
        SET @result = NULL
    END

    RETURN @result
END
GO

DECLARE @nextMeetingDate datetime

SET @nextMeetingDate = dbo.GetWeekdayInMonth(GETDATE(), 4, 3)
IF (@nextMeetingDate IS NULL OR @nextMeetingDate < GETDATE())
BEGIN
    SET @nextMeetingDate = dbo.GetWeekDayInMonth(DATEADD(MONTH, 1, GETDATE()), 4, 3)
END

SELECT @nextMeetingDate 

答案 2 :(得分:0)

这是另一个基于函数的解决方案,它使用日期数学,在给定日期或之后返回下一个工作日。它没有使用任何循环来说明,但如果下一个第N个工作日在下个月,它最多可以重复一次迭代。

此功能将DATEFIRST设置考虑在使用非默认值的环境中。

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      David Grimberg
-- Create date: 2015-06-18
-- Description: Gets the next Nth weekday
-- @param Date is any date in a month
-- @param DayOfWeek is the weekday of interest ranging
--            from 1 to 7 with @@DATEFIRST being the
--            first day of the week
-- @param NthWeekday represents which ordinal weekday to return.
--            Positive values return dates relative to the start
--            of the month.  Negative values return dates relative
--            to the end of the month.  Values > 4 indicate the 
--            last week, values < -4 indicate the first week.
--            Zero is assumed to be 1.
-- =============================================
ALTER FUNCTION dbo.xxGetNextNthWeekday
(
  @Date       date,
  @NthWeekday smallint,
  @DayOfWeek  tinyint
)
RETURNS date
AS
BEGIN
  DECLARE @FirstOfMonth date
  DECLARE @inc int
    DECLARE @Result date

  -- Clamp the @NthWeekday input to valid values
  set @NthWeekday = case when @NthWeekday = 0 then 1
                         when @NthWeekday > 4 then -1
                         when @NthWeekday < -4 then 1
                         else @NthWeekday
                    end

  -- Normalize the requested day of week taking
  -- @@DATEFIRST into consideration.
    set @DayOfWeek = (@@DATEFIRST + 6 + @DayOfWeek) % 7 + 1

  -- Gets the first of the current month or the
  -- next month if @NthWeekday is negative.
  set @FirstOfMonth = dateadd(month, datediff(month,0,@Date)
                                   + case when @NthWeekday < 0 then 1 else 0 end
                                   , 0)

  -- Add and/or subtract 1 week depending direction of search and the
  -- relationship of @FirstOfMonth's Day of the Week to the @DayOfWeek
  -- of interest
  set @inc = case when (datepart(WEEKDAY, @FirstOfMonth)+@@DATEFIRST-1)%7+1 > @DayOfWeek
                  then 0 
                  else -1
             end
           + case when @NthWeekday < 0 then 1 else 0 end

  -- Put it all together
  set @Result = dateadd( day
                       , @DayOfWeek-1
                       , dateadd( WEEK
                                , @NthWeekday + @inc
                                , dateadd( WEEK -- Gets 1st Sunday on or
                                         , datediff(WEEK, -1, @FirstOfMonth)
                                         ,-1 ) ) )  -- before @FirstOfMonth
  -- [Snip here] --
  if @Result < @Date 
    set @Result = dbo.xxGetNextNthWeekday( dateadd(month, datediff(month, 0, @Date)+1, 0)
                                         , @NthWeekday, @DayOfWeek)
  -- [to here for no recursion] --

  Return @Result
END

如果您希望基于@Date参数而不是下一个工作日的当前月份的过去或将来的第N个工作日,则会删除上面提到的递归部分。