views:

1274

answers:

6

Hi there

According to the official (gregorian) calendar, the week number for 29/12/2008 is 1, because after the last day of week 52 (i.e. 28/12) there are three or less days left in the year. Kinda weird, but OK, rules are rules.

So according to this calendar, we have these boundary values for 2008/2009

  • 28/12 is week 52
  • 29/12 is week 1
  • 1/1 is week 1
  • 8/1 is week 2

C# offers a GregorianCalendar class, that has a function GetWeekOfYear(date, rule, firstDayOfWeek).

The parameter rule is an enumeration with 3 possible values: FirstDay, FirstFourWeekDay, FirstFullWeek. From what I've understood I should go for the FirstFourWeekDay rule, but I tried all of them just in case.

The last parameter informs which week day should be considered the first day of the week, according to that calendar it's Monday so Monday it is.

So I fired up a quick and dirty console app to test this:

using System;
using System.Globalization;

namespace CalendarTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var cal = new GregorianCalendar();
            var firstWeekDay = DayOfWeek.Monday;
            var twentyEighth = new DateTime(2008, 12, 28);
            var twentyNinth = new DateTime(2008, 12, 29);
            var firstJan = new DateTime(2009, 1, 1);
            var eightJan = new DateTime(2009, 1, 8);
            PrintWeekDays(cal, twentyEighth, firstWeekDay);
            PrintWeekDays(cal, twentyNinth, firstWeekDay);
            PrintWeekDays(cal, firstJan, firstWeekDay);
            PrintWeekDays(cal, eightJan, firstWeekDay);
            Console.ReadKey();
        }

        private static void PrintWeekDays(Calendar cal, DateTime dt, DayOfWeek firstWeekDay)
        {
            Console.WriteLine("Testing for " + dt.ToShortDateString());
            Console.WriteLine("--------------------------------------------");
            Console.Write(CalendarWeekRule.FirstDay.ToString() + "\t\t");
            Console.WriteLine(cal.GetWeekOfYear(dt, CalendarWeekRule.FirstDay, firstWeekDay));
            Console.Write(CalendarWeekRule.FirstFourDayWeek.ToString() + "\t");
            Console.WriteLine(cal.GetWeekOfYear(dt, CalendarWeekRule.FirstFourDayWeek, firstWeekDay));
            Console.Write(CalendarWeekRule.FirstFullWeek.ToString() + "\t\t");
            Console.WriteLine(cal.GetWeekOfYear(dt, CalendarWeekRule.FirstFullWeek, firstWeekDay));
            Console.WriteLine("--------------------------------------------");
        }
    }
}

... and this what I get

Testing for 28.12.2008
--------------------------------------------
FirstDay                52
FirstFourDayWeek        52
FirstFullWeek           51
--------------------------------------------
Testing for 29.12.2008
--------------------------------------------
FirstDay                53
FirstFourDayWeek        53
FirstFullWeek           52
--------------------------------------------
Testing for 01.01.2009
--------------------------------------------
FirstDay                1
FirstFourDayWeek        1
FirstFullWeek           52
--------------------------------------------
Testing for 08.01.2009
--------------------------------------------
FirstDay                2
FirstFourDayWeek        2
FirstFullWeek           1
--------------------------------------------

So as we see, none of the combinations above matches the official calendar (if you are in a hurry, just see that 29/12 never gets week #1).

What am I getting wrong here? Maybe there's something glaring that I am missing? (it's Friday and late work hours here in Belgium, bear with me ;))

Edit: Maybe I should explain: what I need is a function that works for any year, returning the same results as the gregorian calendar I linked. So no special workarounds for 2008.

+1  A: 

In my experience, the demonstrated behavior is the typical behavior, referring to the partial final week as week 53. This may be because all significant exposure I've had to week numbers has been related to accounting the end of the calendar year for reporting purposes, and the IRS (or tax agency of your choice) considers the calendar year to end on 31 December, not the last full week of the year.

Greg D
+2  A: 

Week numbers differ from country to country and should depend on your locale/regional settings if I'm not entirely wrong.

Edit: Wikipedia supports my vague recollection that these numbers differ based on country: http://en.wikipedia.org/wiki/Week_number#Week_number

I would expect a respectable framework to obey the COUNTRY selected in your local runtime.

krosenvold
Hm, could be. I will see if I get things right if I fix it on en-US cultureinfo.
rodbv
Did some tests and some MSDN reading: the culture is used to inform the 2nd and 3rd parameters of the GetWeekOfYear, rule and firstdayofweek. But since I am providing those, the culture becomes irrelevant.
rodbv
A: 

As a work around, you could say the week number is WeekNumber mod 52. I believe this would work for the cases you describe.

Kibbee
It could solve for this case but I need a general algorithm
rodbv
and besides that, there are years with 53 weeks, e.g. 2009
rodbv
A: 

As a workaround, why not use FirstFourDayWeek but add:

  if ( weekNumber > 52 )
    weekNumber = 1;
Mike Scott
I need a general algorithm..there are cases that the year has officially 53 weeks (2009 is like that, IIRC)
rodbv
+13  A: 

This article looks deeper into the issue and possible workarounds. The hub of the matter is that the .NET calendar implementation does not seem to faithfully implement the ISO standard

Conrad
hmm... MS disobeying standards? I'm shocked.
rmeador
Looking promising :) unfortunately I can't test this now, but I'll do it tomorrow.
rodbv
rmeador: from what I've seen Java has the same issue...
rodbv
I've tested rodbv's code with the solution in the article and it returns the correct results.
Jesper Palm
It also caught me by surprise in a project I was doing for a client who operates using week numbers. The article helped out quite a bit. You can read this article for more info http://en.wikipedia.org/wiki/ISO_week_date
Conrad
+2  A: 

@Conrad is correct. The .NET implementation of DateTime and the GregorianCalendar don't implement/follow the full ISO 8601 spec. That being said, they spec is extremely detailed and non-trivial to implement fully, at least for the parsing side of things.

Some more information is available at the following sites:

Simply put:

A week is identified by its number in a given year and begins with a Monday. The first week of a year is the one which includes the first Thursday, or equivalently the one which includes January 4.

Here is part of the code I use for properly handling ISO 8601 dates:

    #region FirstWeekOfYear
    /// <summary>
    /// Gets the first week of the year.
    /// </summary>
    /// <param name="year">The year to retrieve the first week of.</param>
    /// <returns>A <see cref="DateTime"/>representing the start of the first
    /// week of the year.</returns>
    /// <remarks>
    /// Week 01 of a year is per definition the first week that has the Thursday 
    /// in this year, which is equivalent to the week that contains the fourth
    /// day of January. In other words, the first week of a new year is the week
    /// that has the majority of its days in the new year. Week 01 might also 
    /// contain days from the previous year and the week before week 01 of a year
    /// is the last week (52 or 53) of the previous year even if it contains days 
    /// from the new year.
    /// A week starts with Monday (day 1) and ends with Sunday (day 7). 
    /// </remarks>
    private static DateTime FirstWeekOfYear(int year)
    {
        int dayNumber;

        // Get the date that represents the fourth day of January for the given year.
        DateTime date = new DateTime(year, 1, 4, 0, 0, 0, DateTimeKind.Utc);

        // A week starts with Monday (day 1) and ends with Sunday (day 7).
        // Since DayOfWeek.Sunday = 0, translate it to 7. All of the other values
        // are correct since DayOfWeek.Monday = 1.
        if (date.DayOfWeek == DayOfWeek.Sunday)
        {
            dayNumber = 7;
        }
        else
        {
            dayNumber = (int)date.DayOfWeek;
        }

        // Since the week starts with Monday, figure out what day that 
        // Monday falls on.
        return date.AddDays(1 - dayNumber);
    }

    #endregion

    #region GetIsoDate
    /// <summary>
    /// Gets the ISO date for the specified <see cref="DateTime"/>.
    /// </summary>
    /// <param name="date">The <see cref="DateTime"/> for which the ISO date
    /// should be calculated.</param>
    /// <returns>An <see cref="Int32"/> representing the ISO date.</returns>
    private static int GetIsoDate(DateTime date)
    {
        DateTime firstWeek;
        int year = date.Year;

        // If we are near the end of the year, then we need to calculate
        // what next year's first week should be.
        if (date >= new DateTime(year, 12, 29))
        {
            if (date == DateTime.MaxValue)
            {
                firstWeek = FirstWeekOfYear(year);
            }
            else
            {
                firstWeek = FirstWeekOfYear(year + 1);
            }

            // If the current date is less than next years first week, then
            // we are still in the last month of the current year; otherwise
            // change to next year.
            if (date < firstWeek)
            {
                firstWeek = FirstWeekOfYear(year);
            }
            else
            {
                year++;
            }
        }
        else
        {
            // We aren't near the end of the year, so make sure
            // we're not near the beginning.
            firstWeek = FirstWeekOfYear(year);

            // If the current date is less than the current years
            // first week, then we are in the last month of the
            // previous year.
            if (date < firstWeek)
            {
                if (date == DateTime.MinValue)
                {
                    firstWeek = FirstWeekOfYear(year);
                }
                else
                {
                    firstWeek = FirstWeekOfYear(--year);
                }
            }
        }

        // return the ISO date as a numeric value, so it makes it
        // easier to get the year and the week.
        return (year * 100) + ((date - firstWeek).Days / 7 + 1);
    }

    #endregion

    #region Week
    /// <summary>
    /// Gets the week component of the date represented by this instance.
    /// </summary>
    /// <value>The week, between 1 and 53.</value>
    public int Week
    {
        get
        {
            return this.isoDate % 100;
        }
    }
    #endregion

    #region Year
    /// <summary>
    /// Gets the year component of the date represented by this instance.
    /// </summary>
    /// <value>The year, between 1 and 9999.</value>
    public int Year
    {
        get
        {
            return this.isoDate / 100;
        }
    }
    #endregion
Scott Dorman