For a program of mine I needed to get week number. Simple, I though, I'll just use standard function. I knew from past that C# had one. I also knew that it offered me a choice of how to determine first week. I could start counting weeks starting from first day in year, first full week or first week that has four days in it.
Short trip to Wikipedia has shown that ISO 8601 date standard has part that deals with weeks. One of alternative definitions read that it is "the first week with the majority (four or more) of its days in the starting year". With that sorted out, code was trivial:
private static int GetWeekNumber(DateTime date) {
var cal = new GregorianCalendar();
return cal.GetWeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
}
Nice, clean and wrong. Week numbers for all years that begin on Thursday were incorrect. For example, January 1 2009 is Thursday. According to ISO 8601 that means that this is first week and Monday, December 29 2008 is it's first day. Result should have been 1 but .NET returned 53 instead. Probably something to do with United States and week starting on Sunday.
In any case, new algorithm was needed. Simplest thing would be to do all steps that a person would do in order to calculate it:
private static int GetWeekNumber(DateTime date) {
var currNewYear = new DateTime(date.Year, 1, 1);
var currFirstThursday = currNewYear.AddDays((14 - (int)currNewYear.DayOfWeek - 3) % 7);
var currFirstMonday = currFirstThursday.AddDays(-3);
if (date >= currFirstMonday) {
var nextNewYear = new DateTime(date.Year + 1, 1, 1);
var nextFirstThursday = nextNewYear.AddDays((14 - (int)nextNewYear.DayOfWeek - 3) % 7);
var nextFirstMonday = nextFirstThursday.AddDays(-3);
if (date >= nextFirstMonday) {
return 1 + (date.Date - nextFirstMonday).Days / 7;
} else {
return 1 + (date.Date - currFirstMonday).Days / 7;
}
} else {
var prevNewYear = new DateTime(date.Year - 1, 1, 1);
var prevFirstThursday = prevNewYear.AddDays((14 - (int)prevNewYear.DayOfWeek - 3) % 7);
var prevFirstMonday = prevFirstThursday.AddDays(-3);
if (date >= prevFirstMonday) {
return 1 + (date.Date - prevFirstMonday).Days / 7;
} else {
return 1 + (date.Date - currFirstMonday).Days / 7;
}
}
}
Can this code be written shorter? Of course:
private static int GetWeekNumber(DateTime date) {
var day = (int)date.DayOfWeek;
if (day == 0) { day = 7; }
var nearestThu = date.AddDays(4 - day);
var year = nearestThu.Year;
var janFirst = new DateTime(year, 1, 1);
return 1 + (nearestThu - janFirst).Days / 7;
}
This algorithm is described on Wikipedia so I will not get into it here. It is enough to say that it is gives same results as first method but in smaller volume.
Finally I could sleep at night knowing that my weeks are numbered. :)
P.S. If someone is eager to setup their own algorithm, here is test data that I have used. Most of them were taken from ISO 8601 week date Wikipedia entry but I also added a few:
Sat 01 Jan 2005 2004-W53-6
Sun 02 Jan 2005 2004-W53-7
Mon 03 Jan 2005 2005-W01-1
Mon 10 Jan 2005 2005-W02-1
Sat 31 Dec 2005 2005-W52-6
Mon 01 Jan 2007 2007-W01-1
Sun 30 Dec 2007 2007-W52-7
Tue 01 Jan 2008 2008-W01-2
Fri 26 Sep 2008 2008-W39-5
Sun 28 Dec 2008 2008-W52-7
Mon 29 Dec 2008 2009-W01-1
Tue 30 Dec 2008 2009-W01-2
Wed 31 Dec 2008 2009-W01-3
Thu 01 Jan 2009 2009-W01-4
Mon 05 Jan 2009 2009-W02-1
Thu 31 Dec 2009 2009-W53-4
Sat 02 Jan 2010 2009-W53-6
Sun 03 Jan 2010 2009-W53-7
P.P.S. Getting year and day in week is quite simple and thus left for exercise of reader. :)