Determinar el tercer viernes de cada mes


16

Necesito determinar las fechas que son el "3er viernes de cada mes" para un rango de fechas de "1.1.1996 - 30.8.2014" en SQL Server.

Espero que deba usar una combinación de DENSE_RANK()y PARTITION BY()establecer "rango = 3". Sin embargo, soy nuevo en SQL y no puedo encontrar el código correcto.

Respuestas:


26

Dado:

  • El viernes se llama "viernes"
  • El tercer viernes del mes siempre será del 15 al 21 del mes.

    select thedate
    from yourtable
    where datename(weekday, thedate) = 'Friday'
    and datepart(day, thedate)>=15 and datepart(day, thedate)<=21;

También puede usar weekdaycon datepart(), pero es más legible con un nombre IMO. Sin embargo, las comparaciones de cadenas obviamente serán más lentas.


14

Para obtener una respuesta independiente de idioma / cultura, debe tener en cuenta los diferentes nombres de los días de semana y el comienzo de la semana.

En Italia, el viernes es "Venerdì" y el primer día de la semana es lunes, no domingo como en Estados Unidos.

1900-01-01 era un lunes, por lo que podemos usar esta información para calcular el día de la semana de forma independiente de la localidad:

WITH dates AS (
    SELECT DATEADD(day, number, GETDATE()) AS theDate
    FROM master.dbo.spt_values
    WHERE type = 'P'
)
SELECT theDate, DATENAME(dw, theDate), DATEPART(dw, theDate)
FROM dates
WHERE DATEDIFF(day, '19000101', theDate) % 7 = 4
    AND DATEPART(day, thedate)>=15 and DATEPART(day, thedate)<=21;

12

Otra forma, que usa la respuesta de Phil como base, y se ocupa de diferentes configuraciones:

select thedate
from yourtable
where (datepart(weekday, thedate) + @@DATEFIRST - 2) % 7 + 1 = 5   -- 5 -> Friday
  and (datepart(day, thedate) - 1) / 7 + 1 = 3 ;                   -- 3 -> 3rd week

El 5código (si desea un día de la semana que no sea viernes) debe ser (igual que los SET DATEFIRSTcódigos):

1 for Monday
2 for Tuesday
3 for Wednesday
4 for Thursday
5 for Friday
6 for Saturday
7 for Sunday

También puede usar una fecha "conocida buena" para estar seguro frente a la configuración de idioma. Por ejemplo, si busca viernes, consulte un calendario y vea que el 2 de enero de 2015 era viernes. La primera comparación podría entonces escribirse como:

DATEPART(weekday,thedate) = DATEPART(weekday,'20150102') --Any Friday

Vea también Cómo obtener el enésimo día de la semana de un mes por Peter Larsson.


4

De hecho, escribí un artículo sobre este tipo de cálculo aquí

Básicamente, puede usar el siguiente código para encontrar el tercer viernes de cada mes en cualquier rango de fechas

USE TEMPDB
set nocount on;
IF OBJECT_ID('dbo.#t') is not null 
 DROP TABLE dbo.#t;
CREATE TABLE #t ([Date] datetime,
  [Year] smallint, [Quarter] tinyint, [Month] tinyint
, [Day] smallint -- from 1 to 366 = 1st to 366th day in a year
, [Week] tinyint -- from 1 to 54 = the 1st to 54th week in a year; 
, [Monthly_week] tinyint -- 1/2/3/4/5=1st/2nd/3rd/4th/5th week in a month
, [Week_day] tinyint -- 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat, 7=Sun
);
GO
USE TEMPDB
-- populate the table #t, and the day of week is defined as
-- 1=Mon, 2=Tue, 3=Wed, 4=Thu,5=Fri, 6=Sat, 7=Sun
;WITH   C0   AS (SELECT c FROM (VALUES(1),(1)) AS D(c)),
  C1   AS (SELECT 1 AS c FROM C0 AS A CROSS JOIN C0 AS B),
  C2   AS (SELECT 1 AS c FROM C1 AS A CROSS JOIN C1 AS B),
  C3   AS (SELECT 1 AS c FROM C2 AS A CROSS JOIN C2 AS B),
  C4   AS (SELECT 1 AS c FROM C3 AS A CROSS JOIN C3 AS B), 
  C5   AS (SELECT 1 AS c FROM C4 AS A CROSS JOIN C3 AS B),
  C6   AS (select rn=row_number() over (order by c)  from C5),
  C7   as (select [date]=dateadd(day, rn-1, '19000101') FROM C6 WHERE rn <= datediff(day, '19000101', '99991231')+1)

INSERT INTO #t ([year], [quarter], [month], [week], [day], [monthly_week], [week_day], [date])
SELECT datepart(yy, [DATE]), datepart(qq, [date]), datepart(mm, [date]), datepart(wk, [date])
     , datediff(day, dateadd(year, datediff(year, 0, [date]), 0), [date])+1
  , datepart(week, [date]) -datepart(week, dateadd(month, datediff(month, 0, [date]) , 0))+1
  , CASE WHEN datepart(dw, [date])+@@datefirst-1 > 7 THEN (datepart(dw, [date])+@@datefirst-1)%7
         ELSE datepart(dw, [date])+@@datefirst-1 END
 , [date]
FROM C7
    --where [date] between '19900101' and '20990101'; -- if you want to populate a range of dates
GO

select convert(char(10), [Date], 120) 
from #t
where Monthly_week=3
and week_day=5
and [date] between '2015-01-01' and '2015-12-31' -- change to your own date range

2

Sí, sé que esta es una publicación anterior. Pensé que daría una inclinación diferente a las cosas a pesar de su edad. Je ... y mis disculpas. Me acabo de dar cuenta de que casi he duplicado lo que @jyao publicó anteriormente.

Basado en la edición actual de la pregunta original del OP, no pude entender por qué las personas publicaron las respuestas que hicieron.

Al revisar las ediciones, encontré la pregunta original y la publiqué a continuación ...

Tengo una serie temporal que va del 1.1.1996 al 30.8.2014 en una base de datos SQL, por ejemplo, con la tabla "db.dbo.datestable".

Necesito determinar las fechas que son el "3er viernes de cada mes" para este rango de fechas en SQL.

Espero usar una combinación de "DENSE_RANK ()" y "PARTITION BY ()" para establecer "rank = 3". Sin embargo, soy nuevo en SQL y no puedo encontrar el código correcto.

¿Puedes resolver este problema?

La parte de la pregunta original que he enfatizado en negrita parece ser la clave. Ciertamente podría ser incorrecto, pero me parece que el OP decía que tiene una tabla "Calendario" llamada "dbo.datestable" y, para mí, eso hace una gran diferencia y ahora entiendo por qué muchas de las respuestas son lo que están incluyendo el que generó las fechas porque fue publicado el 10 de noviembre ... un día después de la edición final de la pregunta, que eliminó los vestigios finales de la referencia al "dbo.datestable".

Como dije, podría estar equivocado, pero aquí está mi interpretación de la pregunta original.

Tengo una tabla de "Calendario" llamada "dbo.datestable". Dado cualquier rango de fechas cubierto por esa tabla, ¿cómo puedo devolver solo las fechas que son el tercer viernes de cada mes dentro de ese rango de fechas?

Como los métodos convencionales para hacer esto ya han sido cubiertos, agregaré una alternativa que podría ser útil para algunos.

Simulemos un par de columnas que creo que el OP ya tendrá en su tabla. Por supuesto, estoy adivinando los nombres de las columnas. Subtique las columnas equivalentes para su tabla "Calendario". Además, estoy haciendo todo esto en TempDB para no tener la posibilidad de interferir con la tabla "Calendario" real de alguien.

--=================================================================================================
--      Simulate just a couple of the necessary columns of the OPs "Calendar" table.
--      This is not a part of the solution.  We're just trying to simulate what the OP has.
--=================================================================================================
--===== Variables to control the dates that will appear in the "Calendar" table.
DECLARE  @StartDT   DATETIME
        ,@EndDT     DATETIME
;
 SELECT  @StartDT = '1900' --Will be inclusive start of this year in calculations.
        ,@EndDT   = '2100' --Will be exclusive start of this year in calculations.
;
--===== Create the "Calendar" table with just enough columns to simulate the OP's.
 CREATE TABLE #datestable
        (
         TheDate    DATETIME NOT NULL
        ,DW         TINYINT  NOT NULL  --SQL standard abbreviate of "Day of Week"
        )
;
--===== Populate the "Calendar" table (uses "Minimal Logging" in 2008+ this case).    
   WITH cteGenDates AS
(
 SELECT TOP (DATEDIFF(dd,@StartDT,@EndDT)) --You can use "DAY" instead of "dd" if you prefer. I don't like it, though.
        TheDate = DATEADD(dd, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))-1, @StartDT)
   FROM      sys.all_columns ac1
  CROSS JOIN sys.all_columns ac2
)
 INSERT INTO #datestable WITH (TABLOCK)
 SELECT  TheDate
        ,DW = DATEDIFF(dd,0,TheDate)%7+1 --Monday is 1, Friday is 5, Sunday is 7 etc.
   FROM cteGenDates
 OPTION (RECOMPILE) -- Help keep "Minimal Logging" in the presence of variables.
;
--===== Add the expected named PK for this example.
  ALTER TABLE #datestable 
    ADD CONSTRAINT PK_datestable PRIMARY KEY CLUSTERED (TheDate)
;

También es un hecho que no sé si el OP puede hacer cambios en su tabla "Calendario", por lo que esto podría no ayudarlo, pero puede ayudar a otros. Con eso en mente, agreguemos una columna DWoM (Día de la semana para el mes). Si no te gusta el nombre, no dude en cambiarlo a lo que usted necesita en su propia caja.

--===== Add the new column.
  ALTER TABLE #datestable
    ADD DWOM TINYINT NOT NULL DEFAULT (0)
;

A continuación, debemos completar la nueva columna. El OP tuvo una idea de esto en su publicación original no adulterada.

--===== Populate the new column using the CTE trick for updates so that
     -- we can use a Windowing Function in an UPDATE.
   WITH cteGenDWOM AS
(
 SELECT DW# = ROW_NUMBER() OVER (PARTITION BY DATEDIFF(mm,0,TheDate), DW
                                     ORDER BY TheDate)
        ,DWOM
   FROM #datestable
)
 UPDATE cteGenDWOM
    SET DWOM = DW#
;

Ahora, debido a que es una columna de longitud fija, que acaba de crear un montón de divisiones de página, por lo que debemos reconstruir el Índice agrupado para "volver a empaquetar" la tabla para tener tantas filas por página como sea posible en aras del rendimiento.

--===== "Repack" the Clustered Index to get rid of the page splits we 
     -- caused by adding the new column.
  ALTER INDEX PK_datestable
     ON #datestable
        REBUILD WITH (FILLFACTOR = 100, SORT_IN_TEMPDB = ON)
;

Una vez hecho esto, las consultas que hacen cosas como regresar el tercer viernes de cada mes en un rango de fechas dado se vuelven triviales y bastante obvias de leer.

--===== Return the 3rd Friday of every month included in the given date range.
 SELECT *
   FROM #datestable
  WHERE TheDate >= '1996-01-01' --I never use "BETWEEN" for dates out of habit for end date offsets.
    AND TheDate <= '2014-08-30'
    AND DW      =  5 --Friday
    AND DWOM    =  3 --The 3rd one for every month
  ORDER BY TheDate
;

0

Aquí es un simple corte y pegue solución. Puede convertir esto en una función si lo desea.

Declare @CurrDate Date
Set @CurrDate = '11-20-2016'

declare @first datetime -- First of the month of interest (no time part)
declare @nth tinyint -- Which of them - 1st, 2nd, etc.
declare @dow tinyint -- Day of week we want
set @first = DATEFROMPARTS(YEAR(@CurrDate), MONTH(@CurrDate), 1) 
set @nth = 3
set @dow = 6
declare @result datetime
set @result = @first + 7*(@nth-1)
select  @result + (7 + @dow - datepart(weekday,@result))%7
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.