Calculating the ISO week number for a date
published: Wed, 20-Aug-2003 | updated: Sat, 6-Aug-2016
Calculating the ISO week number for a particular date seems to cause problems for some developers. In actuality, the algorithm is very simple, but what seems to happen is that people try and implement it in one line of code. My answer is don't; I would doubt that your application's performance profile depends on calculating the week number ultra-quickly. Also, as you'll see, there are some boundary conditions that can trip you up.
So, how's it done? How do you calculate the ISO week number for a particular date?
What is an ISO week number?
First, we need to review what the ISO week number is. According to the ISO (International Standards Organization) in document ISO 8601, an ISO week starts on a Monday (which is counted as day 1 of the week), and week 1 for a given year is the week that contains the first Thursday in the year.
Calculating the date of ISO week 1 for a given year
If you play around with the numbers, you'll see how to calculate the date of the Monday for week 1. The first Thursday of the year will be either the 1st, 2nd, all the way up to the 7th of January. If it were the 1st, week 1 will start on the 29-Dec of the previous year (yes, this is correct: the ISO week 1 for a given year may have dates from the previous year); if the 2nd, week 1 will start on 30-Dec of the previous year; if the 3rd, 31-Dec; if the 4th, 1-Jan; if the 5th, 2-Jan; if the 6th, 3-Jan; and finally if the first Thursday were the 7th, week 1 would start on 4-Jan.
However, calculating the date of the first Thursday is hard. Well, not hard, but complicated. A better way is to see that the ISO week is so defined that the 4-Jan of every year is in week 1. In other words, that the first week must contain four or more days from the year (if 1-Jan were a Thursday, 4-Jan would be the Sunday, and hence would form week 1). So we calculate 4-Jan and work out which day of the week it is. If it's Thursday, week 1 starts three days earlier, if Friday, four days earlier, if Saturday, five days earlier, if Sunday, six days earlier. If it's a Monday, we found the week 1 start date straight away; if Tuesday, week 1 starts one day earlier, if Wednesday, two days earlier.
public static DateTime GetIsoWeekOne(int Year) { // get the date for the 4-Jan for this year DateTime dt = new DateTime(Year, 1, 4); // get the ISO day number for this date 1==Monday, 7==Sunday int dayNumber = (int) dt.DayOfWeek; // 0==Sunday, 6==Saturday if (dayNumber == 0) { dayNumber = 7; } // return the date of the Monday that is less than or equal // to this date return dt.AddDays(1 - dayNumber); }
Note that I'm assuming that the DayOfWeek
enumeration starts with Sunday. One the one hand this is dodgy programming behavior (the .NET Framework people may change this in the future, so I should use the actual enumerations in a switch statement), but on the other it's acceptable (it's documented like this and it's unlikely that the .NET Framework people would change it now).
(Also note that, according to Design by Contract principles, I should be validating the year value that's passed in. I'm cheating a little by letting the call to the DateTime constructor take care of the validation: if the year is out of range, it's this constructor that will throw an exception.)
Calculating the ISO week for an easy date
There are a couple of things to point out right away, I think. First is that 29-Dec, 30-Dec, and 31-Dec of a given year could actually be in the first week of the succeeding year, and second is that 1-Jan, 2-Jan, 3-Jan of a given year could be in the last week of the previous year.
Apart from those exceptional 6 days, it's pretty easy to calculate the week number for a given day: calculate the Monday of week 1 in the same year, subtract it from the date you're given to get the number of days in between, divide this by 7 (discarding the remainder) and add 1. The result is the week number.
Let's illustrate with a concrete example. This year (2003), week 1 started on 30-Dec-2002 (the first Thursday of 2003 was 2-Jan). Say we were trying to calculate the week number for Mon 3-Feb. Subtracting 30-Dec-2002 from 3-Feb-2003 gives 35 days. Divide this by 7 gives 5. Add 1 to give 6. 3-Feb is thus in week 6. By looking at a diary or a calendar, you can verify that this is correct. Another test: Sun 2-Feb. The difference in days is 34. Divide by 7 (discarding the remainder) gives 4. Add 1 to give us the answer that Sun 2-Feb is in week 5. The previous test will show us that this is true: the Sunday prior to a Monday is in a previous week.
Calculating the ISO week for a hard date
Having solved the problem for 359 (or 360) days of the year, we should now solve it for the 6 problematic days. (In other words a 98% success rate for an algorithm isn't good enough <g>.)
Let's look at the case of 1-Jan in depth. We need to see if it's counted as being in the previous year, so we calculate when week 1 of this year starts. If week 1 starts after 1-Jan then obviously 1-Jan is going to appear in the last week of the previous year. So we calculate the start date for week 1 of the previous year, subtract it from 1-Jan, divide by 7 and add 1. Obviously this algorithm will also work for 2-Jan and 3-Jan.
Next up, let's think about 31-Dec. This may appear in the first week of the following year. So calculate the start of week 1 of the following year. If 31-Dec is less than this, it will be in the last week of its year, and we'll use the standard algorithm to calculate it. If 31-Dec is greater than or equal to than the start of week 1 of the following year, it's obviously in that week. Notice that we initially need to calculate week 1 of the following year for 31-Dec, not week 1 of the current year. This is different from all the other dates. (Of course, the same argument and algorithm will apply for 29-Dec and 30-Dec.)
Implementing the ISO week calculation
Now we've analyzed the situation completely, we can implement the algorithm in code. Rather than return two values, one for the week number and the other for the year, I implemented the method to return a single int value of the form YYYYWW.
public static int GetIsoWeek(DateTime dt) { DateTime week1; int IsoYear = dt.Year; if (dt >= new DateTime(IsoYear, 12, 29)) { week1 = GetIsoWeekOne(IsoYear + 1); if (dt < week1) { week1 = GetIsoWeekOne(IsoYear); } else { IsoYear++; } } else { week1 = GetIsoWeekOne(IsoYear); if (dt < week1) { week1 = GetIsoWeekOne(--IsoYear); } } return (IsoYear * 100) + ((dt - week1).Days / 7 + 1); }
If you want the actual week number and year rather than this compound return value, use the following code:
IsoWeek = GetIsoWeek(myDate); int year = IsoWeek / 100; int week = IsoWeek % 100;
Summary
This algorithm turned out to be a case of thinking things through carefully and not losing sight of the boundary cases. Of the implementations I've seen, those that attempt to calculate the week number of a given date usually get it right, however, they lose sight of the fact that the year for the week may be different than the date's year, meaning that their answer could ultimately be wrong.
I did find one example on The Code Project that calculates the week number from first principles (as in, ignoring the date calculation methods in the .NET Framework, such as the ones I used) and gets the week number right, but always returns the date's year, despite pointing out in the text that the year may be different!