Last Wednesday of every month - sql

I need to work out the last Wednesday for each month in a range of dates.
I have the code for calculating the last Wednesday, but my cte doesn't seem to step through the months correctly.
Current code:
declare #S_Date date
declare #E_Date date
set #S_Date='2016-01-31' --eomonth of first month
set #E_Date='2016-06-15' --ides of last month ( can actually be any day that is
--not equal to or after the last Wednesday of the month)
;with LW(D_Date) as
(
select dateadd(dd,datediff(dd,0,#S_Date)/7*7+2,0)
union all
select dateadd(dd,datediff(dd,0,eomonth(D_Date,1))/7*7+2,0)
from LW
where D_date<#E_Date
)
select d_date
from LW
changing the eomonth to
select dateadd(dd,datediff(dd,0,dateadd(mm,1,D_Date))/7*7+2,0)
doesn't seem to work either
Expected results:
2016-01-27
2016-02-24
2016-03-30
2016-04-27
2016-05-25
2016-06-29

Here's a way to do it with EOMONTH() and DATEFROMPARTS():
declare #S_Date date
declare #E_Date date
set #S_Date='2016-01-31' --eomonth of first month
set #E_Date='2016-06-15' --ides of last month ( can actually be any day that is
--not equal to or after the last Wednesday of the month)
;With Date (Date) As
(
Select DateFromParts(Year(#S_Date), Month(#S_Date), 1) Union All
Select DateAdd(Day, 1, Date)
From Date
Where Date < EOMonth(#E_Date)
)
Select Max(Date) As LastWednesday
From Date
Where DatePart(WeekDay, Date) = 4
Group By Year(Date), Month(Date)
Option (MaxRecursion 0)
Output
LastWednesday
2016-01-27
2016-02-24
2016-03-30
2016-04-27
2016-05-25
2016-06-29

Here a slightly different approach without the partitioning... result should be the same:
declare #S_Date date
declare #E_Date date
set #S_Date='2016-01-31' --eomonth of first month
set #E_Date='2016-06-15' --ides of last month ( can actually be any day that is
--not equal to or after the last Wednesday of the month)
;With Date (Date, WD) As
(
Select DateFromParts(Year(#S_Date), Month(#S_Date), 1), DatePart(WeekDay, DateFromParts(Year(#S_Date), Month(#S_Date), 1)) AS wd
Union All
Select DateAdd(Day, 1, Date), DatePart(WeekDay, DateAdd(Day, 1, Date)) AS wd
From Date
Where Date < EOMonth(#E_Date)
)
Select Max(Date) As LastWednesday
From Date
Where wd = 4
group by MONTH(Date)
Option (MaxRecursion 0)

You need a calendar table ..I have a calendar table and below screenshot shows you how simple it is check or get last wednesday of each month..
query used:
select * from dbo.calendar where year='2013' and wkdname='Wednesday'
and last=1
Output of above query which you can modify as per your requirement:

This is rather straightforward solution with eomonth.
declare #s_date date='2016-01-01',#e_date date='2016-12-31' --date range
;with eom_tbl as (--CTE
--anchor from start date
select EOMONTH(#s_date,0) eom,datepart(weekday,EOMONTH(#s_date,0)) dw
union all
--recursive query
select EOMONTH(eom,1) eom,datepart(weekday,EOMONTH(eom,1)) dw
from eom_tbl where eom<#e_date
)
select eom,dw,
--calculate last Wednesday (4)
case when dw<4 then dateadd(dd,-3-dw,eom)
else dateadd(dd,4-dw,eom) end lastWed,
--extra check
datepart(dw,case when dw<4 then dateadd(dd,-3-dw,eom)
else dateadd(dd,4-dw,eom) end ) ddw
from eom_tbl

Related

get 5th previous date from current date through SQL except public holiday

i need a sql function which has to return 5th previous business date except saturday sunday and public holiday,
Ex: i should get last thursday (04-01-2018) if i won't have any public holiday inbetween im able to achieve that by,
SELECT DATEADD(DAY, CASE DATENAME(WEEKDAY, GETDATE())
WHEN 'Sunday' THEN -2
WHEN 'Monday' THEN -3
ELSE -1 END, DATEDIFF(DAY, 5, GETDATE()))
but how to omit public holiday from this,
Can anyone help me please
If you don't want (or you can't) create a calendar "tally" table (with columns identifying holidays and week-end days) you can try a query like following one.
sample data for holidays table
CREATE TABLE HOL_TAB (DAT DATETIME);
INSERT INTO HOL_TAB VALUES ('2018-01-05');
INSERT INTO HOL_TAB VALUES ('2018-01-04');
The query use a CTE to "create" on the fly a small tally calendar table (I used 12 as limit, but you can change it).
The last SELECT use a join with holiday table to exclude those days and then ROW_NUMBER() to extract "first" five days.
To keep query similar to the one you made I used DATENAME, but I suggest to avoid its use and use instead other methods).
WITH CAL_TAB AS (
SELECT DATEADD(dd, 0, CAST(GETDATE() AS DATE) ) AS DAT
, 1 AS COUN
UNION ALL
SELECT DATEADD(dd, -1- CASE DATENAME (WEEKDAY, DATEADD(dd,-1,B.DAT) ) WHEN 'Sunday' THEN 2 WHEN 'Saturday' THEN 1 ELSE 0 END, B.DAT ) AS DAT
, B.COUN+1 AS COUN
FROM CAL_TAB B
WHERE B.COUN<12 /* 12 is just to limit number of days */
)
SELECT DAT, WD
FROM (SELECT C.DAT, C.COUN, DATENAME(WEEKDAY, C.DAT) AS WD, ROW_NUMBER() OVER (ORDER BY COUN) AS RN
FROM CAL_TAB C
WHERE NOT EXISTS(SELECT DAT FROM HOL_TAB D WHERE D.DAT=C.DAT)
) E WHERE RN<=5;
Output:
DAT WD
1 10.01.2018 00:00:00 Wednesday
2 09.01.2018 00:00:00 Tuesday
3 08.01.2018 00:00:00 Monday
4 03.01.2018 00:00:00 Wednesday
5 02.01.2018 00:00:00 Tuesday
try This Logic
WITH CTE
AS
(
SELECT
MyDate = DATEADD(DD,-5,GETDATE())
)
SELECT
MyDate = CASE WHEN DATENAME(WEEKDAY, MyDate) IN ('Sunday','Saturday')
THEN NULL
WHEN MHL.Holiday IS NOT NULL
THEN NULL
ELSE CTE.MyDate END
FROM CTE
LEFT JOIN MyHoliDayList MHL
ON CTE.MyDate = MHL.Holiday
Try this Method:
DECLARE #Holiday TABLE(HoliDay DATE)
INSERT INTO #Holiday VALUES ('2018-01-02')
INSERT INTO #Holiday VALUES ('2018-01-05')
DECLARE #WithOutHoliDay DATETIME = (SELECT DATEADD(DAY, CASE DATENAME(WEEKDAY, GETDATE())
WHEN 'Sunday' THEN -2
WHEN 'Monday' THEN -3
ELSE -1 END, DATEDIFF(DAY, 5, GETDATE())))
SELECT DATEADD(DAY,0-
(SELECT COUNT(1)
FROM #Holiday
WHERE HoliDay BETWEEN #WithOutHoliDay AND GETDATE()),#WithOutHoliDay)
This should give, what exactly you need...

SQL Server : is there a way for me to get weeks per month using Month and Year?

I'm not really good when it comes to database...
I'm wondering if it is possible to get the weeks of a certain month and year..
For example: 1 (January) = month and 2016 = year
Desired result will be:
week 1
week 2
week 3
week 4
week 5
This is what I have tried so far...
declare #date datetime = '01/01/2016'
select datepart(day, datediff(day, 0, #date) / 7 * 7) / 7 + 1
This only returns the total of the weeks which is 5.
declare #MonthStart datetime
-- Find first day of current month
set #MonthStart = dateadd(mm,datediff(mm,0,getdate()),0)
select
Week,
WeekStart = dateadd(dd,(Week-1)*7,#MonthStart)
from
( -- Week numbers
select Week = 1 union all select 2 union all
select 3 union all select 4 union all select 5
) a
where
-- Necessary to limit to 4 weeks for Feb in non-leap year
datepart(mm,dateadd(dd,(Week-1)*7,#MonthStart)) =
datepart(mm,#MonthStart)
Got the answer in the link: http://www.sqlservercentral.com/Forums/Topic1328013-391-1.aspx
Here is one way to approach this:
A month has a minimum of 29 or more days, and a max of 31 or less. Meaning there are almost always 5 weeks a month, with the exception of a non-leap year's feburary, and in those cases, 4 weeks a month.
You can refer to this to find out which years are "leap".
Check for leap year
Hope this helps!
The following code will allow you to select a start and end date, and output one row per week, with numbered weeks, between those dates:
declare #start date = '1/1/2016'
declare #end date = '5/1/2016'
;with cte as (select #start date
, datename(month, #start) as month
union all
select dateadd(dd, 7, date)
, datename(month, dateadd(dd, 7, date))
from CTE
where date <= #end
)
select *, 'week '
+ cast(row_number() over (partition by month order by date) as varchar(1))
from CTE
order by date

How to get all the dates in a full calendar month

In a calendar control, we can see some dates from the previous month and next month also. Sample image below
(ie Apr-2016: Starts from Mar-28 and ends in May-08
Mar-2016: Starts from Apr Feb-29 and ends in Apr-10)
Here, i need to generate a list of all the dates in a calendar control for a particular year month. My week start is Monday.
Here is the tsql script i have tried so far.
DECLARE #V_DATE DATE = GETDATE()
;WITH CTE_DATE AS (
SELECT DATEADD(dd,-(DAY(#V_DATE)-1),#V_DATE) CDATE
UNION ALL
SELECT DATEADD(dd,1,CDATE)
FROM CTE_DATE
WHERE DATEADD(dd,1,CDATE) <= DATEADD(dd,-(DAY(DATEADD(mm,1,CDATE))),DATEADD(mm,1,CDATE))
)
SELECT * FROM CTE_DATE
Result Is:
2016-04-01
2016-04-02
.
.
2016-04-29
2016-04-30
It will list all the days from a inputted year month, but i need to include the
missing dates from the previous month as well as next month.
Expected result for Apr-2016
2016-03-28
2016-03-29
.
2016-04-15
.
2016-05-07
2016-05-08
Expected result for May-2016
2016-04-25
2016-04-26
.
2016-05-15
.
2016-06-04
2016-06-05
Note:- The calendar control is always showing 42 days.
since your week is starts on Monday,you can take referece to date 0 '1900-01-01' which is a Monday. Adding 41 days would gives you your end date
select
date_fr = dateadd(day, datediff(day, 0, '2016-05-01') / 7 * 7, 0),
date_to = dateadd(day, datediff(day, 0, '2016-05-01') / 7 * 7, 41)
the following gives you date 1900-01-01 and Monday
select convert(datetime, 0), datename(weekday, 0)
Have you considered creating a dates table in your database. You would have columns for dates and a column for week number. Linking to this table you could find the week number for your start and end dates, you could then re-link to the table to find the first date of your start week and the last date of your end week. This would probably be more efficient than calculations at each step each time, it is a simple link.
I have create done script for this. This is working as per my expectation, may be helpful for future reference. (Thanks #Squirrel for the logic).
DECLARE #V_ST_DATE DATE = GETDATE()
SET #V_ST_DATE = DATEADD(DAY,-(DAY(#V_ST_DATE)-1),#V_ST_DATE)
SET #V_ST_DATE = DATEADD(WEEK,DATEDIFF(WEEK, 0, #V_ST_DATE) ,0) +
(CASE WHEN DATEADD(WEEK,DATEDIFF(WEEK, 0, #V_ST_DATE) ,0) > #V_ST_DATE THEN -7 ELSE 0 END)
;WITH CTE_DATE AS (
SELECT #V_ST_DATE CDATE,0 TDAYS
UNION ALL
SELECT DATEADD(DAY,1,CDATE) , DATEDIFF(DAY,#V_ST_DATE, DATEADD(DAY,1,CDATE))
FROM CTE_DATE
WHERE DATEDIFF(DAY,#V_ST_DATE, DATEADD(DAY,1,CDATE)) < 42
)
SELECT * FROM CTE_DATE

Unexpected behaviour of DATEPART

Script that lists dates and week number of date:
DECLARE #initDate DATE = '2014-01-01';
DECLARE #endDate DATE = '2014-12-31';
WITH dates (date, week)
AS (SELECT #initDate date,
Datepart(ww, #initDate) week
UNION ALL
SELECT Dateadd(ww, 1, t.date),
Datepart(ww, t.date)
FROM dates t
WHERE t.date < #endDate)
SELECT dates.date,
dates.week
FROM dates
The first three rows are:
date: week number
---------- -----------
2014-01-01 1
2014-01-08 1
2014-01-15 2
..... ..
..... ..
I guess it should be 2 for the second row and 3 for the first, isn't it?
Is it some kind of bug in DATEPART? Even if something is depends on first day of the year, first row date differs from second on one week with any settings.
Could you clarify this, please?
It is because of Recursive CTE not because of Datepart.
In the Recursive part of CTE week is still holding the previous week not the new date that generated in recursive part
Try changing your query like this.
DECLARE #initDate DATE = '2014-01-01';
DECLARE #endDate DATE = '2014-12-31';
WITH dates (date, week)
AS (SELECT #initDate date,
Datepart(ww, #initDate) week
UNION ALL
SELECT Dateadd(ww, 1, t.date),
Datepart(ww, Dateadd(ww, 1, t.date))
FROM dates t
WHERE t.date < #endDate)
SELECT dates.date,
dates.week
FROM dates

How to get Monday, Sunday based on a Week Number in SQL Server?

I have a week number (e.g. 23) and I'd like to get the dates for Monday and Sunday of that week.
I am assuming Monday as the first day of the week (e.g. I have SET DATEFIRST 1 in my script).
DECLARE #startweek1 datetime
SET DATEFIRST 1
--first monday of year
SELECT #startweek1 = DATEADD(day, 8-DATEPART(weekday, '2011-01-01'), '2011-01-01')
--day based
SELECT
DATEADD(day, 22 * 7, #startweek1) AS MondayWeek23,
DATEADD(day, 23 * 7 -1 , #startweek1) AS SundayWeek23
--week based
SELECT
DATEADD(week, 22, #startweek1) AS MondayWeek23,
DATEADD(day, -1, DATEADD(week, 23 , #startweek1)) AS SundayWeek23
Edit:
This solution works if week 1 does not start on day 1 as Andomar said
Edit 2:
According to Wikipedia: 2008-12-29 is day 1 of week 1 of 2009 in ISO.
And week numbers vary as Andomar said
Cyberkiwi mentioned this code is wrong for 2007: it's wrong far more often than that. The same applies to his code too which matches 2007 but is equally wrong for the rest.
Declare #StartDate datetime;
Set #StartDate = '20110101';
With StartOfWeek As
(
Select DateAdd(
week
, 23
, DateAdd(
d
, -(DatePart(dw, #StartDate) - 1)
, #StartDate
) ) As Sunday
)
Select Sunday, DateAdd(d,1,Sunday) As Monday
From StartOfWeek
We can use the DATEFIRST variable to set the start day of the week. By default, the week starts on Sunday(7). In order to start with Monday, we will set its value to 1.
# Printing default value
Select ##DATEFIRST; -- This will give 7 value i.e. default value
# Changing first day of the week to Monday.
Declare #CurrentWeekNumber int
SET DATEFIRST 1
set #CurrentWeekNumber= (SELECT DATEPART(WEEK, GETDATE()))
select #CurrentWeekNumber
PS: Changes to DATEFIRST are valid for current session i.e. it doesn't impact other DB queries which might rely on different week start day.
A calendar table makes this kind of date query pretty simple. A calendar table just has the relevant calendar details calculated and stored at load time instead of calculated at run time. Depending on the application, that can speed things up a lot. This way is dbms agnostic; it just uses literals in the WHERE clause.
select cal_date from calendar
where iso_year = 2011
and iso_week = 23
and cal_dow in ('Mon', 'Sun');
And another way that relies only on having a table of dates.
select cal_date from calendar
where extract(isoyear from cal_date) = 2011
and extract(week from cal_date) = 23
and (extract(isodow from cal_date) = 1 or
extract(isodow from cal_date) = 7);
EXTRACT() is standard SQL, but I'm not sure whether the names of the subfields isoyear, week, isodow are standard SQL.
Note: Any answer that starts with assuming year is 2011 might as well take a static first date, i.e. by replacing #firstMon in my last query below with just the static date '20101227'.
SET DATEFIRST 1
declare #targetYear int
declare #targetWeek int
select #targetYear = 2011, #targetWeek = 23
declare #firstMon datetime
set #firstMon = dateadd(d,1-datepart(dw, right(#targetYear,4)+'0101'),right(#targetYear,4)+'0101')
select
MonOfTargetWeek = dateadd(week, #targetWeek-1, #firstMon),
SunOfTargetWeek = dateadd(week, #targetWeek-1, #firstMon+6)
And the real, final query - which returns a Mon/Sun of NULL when it doesn't exist in that week, such as Mon of partial-week-one or Sun of partial-week-53
;with tmp(Mon, Sun) as (
select
MonOfTargetWeek = dateadd(week, #targetWeek-1, #firstMon),
SunOfTargetWeek = dateadd(week, #targetWeek-1, #firstMon+6)
)
select
RealMonday = case when Year(Mon)=#targetYear then Mon end,
RealSunday = case when Year(Sun)=#targetYear then Sun end
from tmp
For GBN's claim that this answer is wrong (within his answer), I submit below the proof of correctness
I have turned the code into a function.
CREATE function dbo.getMonSunForWeek(#targetYear int, #targetWeek int)
returns table as return
with pre(firstMon) as (
select dateadd(d,1-datepart(dw, right(#targetYear,4)+'0101'),right(#targetYear,4)+'0101'))
, tmp(Mon, Sun) as (
select
dateadd(week, #targetWeek-1, firstMon),
dateadd(week, #targetWeek-1, firstMon+6)
from pre
)
select
Mon, Sun,
RealMonday = case when Year(Mon)=#targetYear then Mon end,
RealSunday = case when Year(Sun)=#targetYear then Sun end
from tmp
GO
And below I present the ENTIRE range of years and weeks (1-53) for the years from 1950 through 2047. In EVERY SINGLE case, when the Monday/Sunday has been determined, and working backwards using DATEPART(week), SQL Server agrees with the week numbering from the function.
set datefirst 1;
select
[Year] = years.number,
[Week] = weeks.number,
[Mon] = fun.realmonday,
[Sun] = fun.realsunday,
[WeekX] = isnull(datepart(week, fun.realmonday), datepart(week, fun.realsunday)),
[MonX] = fun.Mon,
[SunX] = fun.Sun
from master..spt_values years
inner join master..spt_values weeks on weeks.type='P' and weeks.number between 1 and 53
cross apply dbo.getMonSunForWeek(years.number, weeks.number) fun
where years.type='P' and years.number between 1950 and 2047
order by [year], [Week]
Output around the 2005-2006 changeover
when including prior/next year
Year Week Mon-of-week Sun-of-week datepart(week) Mon -and- Sun
2005 52 2005-12-19 2005-12-25 52 2005-12-19 2005-12-25
2005 53 2005-12-26 NULL 53 2005-12-26 2006-01-01
2006 1 NULL 2006-01-01 1 2005-12-26 2006-01-01
2006 2 2006-01-02 2006-01-08 2 2006-01-02 2006-01-08
You might try some dateadd logic...
dateadd(week, 23, '2011-01-01')
dateadd(day, 7, dateadd(week, 23, '2011-01-01'))
UPDATE:
select dateadd(day, 23, dateadd(week, 23, '2011-01-01')) , Datename(weekday,dateadd(day, 23, dateadd(week, 23, '2011-01-01')));
// RETURNS 7/4/2011, Monday
select Datename(weekday,dateadd(day, 23, dateadd(week, 23, '2011-01-01')));
// RETURNS Monday
EXAMPLE FROM: http://msdn.microsoft.com/en-us/library/ms186819.aspx
DECLARE #datetime2 datetime2 = '2007-01-01 13:10:10.1111111'
SELECT 'year', DATEADD(year,1,#datetime2)
UNION ALL
SELECT 'quarter',DATEADD(quarter,1,#datetime2)
UNION ALL
SELECT 'month',DATEADD(month,1,#datetime2)
UNION ALL
SELECT 'dayofyear',DATEADD(dayofyear,1,#datetime2)
UNION ALL
SELECT 'day',DATEADD(day,1,#datetime2)
UNION ALL
SELECT 'week',DATEADD(week,1,#datetime2)
UNION ALL
SELECT 'weekday',DATEADD(weekday,1,#datetime2)
UNION ALL
SELECT 'hour',DATEADD(hour,1,#datetime2)
UNION ALL
SELECT 'minute',DATEADD(minute,1,#datetime2)
UNION ALL
SELECT 'second',DATEADD(second,1,#datetime2)
UNION ALL
SELECT 'millisecond',DATEADD(millisecond,1,#datetime2)
UNION ALL
SELECT 'microsecond',DATEADD(microsecond,1,#datetime2)
UNION ALL
SELECT 'nanosecond',DATEADD(nanosecond,1,#datetime2);
/*
Year 2008-01-01 13:10:10.1111111
quarter 2007-04-01 13:10:10.1111111
month 2007-02-01 13:10:10.1111111
dayofyear 2007-01-02 13:10:10.1111111
day 2007-01-02 13:10:10.1111111
week 2007-01-08 13:10:10.1111111
weekday 2007-01-02 13:10:10.1111111
hour 2007-01-01 14:10:10.1111111
minute 2007-01-01 13:11:10.1111111
second 2007-01-01 13:10:11.1111111
millisecond 2007-01-01 13:10:10.1121111
microsecond 2007-01-01 13:10:10.1111121
nanosecond 2007-01-01 13:10:10.1111111
*/