ISO 8601 compliant week numbers

Reason

The .NET Framework doesn’t correctly determine the ISO 8601 compliant number of a week within a year, as outlined in http://support.microsoft.com/kb/200299. This is a problem when working within the EU especially, as most countries here use ISO 8601 week numbers.

Although there are a lot of code samples to be found on e.g. Stack Overflow solving the problem, I found most of them to involve too many magic numbers to be acceptable for production code.

Code

The following functionality was inspired by the concept of week dates and is comprised of a Week and WeekDate class. Source code and tests can also be found on GitHub.

Week

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

public class Week : IComparable<Week>, IEquatable<Week>
{
    private const DayOfWeek FirstDayOfWeek = DayOfWeek.Monday;
    private const DayOfWeek LastDayOfWeek = DayOfWeek.Sunday;
    private const DayOfWeek PivotDayOfWeek = DayOfWeek.Thursday;
    private static readonly Calendar Calendar = CultureInfo.InvariantCulture.Calendar;

    public Week(DateTime date)
    {
        StartOfWeek = GetStartOfWeek(date);
        EndOfWeek = GetEndOfWeek(date);
        DateTime pivotDayOfWeek = GetPivotDayOfWeek(StartOfWeek);
        WeekNumber = Calendar.GetWeekOfYear(pivotDayOfWeek, CalendarWeekRule.FirstFourDayWeek, FirstDayOfWeek);
        WeekYear = pivotDayOfWeek.Year;
    }

    public DateTime StartOfWeek
    {
        get;
        private set;
    }

    public DateTime EndOfWeek
    {
        get;
        private set;
    }

    public int WeekNumber
    {
        get;
        private set;
    }

    public int WeekYear
    {
        get;
        private set;
    }

    private DateTime GetStartOfWeek(DateTime date)
    {
        while (date.DayOfWeek != FirstDayOfWeek)
        {
            date = date.AddDays(-1);
        }
        return date;
    }

    private DateTime GetEndOfWeek(DateTime date)
    {
        while (date.DayOfWeek != LastDayOfWeek)
        {
            date = date.AddDays(1);
        }
        return date;
    }

    private DateTime GetPivotDayOfWeek(DateTime startOfWeek)
    {
        while (startOfWeek.DayOfWeek != PivotDayOfWeek)
        {
            startOfWeek = startOfWeek.AddDays(1);
        }
        return startOfWeek;
    }

    /// <summary>
    /// Returns a sortable ISO week string (e.g. "2009-W53").
    /// </summary>
    public override string ToString()
    {
        return string.Format("{0}-W{1:00}", WeekYear, WeekNumber);
    }

    public int CompareTo(Week other)
    {
        return other == null ? -1 : string.Compare(ToString(), other.ToString(), StringComparison.Ordinal);
    }

    public bool Equals(Week other)
    {
        return other != null && string.Equals(ToString(), other.ToString(), StringComparison.Ordinal);
    }

    public override bool Equals(object obj)
    {
        return Equals(obj as Week);
    }

    public override int GetHashCode()
    {
        return ToString().GetHashCode();
    }

    public static IList<Week> GetWeeks(DateTime from, DateTime to)
    {
        List<Week> weeks = new List<Week>();
        weeks.Add(new Week(from));
        while (weeks.Last().EndOfWeek.Date < to.Date)
        {
            DateTime startOfNextWeek = weeks.Last().EndOfWeek.AddDays(1);
            weeks.Add(new Week(startOfNextWeek));
        }
        return weeks;
    }
}

WeekDate

using System;
using System.Collections.Generic;
using System.Linq;

public class WeekDate : Week
{
    private readonly DateTime _date;

    public WeekDate(DateTime date)
        : base(date)
    {
        _date = date;
        DayNumber = GetDayNumber(date.DayOfWeek);
    }

    public int DayNumber
    {
        get;
        private set;
    }

    /// <summary>
    /// Returns a sortable ISO week date string (e.g. "2009-W53-2").
    /// </summary>
    public override string ToString()
    {
        return string.Format("{0}-W{1:00}-{2}", WeekYear, WeekNumber, DayNumber);
    }

    private int GetDayNumber(DayOfWeek dayOfWeek)
    {
        switch (dayOfWeek)
        {
            case DayOfWeek.Monday:
                return 1;
            case DayOfWeek.Tuesday:
                return 2;
            case DayOfWeek.Wednesday:
                return 3;
            case DayOfWeek.Thursday:
                return 4;
            case DayOfWeek.Friday:
                return 5;
            case DayOfWeek.Saturday:
                return 6;
            case DayOfWeek.Sunday:
                return 7;
            default:
                throw new ArgumentOutOfRangeException("dayOfWeek", dayOfWeek, "Unknown day of week.");
        }
    }

    public static IList<WeekDate> GetWeekDates(DateTime from, DateTime to)
    {
        List<WeekDate> weekDates = new List<WeekDate>();
        weekDates.Add(new WeekDate(from));
        while (weekDates.Last()._date < to.Date)
        {
            DateTime nextDay = weekDates.Last()._date.AddDays(1);
            weekDates.Add(new WeekDate(nextDay));
        }
        return weekDates;
    }
}

Example

A screenshot of unit testing the Week class:

Screenshot of unit testing the Week class

A screenshot of unit testing the WeekDate class:

Screenshot of unit testing the WeekDate class

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s