Pseudo code of what I'd like to do:
SELECT * FROM table WHERE NTH_DAY(DATE(table_date)) = NTH_DAY($input_date);
I want to determine what the nth weekday of the month is for a given date. For example, if given the input "2013-08-30" it should return 5, since it is the fifth occurrence of that weekday (Friday) in the month.
I've been reading through countless similar questions but the majority are looking for just the opposite, for example they want to find the date of the fifth Friday, whereas I want to determine what the nth number of a date is. Other questions appear to be what I'm after but they're in different programming languages I don't understand.
Can someone please explain if this is possible in a MySQL query and how to do it?
To find which "nth" a given day is is rather easy:
select (case when day(table_date) between 1 and 7 then 1
when day(table_date) between 8 and 14 then 2
when day(table_date) between 15 and 21 then 3
when day(table_date) between 22 and 28 then 4
else 5
end) as NthDay
You can also do this using "remainder" arithmetic:
select 1 + floor((day(table_date) - 1) / 7) ) as NthDay
I'm going to throw my two cents in here with a third option that gets it based on finding the 1st of each day, and then adding multiples of 7 to get to the final result.
Function
CREATE FUNCTION `ISNTHDAYINMONTH`(checkDate DATE, weekNumber INTEGER, dayOfWeek INTEGER, monthOfYear INTEGER) RETURNS INTEGER
NO SQL
DETERMINISTIC
BEGIN
DECLARE firstOfMonth DATE;
SET firstOfMonth = DATE_SUB(checkDate, INTERVAL DAYOFMONTH(checkDate) - 1 DAY); #Find the first day of the current month
IF DAYOFWEEK(checkDate) = dayOfWeek AND MONTH(checkDate) = monthOfYear #Make sure at least this matches
AND DAYOFMONTH(checkDate) = ((1 + (7 + dayOfWeek - DAYOFWEEK(firstOfMonth)) % 7) + 7 * (weekNumber - 1)) THEN #When the date matches the nth dayOfWeek day of the month
RETURN 1;
ELSE
RETURN 0; #Nope
END IF;
END
Use
SELECT ISNTHDAYINMONTH(datecol, weekNumber, dayOfWeek, monthOfYear);
Where weekNumber is 1 through 5, dayOfWeek is 1 through 7 (1 = Sun), and monthOfYear is 1 through 12.
Example
To check if datecol is the second Tuesday of April:
SELECT ISNTHDAYINMONTH(datecol, 2, 3, 4);
Calculations
Let's break it down. This line gets the first day of the month for the date that is passed in.
SET firstOfMonth = DATE_SUB(checkDate, INTERVAL DAYOFMONTH(checkDate) - 1 DAY); #Find the first day of the current month
This check ensures that the date has the correct day of the week, and correct month (just in case the user passed in a date that isn't even the right day of the week).
DAYOFWEEK(checkDate) = dayOfWeek AND MONTH(checkDate) = monthOfYear
Now for the big one:
DAYOFMONTH(checkDate) = ((1 + (7 + dayOfWeek - DAYOFWEEK(firstOfMonth)) % 7) + 7 * (weekNumber - 1))
To get the amount we need to add to 1 to get the date, we calculate dayOfWeek that we are looking at, minus the day of the week that the first day of the month falls on to get the offset mod 7 ((dayOfWeek - DayOfWeek(first)) % 7).
Note that because the first of the month could land on a day before or on the day we are looking at, we add an additional 7 and then mod by seven. This is needed because MySQL's Mod function does not properly compute mod. I.e. -1 % 7 should be 6, but MySQL returns -1. Adding 7 and taking the modulus ensures the result will always be correct.
To summarize:
NumberToAddToOneToGetDateOfFirstWeekDay = (7 + DayOfWeek - DayOfWeek(firstDayOfMonth)) %
Then we add one and how every many multiples of 7 are needed to get to the correct week.
select ceiling(DATEPART(day, GETDATE())/7.0)
This will always give you nth occurrence number of current day in a month.
For example, if the current day is Friday, and it occurred 3rd time this month. It will return 3.
If you replace GETDATE() with your Date variable or column name it will return the occurrence of the day on that date in the corresponding month.
This would give the number of the week (which is actually the same as if it is the 5th friday or 5th Monday)
SELECT WEEK(dateasd,5) -
WEEK(DATE_SUB(dateasd, INTERVAL DAYOFMONTH(dateasd)-1 DAY),5)
from test
Use Weekday() function to find which day it is
Fiddle at
http://www.sqlfiddle.com/#!2/7a8b6/2/0
Related
I'm trying to calculate the week number of a quarter in MySQL.
Unfortunately, all existing solutions are based on DATEDIFF between two dates which is not what I want.
For example, today's date is Feb 24th. It is week 9 in the quarter as the first three days of January (Friday, Saturday and Sunday in this current year) were week 1.
If I do a DateDiff and simply divide by 7, it's still only giving me the number of weeks total by days. It's not giving me the calendar weeks since the start of the quarter.
CEIL(DATEDIFF(CURDATE(), MAKEDATE(YEAR(CURDATE()), 2) + INTERVAL QUARTER(CURDATE()) QUARTER - INTERVAL 1 QUARTER) /7) AS WEEK_IN_QUARTER,
This gives me week 8, when the expected (wanted) output is 9.
Adding 1 to the week is going to cause problems when the opposite is true and when there's only a partial week left in the quarter or when we're 4 days into any week.
The first week should start on the first day of the quarter and end on the Sunday of that week.
I've been through every MySQL DATE function and I'm really stuck.
I normally do this on the client side using Python (see below) but I'd love to know if there's an easy way to do this in MySQL before trying to translate this. Any Help would be very much appreciated.
Python code if it makes what I'm trying to say a bit clearer:
from datetime import timedelta, date
from dateutil import relativedelta
NEXT_MONDAY = relativedelta.relativedelta(weekday=relativedelta.MO)
LAST_MONDAY = relativedelta.relativedelta(weekday=relativedelta.MO(-1))
ONE_WEEK = timedelta(weeks=1)
def get_week_in_quarter(dt):
d: date = dt.date()
year = d.year
# Q0 = January 1, Q1 = April 1, Q2 = July 1, Q3 = October 1
quarter = ((d.month - 1) // 3)
quarter_start = date(year, (quarter * 3) + 1, 1)
quarter_week_2_monday = quarter_start + NEXT_MONDAY
if d < quarter_week_2_monday:
week = 1
else:
cur_week_monday = d + LAST_MONDAY
week = int((cur_week_monday - quarter_week_2_monday) / ONE_WEEK) + 2
if quarter == 0:
year -= 1
quarter = 4
return week
EDIT 1:
To further clarify, if I separate out the weeks and days on a transition day (which today is), you can see the problem:
SET #day1=DATE(CURDATE());
SET #day2=DATE(MAKEDATE(YEAR(CURDATE()), 2) + INTERVAL QUARTER(CURDATE()) QUARTER - INTERVAL 1 QUARTER);
SELECT CONCAT(SUBSTRING_INDEX(CEIL(DATEDIFF(#day1,#day2)/7),'.',1),'Weeks ',
SUBSTRING_INDEX(ABS(DATEDIFF(#day1,#day2)),'.',1)-SUBSTRING_INDEX(ROUND(DATEDIFF(#day1,#day2))/7,'.',1)*7,'Days'
)AS WEEKINQUARTER
The output is:
8Weeks 4Days
I don't know how to produce the required output (just "9") by dividing the days difference by 7. In Python, I have to set a delta to the last Monday to account for that. I don't know how to do that in MySQL either.
EDIT 2
Strawberry's answer below solved this for me in SQL. I ended up using CEIL instead of floor but for anyone looking for the current Fiscal Quarter week calculation in SQL in the future, this is a solution:
Does this work?
SELECT FLOOR(DATEDIFF('2021-02-24',CONCAT(DATE_FORMAT(LAST_DAY(MAKEDATE(EXTRACT(YEAR FROM '2021-02-24'),1) + INTERVAL QUARTER('2021-02-24')*3-3 MONTH),'%Y-%m-'),'01') - INTERVAL WEEKDAY(CONCAT(DATE_FORMAT(LAST_DAY(MAKEDATE(EXTRACT(YEAR FROM '2021-02-24'),1) + INTERVAL QUARTER('2021-02-24')*3-3 MONTH),'%Y-%m-'),'01')) DAY)/7) x;
week() with mode 3 is the ISO week, which is what I think you want.
You can get the week of the quarter by using arithmetic. The first 13 weeks are in Q1, 14-26 in Q2, 27-39 in Q3 and the rest in Q4.
So:
select (case when week(date, 3) <= 13 then week(date, 3)
when week(date, 3) <= 26 then week(date, 3) - 13
when week(date, 3) <= 39 then week(date, 3) - 26
else week(date, 3) - 39
end)
There are tricks you can use to simplify this using modulo arithmetic, but I think this is clearest for explaining what to do.
I'm trying to solve a task: I have a table containing information about ships' battles. Battle is made of name and date. The problem is to get the last friday of the month when the battle occurred.
WITH num(n) AS(
SELECT 0
UNION ALL
SELECT n+1 FROM num
WHERE n < 31),
dat AS (
SELECT DATEADD(dd, n, CAST(battles.date AS DATE)) AS day,
dateadd(dd, 0, cast(battles.date as date)) as fight,
name FROM num,battles)
SELECT name, fight, max(day) FROM dat WHERE DATENAME(dw, day) = 'friday'
I thought there must be a maximum of date or something, but my code is wrong.
The result should look like this:
Please, help!!
P.S. DATE_FORMAT is not available
Possible problem: as spencer7593 noticed - and as I should have done and didn't - your original query is not MySQL at all. If you're porting a query that's OK. Otherwise this answer will not be helpful, as it makes use of MySQL functions.
The day you want is number 4 (0 being Sunday in MySQL).
So you want the last day of the month if the last day of the month is a 4; if the day of the month is a 5 you want a date which is 1 day earlier; if the day of the month is a 3 you want a date which is 1 day later, but that's impossible (the month ends), so you really need a date six days earlier.
This means that if the daynumber difference is negative, you want it modulo seven.
You can then build this expression (#DATE is your date; I use a fake date for testing)
SET #DATE='2015-02-18';
DATE_SUB(LAST_DAY(#DATE), INTERVAL ((WEEKDAY(LAST_DAY(#DATE))+7-4))%7 DAY);
It takes the last day of the month (LASTDAY(#DATE)), then it computes its weekday, getting a number from 0 to 6. Adds seven to ensure positivity after subtracting; then subtract the desired daynumber, in this case 4 for Friday.
The result, modulo seven, is the difference (always positive) from the last day's daynumber to the wanted daynumber. Since DATE_SUB(date, 0) returns the argument date, we needn't use IF.
SET #DATE='1962-10-20';
SELECT DATE_SUB(LAST_DAY(#DATE), INTERVAL ((WEEKDAY(LAST_DAY(#DATE))+7-4))%7 DAY) AS friday;
+------------+
| friday |
+------------+
| 1962-10-26 |
+------------+
Your query then would become something like:
SELECT `name`, `date`,
DATE_SUB(LAST_DAY(`date`),
INTERVAL ((WEEKDAY(LAST_DAY(`date`))+7-4))%7 DAY) AS friday
FROM battles;
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.
We are using MySQL as our database to store messages with timestamps. Is it possible to create a query that returns messages of the last n weekdays?
I.e. if n is 4 and today is Tuesday, I want messages from this weeks Monday, last weeks Friday, last weeks Thursday and last weeks Wednesday .
If you want to do this directly with mysql it would be a little complicated. As Vatev recommended you should calculate date star date in advance, but if you really want to do this, you'll probably need following functions:
ADD_DATE, with INTERVAL -N WEEKS
FLOOR, in C int/int would do just fine
MOD, a % b :)
WEEKDAY
First of all you need should count how many weeks you should go back, that's easy... For you one week = 5 days, that means
weeks = FLOOR(days / 5)
We've taken care of weeks, so we'll now have to work with the rest:
rest = days MOD 5
Now we have two cases, weekend has occurred or no, for the case that there wasn't weekend days are good. We have to add 2 days to skip it. The weekend occurred if (WEEKDAY(now) - rest) < 0
rest = IF( (WEEKDAY(now) - rest) < 0, rest + 2, rest)
And now we can build it to one par (let's assume you have {days} and {rest} pre-calculated):
WHERE date >= ADD_DATE(
ADD_DATE (
{now},
INTERVAL -IF( (WEEKDAY({now}) - {rest}) < 0, {rest} + 2, {rest}) DAYS,
),
INTERVAL -FLOOR({days} / 5) WEEKS
)
The best i can come up with is calculating the start date ({start_date} in the query) in the language of your choice and then running something like this:
SELECT some_things
FROM your_table
WHERE
WEEKDAY(time_column) < 5
AND time_column >= {start_date}
ORDER BY time_column DESC
You can also make a stored function to calculate 'the date x week days ago' and use it for {start_date}.
Have you tried something like this?
SELECT columns
FROM table
WHERE datediff(column_with_timestamp,NOW()) > n
Where N is as you defined above, the number of days you're looking for.
COL >= date_sub( NOW(), interval 1 week) and
weekday( COL ) < 5
date_sub is to seek rows created last week
weekday is to exclude sunday or saturday
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.