Skip to content

Instantly share code, notes, and snippets.

@niik
Created January 27, 2012 11:47
Show Gist options
  • Save niik/1688424 to your computer and use it in GitHub Desktop.
Save niik/1688424 to your computer and use it in GitHub Desktop.
ISO 8601 Week abstraction
/*
* Copyright (c) 2011 Markus Olsson
*
* var mail = string.Join(".", new string[] {"j", "markus", "olsson"}) + string.Concat('@', "gmail.com");
* var blog = http://blog.freakcode.com
*
* MIT License follows.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* From http://joda-time.sourceforge.net/cal_iso.html:
*
* The first week of a year is the one that includes the first Thursday of the year.
* This definition can mean that the first week of a year starts in the previous year,
* and the last week finishes in the next year.
*
*/
using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
namespace freakcode
{
/// <summary>
/// An immutable representation of a ISO 8601 week number. Supports equality and relative comparison through
/// interfaces as well as overloaded operators.
/// Uses the algorithm proposed by DR J R Stockton on http://en.wikipedia.org/wiki/Talk:ISO_week_date#Algorithms
/// </summary>
public sealed class IsoWeek : IEquatable<IsoWeek>, IComparable<IsoWeek>, IComparable, IEnumerable<DateTime>
{
/// <summary>
/// A datetime instance (with hour, minute, and seconds zeroed) representing the
/// first day (monday) of the week. Used for comparisons and as a basis for calculating
/// offsets.
/// </summary>
private readonly DateTime startDate;
/// <summary>
/// Gets the week number represented by this instance.
/// Valid values range from 1 to 53.
/// </summary>
/// <value>The week number.</value>
public int WeekNumber { get; private set; }
/// <summary>
/// Gets the year represented by this instance. 4 digits.
/// </summary>
/// <value>The year.</value>
public int Year { get; private set; }
/// <summary>
/// Gets a <see cref="System.DateTime"/> instance representing the date of the monday in the week
/// represented by this <see cref="IsoWeek"/> instance.
/// </summary>
public DateTime Monday { get { return this.startDate; } }
/// <summary>
/// Gets a <see cref="System.DateTime"/> instance representing the date of the tuesday in the week
/// represented by this <see cref="IsoWeek"/> instance.
/// </summary>
public DateTime Tuesday { get { return this.startDate.AddDays(1); } }
/// <summary>
/// Gets a <see cref="System.DateTime"/> instance representing the date of the wednesday in the week
/// represented by this <see cref="IsoWeek"/> instance.
/// </summary>
public DateTime Wednesday { get { return this.startDate.AddDays(2); } }
/// <summary>
/// Gets a <see cref="System.DateTime"/> instance representing the date of the thursday in the week
/// represented by this <see cref="IsoWeek"/> instance.
/// </summary>
public DateTime Thursday { get { return this.startDate.AddDays(3); } }
/// <summary>
/// Gets a <see cref="System.DateTime"/> instance representing the date of the friday in the week
/// represented by this <see cref="IsoWeek"/> instance.
/// </summary>
public DateTime Friday { get { return this.startDate.AddDays(4); } }
/// <summary>
/// Gets a <see cref="System.DateTime"/> instance representing the date of the saturday in the week
/// represented by this <see cref="IsoWeek"/> instance.
/// </summary>
public DateTime Saturday { get { return this.startDate.AddDays(5); } }
/// <summary>
/// Gets a <see cref="System.DateTime"/> instance representing the date of the sunday in the week
/// represented by this <see cref="IsoWeek"/> instance.
/// </summary>
public DateTime Sunday { get { return this.startDate.AddDays(6); } }
/// <summary>
/// Gets the week succeeding the one represented by the current instance.
/// </summary>
/// <value>The next week.</value>
public IsoWeek Next { get { return AddWeeks(1); } }
/// <summary>
/// Gets the week preceeding the one represented by the current instance.
/// </summary>
/// <value>The previous week.</value>
public IsoWeek Previous { get { return AddWeeks(-1); } }
/// <summary>
/// Initializes a new instance of the <see cref="IsoWeek"/> class.
/// </summary>
/// <param name="year">The year specified with 4 digits.</param>
/// <param name="weekNumber">The week number.</param>
public IsoWeek(int year, int weekNumber)
{
if (weekNumber < 1 || weekNumber > 53)
throw new ArgumentOutOfRangeException("weekNumber", "Invalid week. Must be at least one and at most 53");
var d = new DateTime(year, 1, 1);
// Locate nearest thursday.
while (d.DayOfWeek != DayOfWeek.Thursday)
d = d.AddDays(1);
// Seek to the thursday of the desired week.
d = d.AddDays(7 * (weekNumber - 1));
// Ensure that the thursday of the given week is within the same year
if (d.Year != year)
throw new ArgumentOutOfRangeException("weekNumber", "Invalid week for year " + year + ": " + weekNumber);
// The week always starts on mondays.
this.startDate = d.AddDays(-3);
this.WeekNumber = weekNumber;
this.Year = year;
}
/// <summary>
/// Returns the ISO 8601 week to which the specified date belongs.
/// </summary>
public static IsoWeek FromDate(DateTime date)
{
// Get the week day where 1 = Monday and 7 = Sunday
var dayOfWeek = (((int)date.DayOfWeek + 6) % 7) + 1;
// Locate the nearest thursday.
date = date.Date.AddDays(4 - dayOfWeek);
var jan1 = new DateTime(date.Year, 1, 1);
// Calculate the number of days in between the nearest thursday and january first of
// the the same year.
var deltaDays = (date - jan1).Days;
return new IsoWeek(date.Year, 1 + (int)Math.Floor(deltaDays / 7.0));
}
/// <summary>
/// Gets the current ISO 8601 week.
/// </summary>
public static IsoWeek Now
{
get
{
return FromDate(DateTime.Now);
}
}
private static Regex parseRe = new Regex(@"^(\d{4})-?(?:W|w)(\d{2})$");
/// <summary>
/// Converts the string representation of a week to its IsoWeek equivalent.
/// </summary>
/// <param name="value">The value.</param>
/// <returns></returns>
public static IsoWeek Parse(string value)
{
IsoWeek week;
if (IsoWeek.TryParse(value, out week))
return week;
throw new FormatException("Could not parse week from given value");
}
/// <summary>
/// Attempts to converts the string representation of a week to its IsoWeek equivalent. Supported formats is 2001W01 and 2001-W01.
/// </summary>
public static bool TryParse(string value, out IsoWeek week)
{
week = null;
if (string.IsNullOrEmpty(value))
return false;
// Simple int=>week conversion. Ie, no year
if (value.Length <= 2)
{
int simpleWeekNumber;
if (!int.TryParse(value, out simpleWeekNumber))
return false;
int currentYear = DateTime.Now.Year;
if (simpleWeekNumber < 1 || simpleWeekNumber > 53)
return false;
if (simpleWeekNumber == 53 && !IsLongYear(currentYear))
return false;
week = new IsoWeek(currentYear, simpleWeekNumber);
return true;
}
var m = parseRe.Match(value);
if (!m.Success)
return false;
int year = int.Parse(m.Groups[1].Value);
int weekNumber = int.Parse(m.Groups[2].Value);
if (year < DateTime.MinValue.Year || year >= DateTime.MaxValue.Year)
return false;
if (weekNumber < 1 || weekNumber > 53)
return false;
if (weekNumber == 53 && !IsLongYear(year))
return false;
week = new IsoWeek(year, weekNumber);
return true;
}
private static bool IsLongYear(int year)
{
// From http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm
// .. one can infer that the last week of the ISO calendar year always includes 28 December.
var d = new DateTime(year, 12, 28);
var w = IsoWeek.FromDate(d);
if (w.WeekNumber == 53)
return true;
return false;
}
/// <summary>
/// Calculates the week that's offset from this instance by the specified number of weeks.
/// Note that the returned week may be in another year than this instance.
/// </summary>
/// <param name="weeks">The number of weeks to offset.</param>
public IsoWeek AddWeeks(int weeks)
{
return IsoWeek.FromDate(this.startDate.AddDays(7 * weeks));
}
/// <summary>
/// Gets a <see cref="System.DateTime"/> instance representing the date of
/// the given day within this week.
/// </summary>
public DateTime GetDayOfWeek(DayOfWeek dayOfWeek)
{
switch (dayOfWeek)
{
case DayOfWeek.Monday: return this.Monday;
case DayOfWeek.Tuesday: return this.Tuesday;
case DayOfWeek.Wednesday: return this.Wednesday;
case DayOfWeek.Thursday: return this.Thursday;
case DayOfWeek.Friday: return this.Friday;
case DayOfWeek.Saturday: return this.Saturday;
case DayOfWeek.Sunday: return this.Sunday;
}
throw new ArgumentException("dayOfWeek", "Unknown day of week: " + dayOfWeek.ToString());
}
/// <summary>
/// Returns a <see cref="System.String"/> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String"/> that represents this instance.
/// </returns>
public override string ToString()
{
return ToIsoWeekString();
}
/// <summary>
/// Returns a <see cref="System.String"/> containing the ISO week string
/// that represents this instance. Example: "2009-W09" and "2009-W10"
/// </summary>
public string ToIsoWeekString()
{
return ToIsoWeekString(false);
}
/// <summary>
/// Returns a <see cref="System.String"/> containing the ISO week string
/// that represents this instance. Example: "2009-W09" and "2009-W10".
/// If compact is set the dash between the year and week will be omitted.
/// </summary>
/// <param name="compact">if set to <c>true</c> the returned string will be in ISO 8601 compact week format (no dashes).</param>
/// <returns></returns>
public string ToIsoWeekString(bool compact)
{
string week = "W" + this.WeekNumber.ToString().PadLeft(2, '0');
if (compact)
return this.Year + week;
return this.Year + "-" + week;
}
public override int GetHashCode()
{
return this.startDate.GetHashCode();
}
public override bool Equals(object obj)
{
if (ReferenceEquals(obj, null))
return false;
IsoWeek week = obj as IsoWeek;
if (ReferenceEquals(week, null))
throw new ArgumentException("obj was not of type IsoWeek");
return this.Equals(week);
}
public bool Equals(IsoWeek other)
{
if (ReferenceEquals(this, other))
return true;
if (ReferenceEquals(other, null))
return false;
return this.startDate.Equals(other.startDate);
}
public int CompareTo(object obj)
{
if (ReferenceEquals(obj, null))
return 1;
IsoWeek week = obj as IsoWeek;
if (ReferenceEquals(week, null))
throw new ArgumentException("obj was not of type IsoWeek");
return CompareTo(week);
}
public int CompareTo(IsoWeek other)
{
if (ReferenceEquals(other, null))
return 1;
return this.startDate.CompareTo(other.startDate);
}
public static bool operator ==(IsoWeek x, IsoWeek y)
{
if (ReferenceEquals(x, y))
return true;
if (ReferenceEquals(x, null))
return ReferenceEquals(y, null);
return x.Equals(y);
}
public static bool operator !=(IsoWeek x, IsoWeek y)
{
return !(x == y);
}
public static bool operator >(IsoWeek x, IsoWeek y)
{
if (ReferenceEquals(x, null))
return false;
return x.CompareTo(y) > 0;
}
public static bool operator <(IsoWeek x, IsoWeek y)
{
if (ReferenceEquals(x, null))
return !ReferenceEquals(y, null);
return x.CompareTo(y) < 0;
}
public static bool operator <=(IsoWeek x, IsoWeek y)
{
if (ReferenceEquals(x, null))
return true;
return x.CompareTo(y) <= 0;
}
public static bool operator >=(IsoWeek x, IsoWeek y)
{
if (ReferenceEquals(x, null))
return ReferenceEquals(y, null);
return x.CompareTo(y) >= 0;
}
public IEnumerator<DateTime> GetEnumerator()
{
yield return Monday;
yield return Tuesday;
yield return Wednesday;
yield return Thursday;
yield return Friday;
yield return Saturday;
yield return Sunday;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return ((IEnumerable<DateTime>)(this)).GetEnumerator();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment