Recipe 9.6. Determining the Date of the First and Last Occurrence of a Specific Weekday in a MonthProblemYou want to find, for example, the first and last Mondays of the current month. SolutionThe choice to use Monday and the current month is arbitrary; you can use the solutions presented in this recipe for any weekday and any month. Because each weekday is seven days apart from itself, once you have the first instance of a weekday, you can add 7 days to get the second and 14 days to get the third. Likewise, if you have the last instance of a weekday in a month, you can subtract 7 days to get the third and subtract 14 days to get the second. DB2Use the recursive WITH clause to generate each day in the current month and use a CASE expression to flag all Mondays. The first and last Mondays will be the earliest and latest of the flagged dates: 1 with x (dy,mth,is_monday) 2 as ( 3 select dy,month(dy), 4 case when dayname(dy)='Monday' 5 then 1 else 0 6 end 7 from ( 8 select (current_date-day(current_date) day +1 day) dy 9 from t1 10 ) tmp1 11 union all 12 select (dy +1 day), mth, 13 case when dayname(dy +1 day)='Monday' 14 then 1 else 0 15 end 16 from x 17 where month(dy +1 day) = mth 18 ) 19 select min(dy) first_monday, max(dy) last_monday 20 from x 21 where is_monday = 1 OracleUse the functions NEXT_DAY and LAST_DAY, together with a bit of clever date arithmetic, to find the first and last Mondays of the current month: select next_day(trunc(sysdate,'mm')-1,'MONDAY') first_monday, next_day(last_day(trunc(sysdate,'mm'))-7,'MONDAY') last_monday from dual PostgreSQLUse the function DATE_TRUNC to find the first day of the month. Once you have the first day of the month, you can use simple arithmetic involving the numeric values of weekdays (SunSat is 17) to find the first and last Mondays of the current month: 1 select first_monday, 2 case to_char(first_monday+28,'mm') 3 when mth then first_monday+28 4 else first_monday+21 5 end as last_monday 6 from ( 7 select case sign(cast(to_char(dy,'d') as integer)-2) 8 when 0 9 then dy 10 when -1 11 then dy+abs(cast(to_char(dy,'d') as integer)-2) 12 when 1 13 then (7-(cast(to_char(dy,'d') as integer)-2))+dy 14 end as first_monday, 15 mth 16 from ( 17 select cast(date_trunc('month',current_date) as date) as dy, 18 to_char(current_date,'mm') as mth 19 from t1 20 ) x 21 ) y MySQLUse the ADDDATE function to find the first day of the month. Once you have the first day of the month, you can use simple arithmetic on the numeric values of weekdays (SunSat is 06) to find the first and last Mondays of the current month: 1 select first_monday, 2 case month(adddate(first_monday,28)) 3 when mth then adddate(first_monday,28) 4 else adddate(first_monday,21) 5 end last_monday 6 from ( 7 select case sign(dayofweek(dy)-2) 8 when 0 then dy 9 when -1 then adddate(dy,abs(dayofweek(dy)-2)) 10 when 1 then adddate(dy,(7-(dayofweek(dy)-2))) 11 end first_monday, 12 mth 13 from ( 14 select adddate(adddate(current_date,-day(current_date)),1) dy, 15 month(current_date) mth 16 from t1 17 ) x 18 ) y SQL ServerUse the recursive WITH clause to generate each day in the current month, and then use a CASE expression to flag all Mondays. The first and last Mondays will be the earliest and latest of the flagged dates: 1 with x (dy,mth,is_monday) 2 as ( 3 select dy,mth, 4 case when datepart(dw,dy) = 2 5 then 1 else 0 6 end 7 from ( 8 select dateadd(day,1,dateadd(day,-day(getdate( )),getdate( ))) dy, 9 month(getdate( )) mth 10 from t1 11 ) tmp1 12 union all 13 select dateadd(day,1,dy), 14 mth, 15 case when datepart(dw,dateadd(day,1,dy)) = 2 16 then 1 else 0 17 end 18 from x 19 where month(dateadd(day,1,dy)) = mth 20 ) 21 select min(dy) first_monday, 22 max(dy) last_monday 23 from x 24 where is_monday = 1 DiscussionDB2 and SQL ServerDB2 and SQL Server use different functions to solve this problem, but the technique is exactly the same. If you eyeball both solutions you'll see the only difference between the two is the way dates are added. This discussion will cover both solutions, using the DB2 solution's code to show the results of intermediate steps.
The first step in finding the first and last Mondays of the current month is to return the first day of the month. Inline view TMP1 in recursive view X finds the first day of the current month by first finding the current date, specifically, the day of the month for the current date. The day of the month for the current date represents how many days into the month you are (e.g., April 10th is the 10th day of the April). If you subtract this day of the month value from the current date, you end up at the last day of the previous month (e.g., subtracting 10 from April 10th puts you at the last day of March). After this subtraction, simply add one day to arrive at the first day of the current month: select (current_date-day(current_date) day +1 day) dy from t1 DY ----------- 01-JUN-2005 Next, find the month for the current date using the MONTH function and a simple CASE expression to determine whether or not the first day of the month is a Monday: select dy, month(dy) mth, case when dayname(dy)='Monday' then 1 else 0 end is_monday from ( select (current_date-day(current_date) day +1 day) dy from t1 ) tmp1 DY MTH IS_MONDAY ----------- --- ---------- 01-JUN-2005 6 0 Then use the recursive capabilities of the WITH clause to repeatedly add one day to the first day of the month until you're no longer in the current month. Along the way, you will use a CASE expression to determine which days in the month are Mondays (Mondays will be flagged with "1"). A portion of the output from recursive view X is shown below: with x (dy,mth,is_monday) as ( select dy,month(dy) mth, case when dayname(dy)='Monday' then 1 else 0 end is_monday from ( select (current_date-day(current_date) day +1 day) dy from t1 ) tmp1 union all select (dy +1 day), mth, case when dayname(dy +1 day)='Monday' then 1 else 0 end from x where month(dy +1 day) = mth ) select * from x DY MTH IS_MONDAY ----------- --- ---------- 01-JUN-2005 6 0 02-JUN-2005 6 0 03-JUN-2005 6 0 04-JUN-2005 6 0 05-JUN-2005 6 0 06-JUN-2005 6 1 07-JUN-2005 6 0 08-JUN-2005 6 0 … Only Mondays will have a value of 1 for IS_MONDAY, so the final step is to use the aggregate functions MIN and MAX on rows where IS_MONDAY is 1 to find the first and last Mondays of the month. OracleThe function NEXT_DAY makes this problem easy to solve. To find the first Monday of the current month, first return the last day of the prior month via some date arithmetic involving the TRUNC function: select trunc(sysdate,'mm')-1 dy from dual DY ----------- 31-MAY-2005 Then use the NEXT_DAY function to find the first Monday that comes after the last day of the previous month (i.e., the first Monday of the current month): select next_day(trunc(sysdate,'mm')-1,'MONDAY') first_monday from dual FIRST_MONDAY ------------ 06-JUN-2005 To find the last Monday of the current month, start by returning the first day of the current month by using the TRUNC function: select trunc(sysdate,'mm') dy from dual DY ----------- 01-JUN-2005 The next step is to find the last week (the last seven days) of the month. Use the LAST_DAY function to find the last day of the month, and then subtract seven days: select last_day(trunc(sysdate,'mm'))-7 dy from dual DY ----------- 23-JUN-2005 If it isn't immediately obvious, you go back seven days from the last day of the month to ensure that you will have at least one of any weekday left in the month. The last step is to use the function NEXT_DAY to find the next (and last) Monday of the month: select next_day(last_day(trunc(sysdate,'mm'))-7,'MONDAY') last_monday from dual LAST_MONDAY ----------- 27-JUN-2005 PostgreSQL and MySQLPostgreSQL and MySQL also share the same solution approach. The difference is in the functions that you invoke. Despite their lengths, the respective queries are extremely simple; little overhead is involved in finding the first and last Mondays of the current month. The first step is to find the first day of the current month. The next step is to find the first Monday of the month. Since there is no function to find the next date for a given weekday, you need to use a little arithmetic. The CASE expression beginning on line 7 (of either solution) evaluates the difference between the numeric value for the weekday of the first day of the month and the numeric value corresponding to Monday. Given that the function TO_CHAR (PostgresSQL), when called with the 'D' or 'd' format, and the function DAYOFWEEK (MySQL) will return a numeric value from 1 to 7 representing days Sunday to Saturday; Monday is always represented by 2. The first test evaluated by CASE is the SIGN of the numeric value of the first day of the month (whatever it may be) minus the numeric value of Monday (2). If the result is 0, then the first day of the month falls on a Monday and that is the first Monday of the month. If the result is1, then the first day of the month falls on a Sunday and to find the first Monday of the month simply add the difference in days between 2 and 1 (numeric values of Monday and Sunday, respectively) to the first day of the month.
If the result from SIGN is 1, then the first day of the month falls between Tuesday and Saturday (inclusive). When the first day of the month has a numeric value greater than 2 (Monday), subtract from 7 the difference between the numeric value of the first day of the month and the numeric value of Monday (2), and then add that value to the first day of the month. You will have arrived at the day of the week that you are after, in this case Monday.
The idea behind the CASE expression is to create a sort of a "next day" function for PostgreSQL and MySQL. If you do not start with the first day of the month, the value for DY will be the value returned by CURRENT_DATE and the result of the CASE expression will return the date of the next Monday starting from the current date (unless CURRENT_DATE is a Monday, then that date will be returned). Now that you have the first Monday of the month, add either 21 or 28 days to find the last Monday of the month. The CASE expression in lines 25 determines whether to add 21 or 28 days by checking to see whether 28 days takes you into the next month. The CASE expression does this through the following process:
|