DATETIME Calculations, Part 3

Calculate last/next occurrences of a weekday

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,<br>  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.

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.

Discuss this Article 3

Diana (not verified)
on Apr 6, 2007
Nice idea, Peter! Thanks for sharing… Regards, Itzik
Diana (not verified)
on Apr 6, 2007
The following feedback was sent to Izik by reader Peter Larsson ... Diana I read your third article today about DATETIMEs. I recently too wrote an article about DATEDIFF function here http://www.sqlteam.com/item.asp?ItemID=26922 In the following conversation, I put together a little test script for getting last day of a period, such as MONTH, QUARTER and YEAR. This without your extra substractions. See http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=80880 The trick is to use the date -1, which incidentally is December 31, 1899. The trick is the 31 days in December. SELECT CURRENT_TIMESTAMP AS Now, Status, DATEADD(MONTH, DATEDIFF(MONTH, StartingPoint1, CURRENT_TIMESTAMP), StartingPoint2) AS [MONTH], DATEADD(QUARTER, DATEDIFF(QUARTER, StartingPoint1, CURRENT_TIMESTAMP), StartingPoint2) AS [QUARTER], DATEADD(YEAR, DATEDIFF(YEAR, StartingPoint1, CURRENT_TIMESTAMP), StartingPoint2) AS [YEAR] FROM ( SELECT 'End Previous' AS Status, 0 AS StartingPoint1, -1 AS StartingPoint2 UNION ALL SELECT 'Start Current', 0, 0 UNION ALL SELECT 'End Current', -1, -1 UNION ALL SELECT 'Start Next', -1, 0 ) AS x ORDER BY 5 I like your Black-belt articles, but I thought this time your calculations could be made simpler. Peter Larsson peso@developerworkshop.net
Marco (not verified)
on Mar 30, 2007
dear Itzik, it would be nice to learn from you how to manage datetime information in UTC format. I'm currently involved with the consolidation of a system, where datetime fields are expressed in local format (time zone) and have to be transformed consistently in UTC form. Good matter for a new article of yours! Thank you marco buttazzoni

Please or Register to post comments.

IT/Dev Connections

Las Vegas
September 30th - October 4th

Paul ThurottOur Experts will show you:
• Common SQL Server
Problems
• Best Practices for T-SQL
• SQL Server Integration
Services
• Database Development

Come See Michael Otey & Tim Ford in Person!

Early Registration Now Open

From the Blogs
May 21, 2013
blog

A Common Misconception about MAXDOP

Out of the box, SQL Server is (and has been) able to take advantage of multiple processors/cores without any effort on behalf of administrators....More
May 9, 2013
blog

My ISO 8601-Compliant Signature 2

My family recently just "officially" announced that we're in the process of adopting a child from South Africa. We're quite excited, of course, but there's a ton of paperwork to do—along with the need for gobs of signatures....More
May 8, 2013
blog

Use SSIS for ETL from Hadoop

In this blog post, Mark Kromer walks you through using SSIS as a way to use ETL techniques using Microsoft's Hadoop on Windows (HDInsight) as a source using Hive connectors...More
SQL Server Pro Forums

Get answers to questions, share tips, and engage with the SQL Server community in our Forums.