Number of e.g. Mondays left in month - mysql

How do you most easily calculate how many e.g. Mondays are left in a month using MySQL (counting today)?
Bonus points for a solution that solves it for all days of the week in one query.
Desired output (run on Tuesday August 17th 2010):
dayOfWeek left
1 2 -- Sunday
2 2 -- Monday
3 3 -- Tuesday (yep, including today)
4 2 -- Wednesday
5 2 -- Thursday
6 2 -- Friday
7 2 -- Saturday

Create a date table that contains one row for each day that you care about (say Jan 1 2000 - Dec 31 2099):
create table dates (the_date date primary key);
delimiter $$
create procedure populate_dates (p_start_date date, p_end_date date)
begin
declare v_date date;
set v_date = p_start_date;
while v_date <= p_end_date
do
insert ignore into dates (the_date) values (v_date);
set v_Date = date_add(v_date, interval 1 day);
end while;
end $$
delimiter ;
call populate_dates('2000-01-01','2099-12-31');
Then you can run a query like this to get your desired output:
set #date = curdate();
select dayofweek(the_date) as dayOfWeek, count(*) as numLeft
from dates
where the_date >= #date
and the_date < str_to_date(period_add(date_format(#date,'%Y%m'),1),'%Y%m')
group by dayofweek(the_date);
That will exclude days of the week that have 0 occurrences left in the month. If you want to see those you can create another table with the days of the week (1-7):
create table days_of_week (
id tinyint unsigned not null primary key,
name char(10) not null
);
insert into days_of_week (id,name) values (1,'Sunday'),(2,'Monday'),
(3,'Tuesday'),(4,'Wednesday'),(5,'Thursday'),(6,'Friday'),(7,'Saturday');
And query that table with a left join to the dates table:
select w.id, count(d.the_Date) as numLeft
from days_of_week w
left outer join dates d on w.id = dayofweek(d.the_date)
and d.the_date >= #date
and d.the_date < str_to_date(period_add(date_format(#date,'%Y%m'),1),'%Y%m')
group by w.id;

i found something
according to this article "find next monday"
http://www.gizmola.com/blog/archives/99-Finding-Next-Monday-using-MySQL-Dates.html
SELECT DATE_ADD(CURDATE(), INTERVAL (9 - IF(DAYOFWEEK(CURDATE())=1, 8,
DAYOFWEEK(CURDATE()))) DAY) AS NEXTMONDAY;
what we need to do is calculate the days between end month and next Monday,
and divide in 7 .
update (include current day) :
so the result is like :
for Monday
SELECT CEIL( ((DATEDIFF(LAST_DAY(NOW()),DATE_ADD(CURDATE(),
INTERVAL (9 - IF(DAYOFWEEK(CURDATE())=1, 8, DAYOFWEEK(CURDATE()))) DAY)))+1)/7)
+ IF(DAYOFWEEK(CURDATE())=2,1,0)
for Tuesday :
SELECT CEIL( ((DATEDIFF(LAST_DAY(NOW()),DATE_ADD(CURDATE(),
INTERVAL (10 - IF(DAYOFWEEK(CURDATE())=1, 8, DAYOFWEEK(CURDATE()))) DAY)))+1)/7)
+ IF(DAYOFWEEK(CURDATE())=3,1,0)

Have a look at my responses to;
MySQL: Using the dates in a between condition for the results
and
Select all months within given date span, including the ones with 0 values
for a way I think would work nicely, similar to #Walker's above, but without having to do the dayofweek() function within the query, and possibly more flexible too. One of the responses has a link to a SQL dump of my table which can be imported if it helps!

Related

Fetch distinct available dates from table

I have a data table where data is present for every date (for 50 branches) except saturday and sunday. I am trying to get the last valid date from table from multiple given dates.
select distinct BW_DATE from backdated_weekly where BW_DATE <='2021-09-30' or BW_DATE<='2021-09-26' order by BW_DATE desc;
As 2021-09-30 is a valid date but 2021-09-26 (Sunday) is not present in table. I am trying to get output as
2021-09-30
2021-09-24
But above query gives all results below given dates.
If it is confirmed there are dates continuously in the table for all mon-fri only, simply select the maximum date up to the given date
SELECT MAX(BW_DATE)
FROM backdated_weekly
WHERE BW_DATE <= '2021-09-30'
UNION
SELECT MAX(BW_DATE)
FROM backdated_weekly
WHERE BW_DATE <= '2021-09-26'
Also we can calculate newest date in mon-fri for a given date directly without any table
WEEKDAY is the function to be used
Returns the weekday index for date (0 = Monday, 1 = Tuesday, … 6 =
Sunday).
SELECT CASE WHEN WEEKDAY('2021-09-30') IN ( 5, 6 ) THEN DATE('2021-09-30') - INTERVAL WEEKDAY('2021-09-30') - 4 DAY ELSE DATE('2021-09-30') END
UNION
SELECT CASE WHEN WEEKDAY('2021-09-26') IN ( 5, 6 ) THEN DATE('2021-09-26') - INTERVAL WEEKDAY('2021-09-26') - 4 DAY ELSE DATE('2021-09-26') END
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=477775c0eddbfa733e60bc629a8a68d4

How to find multiple financial years form start date and finish date in MySQL database?

I'm in a situation to write SQL query in MySQL database that dynamically handles populating Financial years.
Financial starts from '01 July' and finishes at '30 June'
When start date and finish date is in the same financial year then for example : '2018-19' with only 1 record
Scenario 1: IF startDate (DD-MM-YYYY format) = 02-07-2018 and FinishDate (DD-MM-YYYY format) = 21-06-2019 then the Financial year will be '2018-19' with only 1 record.
Scenario 2: IF startDate (DD-MM-YYYY format) = 13-07-2018 and FinishDate (DD-MM-YYYY format) = 17-02-2020 then the Financial year will be '2018-19' as record 1 and record 2 as '2019-20'
When the data for startdate and finishdate keeps changing the SQL code should dynamically update the Financial year accordingly.
Input Table Data:
**SQL statements to create some scenario based data.**
CREATE TABLE Test1(
StartDate date,
FinishDate date);
Insert into Test1(StartDate,FinishDate) values ('02-07-2018','21-06-2019');
Insert into Test1(StartDate,FinishDate) values ('13-07-2018','17-02-2020');
Insert into Test1(StartDate,FinishDate) values ('05-05-2018','04-10-2019');
Insert into Test1(StartDate,FinishDate) values ('09-02-2018','21-11-2023');
Select * from Test1;
Expected Output:
Thanks in advance
Swetha
This is actually a multi-part solution. First, you are not even showing a possible Fiscal Calendar to work with. As such, I am dynamically building one. You just need to use a table name for the inner query based on the number of years you want to cover. It can be any table as long as it has that many rows in it. Ex: You want to forecast up to 10 years, the table needs at least 10 rows.
select
#beginFY := date_add( #beginFY, interval 1 year ) BeginFY,
#endFY := date_add( #endFY, interval 1 year ) EndFY,
concat( Date_Format( #beginFY, '%Y' ),'-', Date_Format(#endFY, '%y')) FiscalYear
from
AnyTableWith10Records,
-- basis to START your fiscal years. I am force-setting
-- the beginning and ending to July 1, June 30 respectively
-- but starting 2016
( select #beginFY := '2016-07-01',
#endFY := '2017-06-30' ) sqlvars
limit 10
The above query will create a result set such as
BeginFY EndFY FiscalYear
2017-07-01 2018-06-30 2017-18
2018-07-01 2019-06-30 2018-19
2019-07-01 2020-06-30 2019-20
...
2026-07-01 2027-06-30 2026-27
So at this point, I have every possible fiscal year record, its own begin/end dates for comparison purposes and the formatted fiscal year for representation in your final output.
Now, take that entire query and join to your test table as below.
select
date_format( T.StartDate, '%d-%b-%y' ) StartDate,
date_format( T.FinishDate, '%d-%b-%y' ) FinishDate,
FY.FiscalYear
from
( select
#beginFY := date_add( #beginFY, interval 1 year ) BeginFY,
#endFY := date_add( #endFY, interval 1 year ) EndFY,
concat( Date_Format( #beginFY, '%Y' ),'-', Date_Format(#endFY, '%y')) FiscalYear
from
AnyTableWith10Records,
( select #beginFY := '2016-07-01',
#endFY := '2017-06-30' ) sqlvars
limit 10 ) FY
JOIN test1 T
on ( FY.BeginFY <= T.StartDate AND T.StartDate <= FY.EndFY )
OR ( FY.BeginFY <= T.FinishDate AND T.FinishDate <= FY.EndFY )
OR ( T.StartDate < FY.BeginFY AND FY.EndFY < T.FinishDate )
order by
T.StartDate,
T.FinishDate,
FY.BeginFY
Notice the JOIN condition. I have 3 criteria and any one of them is valid.
1 based on the start date is ANYWHERE within ANY of the fiscal year begin/end dates.
2 based on the finish date is ANYWHERE within ANY of the fiscal year begin/end dates.
3 The fiscal year is COMPLETELY WITHIN the start/finish such as your Feb 9, 2018 through Nov 21, 2023. The 2019-20, 2020-21, 2021-22 and 2022-2023 are ALL completely encompassed within your start/finish and will be included in final output.
For display result, I sorted based on the start date and then the fiscal year it qualified against. You can always tweak it once you get results you are expecting.

Show next date based on criteria

In perl I have the following database query:
my $list = $db->SelectARef("SELECT p.*, u.usr_login, u.usr_money, u.usr_email, u.usr_pay_email, u.usr_pay_type
FROM Payments p, Users u
WHERE status='PENDING'
AND p.usr_id=u.usr_id
ORDER BY u.usr_pay_type");
Within the array result there is a field named "created".
What I want to do is add another element to the array for each row as "next payment".
Payments are processed after 30 full days from the datetime value but only on the 6th day of every month. Basically I want each result to have a "next payment" element stating which day and month they should get paid on.
e.g created = 2013-07-29 18:55:37
30 days from this is the 28th August 2013
Therefore the next payment date would be the 6th September
I have no idea where to start with this, any help anyone can provide would be greatly appreciated!
Thanks
The following shows the logic:
select (date(t) - interval (day(t) - 1) day) + interval 1 month + interval 5 day
from (select cast(now() as datetime) as t) t;
You can put this into your query as:
SELECT p.*, u.usr_login, u.usr_money, u.usr_email, u.usr_pay_email, u.usr_pay_typel,
created - interval (day(created) - 1) day) + interval 1 month + interval 5 day as nextpay
FROM Payments p join
Users u
on p.usr_id=u.usr_id
WHERE status='PENDING'
ORDER BY u.usr_pay_type;
The expression can be simplified to:
created - interval (day(created) + 4) day) + interval 1 month as nextpay
If I understand correctly, you want to compute a next_payment date which is the nearest 6th day of a month that is at least 30 days from the created date. Another way of saying that is that your due date is always the 6th of the month with at least a 30 day grace period from the close date.
Unfortunately, we can't do a simple rounding or stepping here, because the Gregorian calendar isn't quite a regular series. (It quite isn't a regular series?) So, we'll put conditional logic into a query. As this looks just awful, we'll hide it in a function:
DELIMITER //
CREATE FUNCTION next_payment(close_date DATE)
RETURNS DATE
DETERMINISTIC
READS SQL DATA
LANGUAGE SQL
BEGIN
DECLARE min_grace INT DEFAULT 30; -- minimum grace period of 30 days
DECLARE bill_dom INT DEFAULT 6; -- billing day of month is the 6th
DECLARE d DATE;
SET d = close_date + INTERVAL min_grace DAY;
IF DAY(d) > bill_dom THEN -- Did 30 days\' grace put us beyond the 6th?
SET d = d + INTERVAL 1 MONTH; -- If so, advance to next month.
END IF;
RETURN d + INTERVAL(bill_dom - DAY(d)) DAY; -- Now "round" to the 6th of the month
END
//
DELIMITER ;
How you use the logic is up to you. Adding SELECT next_payment(p.created) AS "next_payment", ... will give you the additional column you want. You could parameterize the above (e.g., CREATE FUNCTION next_payment(close_date DATE, min_grace INT, bill_dom INT)) for greater flexibility. You might rip the SET/IF logic out of the function and jam it into one unwieldy CASE/END statement in your SELECT. You could create a VIEW that automatically "appends" a next_payment column to the table, or ALTER TABLE and populate a new column, etc.

How to get week number of the month from the date in sql server 2008

In SQL Statement in microsoft sql server, there is a built-in function to get week number but it is the week of the year.
Select DatePart(week, '2012/11/30') // **returns 48**
The returned value 48 is the week number of the year.
Instead of 48, I want to get 1, 2, 3 or 4 (week number of the month). I think the week number of the month can be achieved by modules with Month Number of this week. For e.g.
Select DATEPART(week, '2012/11/30')%MONTH('2012/11/30')
But I want to know is there other built-in functions to get WeekNumber of the month in MS SQL SERVER.
Here are 2 different ways, both are assuming the week starts on monday
If you want weeks to be whole, so they belong to the month in which they start:
So saturday 2012-09-01 and sunday 2012-09-02 is week 4 and monday 2012-09-03 is week 1 use this:
DECLARE #date date = '2012-09-01'
SELECT (day(datediff(d,0,#date)/7*7)-1)/7+1
If your weeks cut on monthchange so saturday 2012-09-01 and sunday 2012-09-02 is week 1 and monday 2012-09-03 is week 2 use this:
DECLARE #date date = '2012-09-01'
SELECT
datediff(ww,datediff(d,0,dateadd(m,datediff(m,7,#date),0)
)/7*7,dateadd(d,-1,#date))+1
I received an email from Gerald. He pointed out a flaw in the second method. This should be fixed now
I received an email from Ben Wilkins. He pointed out a flaw in the first method. This should be fixed now
DECLARE #DATE DATETIME
SET #DATE = '2013-08-04'
SELECT DATEPART(WEEK, #DATE) -
DATEPART(WEEK, DATEADD(MM, DATEDIFF(MM,0,#DATE), 0))+ 1 AS WEEK_OF_MONTH
No built-in function. It depends what you mean by week of month. You might mean whether it's in the first 7 days (week 1), the second 7 days (week 2), etc. In that case it would just be
(DATEPART(day,#Date)-1)/7 + 1
If you want to use the same week numbering as is used with DATEPART(week,), you could use the difference between the week numbers of the first of the month and the date in question (+1):
(DATEPART(week,#Date)- DATEPART(week,DATEADD(m, DATEDIFF(m, 0, #Date), 0))) + 1
Or, you might need something else, depending on what you mean by the week number.
Just look at the date and see what range it falls in.
Range 1-7 is the 1st week, Range 8-14 is the 2nd week, etc.
SELECT
CASE WHEN DATEPART(day,yourdate) < 8 THEN '1'
ELSE CASE WHEN DATEPART(day,yourdate) < 15 then '2'
ELSE CASE WHEN DATEPART(day,yourdate) < 22 then '3'
ELSE CASE WHEN DATEPART(day,yourdate) < 29 then '4'
ELSE '5'
END
END
END
END
Similar to the second solution, less code:
declare #date datetime = '2014-03-31'
SELECT DATEDIFF(week,0,#date) - (DATEDIFF(week,0,DATEADD(dd, -DAY(#date)+1, #date))-1)
Check this out... its working fine.
declare #date as datetime = '2014-03-10'
select DATEPART(week,#date) - DATEPART(week,cast(cast(year(#date) as varchar(4))+'-' + cast(month(#date) as varchar(2)) + '-01' as datetime))+1
WeekMonth = CASE WHEN (DATEPART(day,TestDate) - datepart(dw,TestDate))>= 22 THEN '5'
WHEN (DATEPART(day,TestDate) - datepart(dw,TestDate))>= 15 THEN '4'
WHEN (DATEPART(day,TestDate) - datepart(dw,TestDate))>= 8 THEN '3'
WHEN (DATEPART(day,TestDate) - datepart(dw,TestDate))>= 1 THEN '2'
ELSE '1'
END
There is no inbuilt function to get you the week number. I dont think dividing will help you anyway as the number of weeks in a month is not constant.
http://msdn.microsoft.com/en-us/library/bb675168.aspx
I guess you can divide the number(48) by 4 and take the modules of the same and project that as the week number of that month, by adding one to the result.
Here's a suggestion for getting the first and last days of the week for a month:
-- Build a temp table with all the dates of the month
drop table #tmp_datesforMonth
go
declare #begDate datetime
declare #endDate datetime
set #begDate = '6/1/13'
set #endDate = '6/30/13';
WITH N(n) AS
( SELECT 0
UNION ALL
SELECT n+1
FROM N
WHERE n <= datepart(dd,#enddate)
)
SELECT DATEADD(dd,n,#BegDate) as dDate
into #tmp_datesforMonth
FROM N
WHERE MONTH(DATEADD(dd,n,#BegDate)) = MONTH(#BegDate)
--- pull results showing the weeks' dates and the week # for the month (not the week # for the current month)
select MIN(dDate) as BegOfWeek
, MAX(dDate) as EndOfWeek
, datediff(week, dateadd(week, datediff(week, 0, dateadd(month, datediff(month, 0, dDate), 0)), 0), dDate) as WeekNumForMonth
from #tmp_datesforMonth
group by datediff(week, dateadd(week, datediff(week, 0, dateadd(month, datediff(month, 0, dDate), 0)), 0), dDate)
order by 3, 1
A dirty but easy one liner using Dense_Rank function. Performance WILL suffer, but effective none the less.
DENSE_RANK()over(Partition by Month(yourdate),Year(yourdate) Order by Datepart(week,yourdate) asc) as Week
Here is the query that brings the week number on whatever the startday and endday of the week it may be.
SET DATEFIRST 2
DECLARE #FROMDATE DATE='12-JAN-2015'
-- Get the first day of month
DECLARE #ALLDATE DATE=DATEADD(month, DATEDIFF(month, 0, #FROMDATE), 0)
DECLARE #FIRSTDATE DATE
;WITH CTE as
(
-- Get all dates in that month
SELECT 1 RNO,CAST(#ALLDATE AS DATE) as DATES
UNION ALL
SELECT RNO+1, DATEADD(DAY,1,DATES )
FROM CTE
WHERE DATES < DATEADD(MONTH,1,#ALLDATE)
)
-- Retrieves the first day of week, ie, if first day of week is Tuesday, it selects first Tuesday
SELECT TOP 1 #FIRSTDATE = DATES
FROM CTE
WHERE DATEPART(W,DATES)=1
SELECT (DATEDIFF(DAY,#FIRSTDATE,#FROMDATE)/7)+1 WEEKNO
For more information I have answered for the below question. Can check that.
How do I find week number of a date according to DATEFIRST
floor((day(#DateValue)-1)/7)+1
Here you go....
Im using the code below..
DATEPART(WK,#DATE_INSERT) - DATEPART(WK,DATEADD(DAY,1,DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,#DATE_INSERT),0)))) + 1
Try Below Code:
declare #dt datetime='2018-03-15 05:16:00.000'
IF (Select (DatePart(DAY,#dt)%7))>0
Select (DatePart(DAY,#dt)/7) +1
ELSE
Select (DatePart(DAY,#dt)/7)
There is an inbuilt option to get the week number of the year
**select datepart(week,getdate())**
You can simply get week number by getting minimum week number of month and deduct it from week number. Suppose you have a table with dates
select
emp_id, dt , datepart(wk,dt) - (select min(datepart(wk,dt))
from
workdates ) + 1 from workdates
Solution:
declare #dt datetime='2018-03-31 05:16:00.000'
IF (Select (DatePart(DAY,#dt)%7))>0
Select (DatePart(DAY,#dt)/7) +1
ELSE
Select (DatePart(DAY,#dt)/7)
declare #end_date datetime = '2019-02-28';
select datepart(week, #end_date) - datepart(week, convert(datetime, substring(convert(nvarchar, convert(datetime, #end_date), 127), 1, 8) + '01')) + 1 [Week of Month];
Here is the tried and tested solution for this query in any situation - like if 1st of the month is on Friday , then also this will work -
select (DATEPART(wk,#date_given)-DATEPART(wk,dateadd(d,1-day(#date_given),#date_given)))+1
above are some solutions which will fail if the month's first date is on Friday , then 4th will be 2nd week of the month
Logic here works as well 4.3 weeks in every month. Take that from the DATEPART(WEEK) on every month but January. Just another way of looking at things. This would also account for months where there is a 5th week
DECLARE #date VARCHAR(10)
SET #date = '7/27/2019'
SELECT CEILING(DATEPART(WEEK,#date)-((DATEPART(MONTH,#date)-1)*4.3333)) 'Week of Month'
Below will only work if you have every week of the month represented in the select list. Else the rank function will not work, but it is a good solution.
SELECT DENSE_RANK() OVER (PARTITION BY MONTH(DATEFIELD)
ORDER BY DATEPART(WEEK,DATEFIELD) ASC) AS WeekofMont
try this one
declare #date datetime = '20210928'
select convert(int,(((cast(datepart(day,#date) as decimal(4,2))/7)-(1.00/7.00))+1.00))
select datepart(week,#date)-datepart(week,dateadd(day,1,eomonth(dateadd(m,-1,#date))))+1
or
select datepart(week,#date)-datepart(week,dateadd(d,-datepart(d,#date)+1,#date))+1
steps:
1,get the first day of month
2,week of year of the date - week of year of the first day of the month
3,+1
2023-1-1 is sunday
SET DATEFIRST 1;
DECLARE #date date = '2023-1-02';
select datepart(week,#date)-datepart(week,dateadd(day,1,eomonth(dateadd(m,-1,#date))))+1
return 2
SET DATEFIRST 7;
DECLARE #date date = '2023-1-02';
select datepart(week,#date)-datepart(week,dateadd(day,1,eomonth(dateadd(m,-1,#date))))+1
return 1
Code is below:
set datefirst 7
declare #dt datetime='29/04/2016 00:00:00'
select (day(#dt)+datepart(WEEKDAY,dateadd(d,-day(#dt),#dt+1)))/7
select #DateCreated, DATEDIFF(WEEK, #DateCreated, GETDATE())

How to calculate a third monday of every month using SQL statement?

i need to trigger a notification. this notification has to be triggered every third monday of every month.
SELECT
(
DAYOFWEEK(NOW()) = 2
AND
DAYOFMONTH(NOW()) BETWEEN 15 AND 21
)
AS send_notice_today;
Try using dayofweek and dayofmonth functions. http://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_dayofweek
Somehow you can check how many weeks are there from 1st of month to curdate() with dayofmonth (using an operation mod 7), and dayofweek should be 5 (thursday)
So perhaps MORE GENERALLY if you can get the "week of month" for the date using this:
(FLOOR((DAYOFMONTH(given) - 1) / 7)) AS 'week_of_month'
which I believe provides an accurate 0 based week-of-month index for a given date. then you can use the value to find any nth as in:
WHERE (week_of_month) = n AND weekday = {weekday}
you can also use the above to get the "last {weekday}" by:
WHERE (week_of_month >= 4) and weekday = {weekday}
note that the week_of_month can range from 0 to 5 (e.g., a 31 day month whose 1st falls on Saturday will have the 31st in the 6th week (5 as a 0 based index)
hope this helps ...
OK a bit more ... you might define the above as a function as in:
CREATE FUNCTION WEEKOFMONTH(given date) RETURNS int DETERMINISTIC RETURN (FLOOR((DAYOFMONTH(given) - 1) / 7))
and add another function:
CREATE FUNCTION WEEKNAME(given date) RETURNS text CHARACTER SET utf8 COLLATE utf8_unicode_ci DETERMINISTIC RETURN (CONCAT(ELT(WEEKOFMONTH(given)+1,'1st ','2nd ','3rd ','4th/Last ','5th/Last '),DAYNAME(given)))
then you can simply say things like
SELECT * FROM dataTable WHERE WEEKNAME(your_date_field) = "3rd Wednesday"
... I struggled with how the 4th/5th should be returned from WEEKDAY and settled on adding "/Last" for both under the theory that this is "good enough" should one want to test for either 4th, 5th or Last. Using this you can do:
SELECT * FROM dataTable WHERE WEEKNAME(your_date_field) LIKE "%Last%"
The date of the third monday of the current month would be given by the SQL statement:
SELECT date_add(date_sub(curdate(),INTERVAL dayofmonth(curdate())-1 DAY),
INTERVAL (7-weekday(date_sub(curdate(),INTERVAL dayofmonth(curdate())-1 DAY)))+14 DAY)
The approach that I'm taking is get the 1st Monday of the month, and depending on when in the month it is, add either 2 or 3 weeks to it (since when it falls out before/on Monday, you only need to walk 2 more weeks):
;with
filler as (select row_number() over (order by a) a from (select top 100 1 as a from syscolumns) a cross join (select top 100 1 as b from syscolumns) b),
dates as (select dateadd(month, a-1, '1/1/1900') date from filler where a <= 2000),
FirstMonday as (
select dateadd(day, case datepart(weekday,Date)
when 1 then 1
when 2 then 0
when 3 then 6
when 4 then 5
when 5 then 4
when 6 then 3
when 7 then 2
end, Date) as Date
,case when datepart(weekday,Date) = 1 then 3 else 2 end as Weeks
from dates
)
select dateadd(week, Weeks, Date) as ThirdMonday
from FirstMonday
This one calculates the first Monday of any month given the year-month-day
SET #firstday = '2015-04-01';
SELECT ADDDATE( #firstday , MOD((9-DAYOFWEEK(#firstday)),7)) as first_monday;
This one calculates the third Monday of any month given the year-month-day
SET #firstday = '2015-01-01';
SELECT ADDDATE( #firstday , MOD((23-DAYOFWEEK(#firstday)),21)) as third_monday;
This one calculates the third Friday of any month given the year-month-day
SET #firstday = '2015-09-01';
SELECT ADDDATE( #firstday , MOD((20-DAYOFWEEK(#firstday)),20)) as third_friday;
Thanks to #Brewal for the original question and #User2208436 for pointing us toward the answer.
Here's an answer that does not use DAYOFWEEK or DAYOFMONTH. It uses DATEADD and DATEPART only.
We'll need two helper functions:
CREATE FUNCTION dbo.day_of_week(#date Date)
RETURNS INT
AS BEGIN
-- (1 for Sunday, 2 for Monday, etc)
RETURN DATEPART(dw, DATEADD(year, year(#date)-1900, DATEADD(month, month(#date)-1, DATEADD(day, day(#date)-1, 0))))
END
GO
CREATE FUNCTION dbo.date_from_parts(#year INT, #month INT, #day INT)
RETURNS DATE
AS BEGIN
RETURN DATEADD(year, #year-1900, DATEADD(month, #month-1, DATEADD(day, #day-1, 0)))
END
GO
Then using the following example data:
DECLARE #day_of_week INT
SET #day_of_week = 2 -- Monday
DECLARE #year INT
DECLARE #month INT
SET #year = 2016
SET #month = 11
Let's first obtain the FIRST Monday of the month:
We will add an offset, (day_of_week - day of week of first day of the month) % 7, to the first day of the month.
(Also notice we need the construction ((x % n) + n) % n instead of just x % 7, to keep the answer within 0 and 6. For example, just SELECT -3 % 7 returns -3! See Mod negative numbers in SQL just like excel)
Now here's the final construction to obtain the first Monday of the month:
SELECT
DATEADD(
dd,
(((#day_of_week -
dbo.day_of_week(dbo.date_from_parts(#year, #month, 1))) % 7) + 7) % 7,
dbo.date_from_parts(#year, #month, 1)
)
To obtain the third Monday of the month, add 14 to the second term of this answer.
If #firstday is the first day of the month
select date_add(#firstday,
interval (if(weekday(#firstday)>0,21-weekday(#firstday),14)) day);
yields the 3rd monday of the month.
Tested with all months of 2018.
Seems simpler than previous solutions.
The key is the if function on weekday.
if weekday = 0 (first day is a monday), add 14 days
if weekday > 0, add 21 days and subtract the weekday
DECLARE #YEAR DATE='2019-01-01'
SELECT DATEADD( d, 23-(DATEPART(dw,#YEAR )%21),#YEAR )
This will help to get the third Monday of whatever month you want.