In my February and March columns, I talked about challenges related to datetime calculations. I presented techniques for separating the date and time parts of a datetime value, returning the first/last day of the month, and calculating a language-independent weekday number. Now, I want to offer some techniques for calculating the date of the last/next occurrences of a weekday related to a specific date (call it the event date)—for example, calculating the date of the most recent Monday related to today.

All the techniques I present in this article will be based on the calculations of a language-independent weekday number, which I discussed last month. As a reminder, I presented two techniques to calculate a language-independent weekday number. One technique was based on calculating the offset in terms of days between a base date and the event date (call it diff). The base date had the same weekday as the one you wanted to set as the logical first day of the week (e.g., the string 19000101 or the integer 0 for Monday). When converting the integer 0 to a datetime value, you get the base date January 1, 1900, which happens to be a Monday. So 0 represents a date that falls on a Monday, 1 represents a date that falls on a Tuesday, and so on. The expression diff % 7 + 1 produced the weekday number. For example, to calculate the weekday of today's date, assuming Monday as the first day of the week, you use the expression

SELECT DATEDIFF(day, 0, GETDATE()) % 7 + 1;

If today happens to be a Tuesday, the above expression would return 2. In this article, I refer to this technique as datediff-based.

Another technique I presented was based on neutralizing the impact of the DATEFIRST setting on the DATEPART calculation. The expression I used added @@DATEFIRST days to the event date and subtracted a constant representing the logical first day of the week that you want to use. For example, to calculate the weekday of today's date, assuming Monday as the first day of the week, you use the expression

SELECT DATEPART(weekday, GETDATE() + @@DATEFIRST - 1);

Again, if today happens to be a Tuesday, the above expression would return 2. In this article, I refer to this technique as datepart-based.

Last Occurrence of a Weekday

As long as the datediff-based and datepart-based expressions are completely clear to you, we can proceed. You're now equipped to write expressions that calculate the last or next occurrence of a weekday.

Datediff. Suppose today's date is December 19, 2006 (a Tuesday), and you want to calculate the last occurrence of Monday, which might be today (that is, the calculation is inclusive). So if today is a Monday, the expression would return today's date. Because I'm assuming in my example that today's date is December 19, 2006 (Tuesday), the expression should return the most recent occurrence of Monday, which is yesterday. Here's the datediff-based expression that returns the date of the most recent Monday:

-- Last Monday (inclusive)
  SELECT DATEADD(day, DATEDIFF
  (day, 0, -- Base Monday date
  GETDATE()) /7*7, 0); -- Base
  Monday date

The DATEDIFF function calculates the difference in terms of days between a base date, which is a Monday, and today's date (call it diff). The expression then divides diff by 7 and multiplies by 7, practically subtracting the number of days that passed since the most recent Monday (call the result floored_diff). Finally, the expression adds floored_diff days to the base date, returning the date of last Monday.

As expected, the expression returns last Monday's date, assuming today is December 19, 2006:

2006-12-18 00:00:00.000

As a side note, to validate the expressions in this article, you can explicitly specify 20061219 as the date instead of GETDATE().

To return the most recent Tuesday's date, provide a date that falls on a Tuesday as the base date (represented by the integer 1 or the string 19000102):

SELECT DATEADD(day, DATEDIFF(day,
  1, GETDATE())/7*7, 1);

You'll get today's date (December 19, 2006) as output because the calculation is inclusive:

2006-12-19 00:00:00.000

To return the most recent Sunday's date, provide a date that falls on a Sunday as the base date (represented by the integer 6 or the string 19000107):

SELECT DATEADD(day, DATEDIFF(day,
  6, GETDATE()) /7*7, 6);

You get the output

2006-12-17 00:00:00.000

Another flooring technique that you can use instead of dividing diff by 7 and then multiplying it by 7 is to subtract from diff a number representing the offset (in terms of days) of "today's day of the week" from "the day of the week you need." For example, if the requested day of the week is Tuesday (represented by the integer 1 or the string 19000102), this offset would be expressed as

DATEDIFF(day, 1 /* Base Tuesday
  Date */, GETDATE()) % 7

Call this expression offset. Now, embed offset as part of an expression that calculates the most recent occurrence of a requested day of the week:

-- Don't run
  SELECT DATEADD(day, DATEDIFF(day,
  0 /* Base Date */, GETDATE())
  - diff - offset, 0 /* Base Date
  */);

In this case—unlike the expression calculating offset—the base date you use doesn't really matter, as long as you specify the same base date in both the DATEDIFF function and the DATEADD function. The complete expression looks like

-- Last Tuesday (inclusive)
  SELECT DATEADD(day, DATEDIFF
  (day, 0 /* Any Base Date
  */, GETDATE()) -- diff
  (DATEDIFF(day, 1 /* Base
  Tuesday Date */, GETDATE()) %
  7), -- offset 0 /* Any Base
  Date */);

If you're after the most recent Sunday's date, all you need to do is specify a Sunday base date (the integer 6 or the string 19000107) instead of a Tuesday base date:

-- Last Sunday (Inclusive)
  SELECT DATEADD(day, DATEDIFF(day,
  0 /* Any Base Date */,
  GETDATE()) - (DATEDIFF(day,
  6 /* Base Sunday Date */,
  GETDATE()) % 7), 0 /* Any Base
  Date */);

Datepart. You can also rely on the datepart-based technique to calculate offset. Remember that the datepart-based technique calculates a language-independent weekday number. If you think about it, offset is nothing more than a weekday number minus one, assuming the weekday you're looking for is the first day of the week. The following expression uses the datepart-based technique to calculate a language-independent weekday number, assuming Monday is the first day of the week:

DATEPART(weekday, GETDATE() + @@
  DATEFIRST - 1 /* datefirst is
  Monday */)

Don't confuse this expression's constant (1 for Monday) with the constants in the datediff-based technique. This time, the constant doesn't represent a base date as an integer; rather, it represents the logical first day of the week you want to set—1 for Monday, 2 for Tuesday, and so on. So, to get offset (weekday number minus one), assuming Monday is the first day of the week, use the expression

DATEPART(weekday, GETDATE() + @@
  DATEFIRST - 1 /* datefirst is
  Monday */) - 1

To get offset assuming Tuesday as the first day of the week, subtract the constant 2 from @@DATEFIRST:

DATEPART(weekday, GETDATE() + @@
  DATEFIRST - 2 /* datefirst is
  Tuesday */) - 1

Finally, embed the new offset calculation in the complete expression that returns the last occurrence of a weekday—for example, the most recent Tuesday:

-- Last Tuesday
  SELECT DATEADD(day, DATEDIFF(day,
  0 /* Base Date */, GETDATE()) -
  (DATEPART(weekday, GETDATE() +
  @@DATEFIRST - 2 /* datefirst is
  Tuesday */) - 1), 0 /* Base
  Date */);

To return last Sunday's date, subtract the constant 7 from @@DATEFIRST:

-- Last Sunday
  SELECT DATEADD(day, DATEDIFF(day,
  0 /* Base Date */, GETDATE()) -
  (DATEPART(weekday, GETDATE() +
  @@DATEFIRST - 7 /* datefirst is
  Sunday */) - 1), 0 /* Base
  Date */);

I find this article's first datediff-based technique to be the most elegant way to calculate the last occurrence of a weekday. Doubtless, it's the shortest. So, from this point on, I'll rely on this technique for other calculations as well. As a reminder, here's the expression I used to calculate the last occurrence of a Monday:

SELECT DATEADD(day, DATEDIFF(day,
  0, GETDATE()) /7*7, 0);

Twice, you specify a base date representing the weekday you're after (the integer 0 or the string 19000101 for a Monday base date).

Suppose you need the calculation of the last occurrence of a weekday to be exclusive—not to take the event date into consideration. All you need to do is subtract 1 from the event date. For example, to produce the most recent occurrence of Monday in an exclusive manner, you would use the expression

-- Last Monday (exclusive)
  SELECT DATEADD(day, DATEDIFF(day,
  0, GETDATE()-1) /7*7, 0);

This method is applicable to all the techniques that I demonstrated earlier. Simply subtract 1 from the event date to make the calculation exclusive.

Next Occurrence of a Weekday

If you need to produce the next occurrence of a weekday corresponding to a given event date, you can rely on techniques that are similar to those I presented for last occurrence. You might think that to produce the next occurrence of a weekday, all you need to do is add 7 days to the calculation of the last occurrence of that weekday. But the calculation is trickier than you think. To make the calculation of the next occurrence inclusive, you need to add 7 days to the exclusive calculation of the last occurrence. Think about it: If today is Tuesday, the inclusive calculation of the last occurrence of Tuesday will yield today's date. When you add 7 days, you end up with a Tuesday date a week ahead of today—not with today's date. However, if you use the exclusive calculation of last Tuesday's date, you will get the Tuesday date a week ago. Adding 7 days gets you today's date, effectively making the calculation of the next occurrence of a weekday inclusive. Similarly, to make the calculation of the next occurrence exclusive, you need to add 7 days to the inclusive calculation of the last occurrence.

To clarify this idea, Figure 1 shows a few examples, assuming today's date is December 19, 2006 (Tuesday). As you can probably figure out, instead of adding 7 days, you can add (or subtract) any multiplication of 7 days to get the next/last occurrence of a weekday several weeks ahead/ago.

Figure 1: Calculation of next weekday occurrence

Messing with Your Head

You might think my goal is to mess with your head. Unfortunately, there are no built-in functions that perform such calculations, and the techniques I'm sharing are as simple as you can get. Because calculations such as the last/next occurrence of a weekday are frequently needed, it's good to be familiar with the techniques I've presented. But we're not done yet. Next month, I'll discuss datetime-related calculations further. If you think the calculations in this article were tricky, just wait. In the meantime, get involved! Do you have datetime solutions you'd like to share? Check out the sidebar "Share Your DATETIME Thoughts" for some great reader responses.