Best Week Ever

SQL Server provides the ability to find the week of the year in which a given date lies.  For example:

declare @d datetime
set @d = '13 november 2005'
select datepart(ww, @d)

The result of this is 47.

Now, the problem with this is that the correct result according to ISO 8601 is 45, not 47.  Why?  Because ISO 8601 specifies that:

  • The first day of the week is Monday, not Sunday.
  • The first week of the year is the first week with at least 4 days in it.

Unfortunately, SQL Server defines the first week of the year as the week in which 1 January lies.  (Some of us would argue that this makes more sense, but ISO 8601 says what ISO 8601 says.)

What's a poor database developer to do?  Wait until the next release of SQL Server?

Well, using VB.NET and CLR integration the solution is very easy.  Our friends over at VB.NET have included exactly the method we need: a very flexible DatePart method that we can wrap and load into SQL Server.

Here's the source:

Partial Public Class UserDefinedFunctions
<Microsoft.SqlServer.Server.SqlFunction()> _
Public Shared Function WeekInYear(ByVal dt As DateTime) As Integer
Return DatePart("ww", dt, FirstDayOfWeek.Monday, _
FirstWeekOfYear.FirstFourDays)
End Function
End Class

We can now rewrite our example:

declare @d datetime
set @d = '13 november 2005'
select dbo.WeekInYear(@d)

And the result is 45.  ISO 8601 would be proud.

Here's to better living through CLR integration.

Cheers,
-Isaac K. Kunen, Microsoft SQL Server

Comments

  • Anonymous
    November 13, 2005
    Is this function (WeekInYear) available in C# or its just in VB.NET?
  • Anonymous
    November 14, 2005
    The comment has been removed
  • Anonymous
    December 21, 2005
    Is there any reason why you can't just use SET DATEFIRST, or am I missing something?
  • Anonymous
    January 09, 2006
    And if your in c# and don't want to load all of VB runtime, then here is a native c# solution:

    using System;
    using System.Data;
    using System.Data.SqlClient;
    using System.Data.SqlTypes;
    using Microsoft.SqlServer.Server;

    public partial class UserDefinedFunctions
    {
    /// <summary>
    /// Gets ISO week number for the date.
    /// </summary>
    /// <param name="date">A DateTime.</param>
    /// <returns>The int week number that date falls within.</returns>
    [Microsoft.SqlServer.Server.SqlFunction]
    public static int WeekInYear(DateTime date)
    {
    return GetISOWeekInYear(date);
    }

    /// <summary>
    /// Returns the ISO week number of the year that date is within.
    /// </summary>
    /// <param name="date">DateTime representing a day in a week.</param>
    /// <returns>Week number.</returns>
    public static int GetISOWeekInYear(DateTime date)
    {
    int weekNum = 1;
    // Get the ISO week containing the 4th day of Jan. This will always be the first ISO week of the year.
    DateTime startOfWeek = GetStartOfWeekISO(new DateTime(date.Year, 1, 4));

    if (date.Date < startOfWeek)
    {
    // Date is before the first Monday of the year. Jan 1, 2005 and Jan 2 2005 are examples as they are in last week of 2004.
    return GetISOWeekInYear(new DateTime(date.Year-1, 12, 31));
    }

    DateTime endOfWeek = GetEndOfWeekISO(startOfWeek);
    while (true)
    {
    if (date <= endOfWeek)
    return weekNum;
    endOfWeek = endOfWeek.AddDays(7);
    weekNum++;
    }
    }

    /// <summary>
    /// Returns the first day of the week which is the Monday in the week represented by date.
    /// </summary>
    /// <param name="date">DateTime representing a day in the week.</param>
    /// <returns>DateTime of the Monday in the week.</returns>
    public static DateTime GetStartOfWeekISO(DateTime date)
    {
    date = date.Date;
    int dow = (int)date.DayOfWeek;
    if (dow == 0)
    dow = 7;

    // Get the Monday of this week.
    DateTime day = date.AddDays(-(dow - 1));
    return day;
    }

    /// <summary>
    /// Returns the last day of the ISO week which is the Sunday in the week represented by date.
    /// </summary>
    /// <param name="date">DateTime representing a day in the week.</param>
    /// <returns>DateTime of the Sunday in the week.</returns>
    public static DateTime GetEndOfWeekISO(DateTime date)
    {
    DateTime dt = GetStartOfWeekISO(date).AddDays(6);
    return GetEndOfDay(dt);
    }

    /// <summary>
    /// Returns the end of the day represented by date.
    /// This maximizes the hours, minutes, seconds, and milliseconds of the date instance.
    /// </summary>
    /// <param name="date">DateTime to operate on.</param>
    /// <returns>DateTime representing the end of the day.</returns>
    public static DateTime GetEndOfDay(DateTime date)
    {
    return new DateTime(date.Year, date.Month, date.Day, 23, 59, 59, 999);
    }
    };

    --William Stacey[MVP]
  • Anonymous
    January 09, 2006
    "Is there any reason why you can't just use SET DATEFIRST, or am I missing something?"

    Yes, it does not work for this. It returns 46, not 45.
  • Anonymous
    February 05, 2006
    The other day, colleague Shawn Steele posted in his blog about the ISO 8601 Week of Year format in Microsoft...
  • Anonymous
    May 21, 2006
    A possible refactoring (extend for year information) of Shawn Steele's code

       class ISOCalendar
       {
           // Need a calendar.  Culture's irrelevent since we specify start day of week
           private static Calendar cal = CultureInfo.InvariantCulture.Calendar;

           private static DateTime GetThursdayOfSameWeek(DateTime dt)
           {
               int dow = (int) cal.GetDayOfWeek(dt);
               int numOfDaysToAdd = -3; // The case if day of week is Sunday (dow==0)...
               if (dow != 0) // but if not Sunday...
               {
                   numOfDaysToAdd = 4 - dow;
               }
               return dt.AddDays((double)numOfDaysToAdd);
           }

           public static int GetIso8601WeekOfYear(DateTime dt)
           {
                           
               // Find the thursday
               DateTime thursday = GetThursdayOfSameWeek(dt);

               // Return the week of thursday
               return cal.GetWeekOfYear(thursday, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);            
           }

           // "YYYY–Www–D" Representation
           public static string GetYYYYWwwD(DateTime dt)
           {

               // Find the thursday
               DateTime thursday = GetThursdayOfSameWeek(dt);

               // Return the week of thursday
               int week = cal.GetWeekOfYear(thursday, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);

               // Return the year of thursday
               int year = cal.GetYear(thursday);

               // Return the day of week of original date;            
               int dow = (int) dt.DayOfWeek;
               if(dow == 0) dow=7;
               
               return year.ToString() + "-W" + week.ToString("00") + "-" + dow.ToString();
           }
       }
  • Anonymous
    July 20, 2006
    I tested this function and compared it with 2 T-SQL based solutions, and VB6 DatePart using a range comprised of 10k days starting from Jan 1, 2005. What I found are some differences on the following dates with respect to the CLR implementation. It appears that the VB6 Datepart function with a FirstDayofWeek = Monday and FirstWeekofYear = FirstFourDays is closest to the CLR results. However, which one (between VB6 and CLR) is actually correct?

    InputDate  CLR   TSQL1   TSQL2  VB6
    12/31/2007  53   1         1     same
    12/29/2008  53   1         1     1
    12/30/2008  53   1         1     1
    12/31/2008  53   1         1     1
    12/31/2012  53   1         1     1
    12/30/2013  53   1         1     1
    12/31/2013  53   1         1     1
    12/29/2014  53   1         1     1
    12/30/2014  53   1         1     1
    12/31/2014  53   1         1     1
    12/31/2018  53   1         1     1
    12/30/2019  53   1         1     same
    12/31/2019  53   1         1     1
    12/30/2024  53   1         1     1
    12/31/2024  53   1         1     1
    12/29/2025  53   1         1     1
    12/30/2025  53   1         1     1
    12/31/2025  53   1         1     1
    12/31/2029  53   1         1     1
    12/30/2030  53   1         1     1
    12/31/2030  53   1         1     1
    12/29/2031  53   1         1     same
    12/30/2031  53   1         1     1
    12/31/2031  53   1         1     1

    Regards,
    Tristan
  • Anonymous
    July 20, 2006
    This is in addition to my earlier post, in case anybody is wondering about the code behind TSQL1 and TSQL2. TSQL1 uses CREATE FUNCTION example A from SQL2000 Books online. TSQL2 uses code from a posting by Andrea Montanari, SQL Server MVP located here. http://groups.google.com/group/microsoft.public.sqlserver.msde/browse_thread/thread/4fd0f517628fc569/24139bb3cb9ee0be?lnk=st&q=&rnum=9#24139bb3cb9ee0be
  • Anonymous
    July 20, 2006
    I also found another VB6 article (http://support.microsoft.com/kb/q200299/) describing this. I have since modified my VB6 code to use the functions from this knowledgebase article. Lo and behold, it would now appear that the SQL CLR implementation does not have the correct values. TSQL1 and TSQL2 implementations now match the values reported by the VB6 code. I also mistakenly referenced the wrong link for TSQL2. The correct link is: http://www.sqlservercentral.com/columnists/chedgate/onthetrailoftheisoweek.asp Is this a bug in the CLR?
  • Anonymous
    July 21, 2006
    Interesting detective work.  I'm no expert in this, but it does look like a bug to me.  Just looking at the calendar, the result for 12/31/2007 looks like it should be 1, not 53.