Adding Dates to C#

Published on: Author: Michael

UPDATE: A bug was recently found when determining the difference between 2 dates across a year boundary. A corrected version has been uploaded to the site.


In .NET the DateTime type represents both a date and a time. .NET doesn’t really have a pure date type. Ironically it does have a pure time type in the form of TimeSpan. In general this isn’t an issue but when you really just need a date (i.e. the end of a grading period) then you have to be aware of the time. For example when comparing 2 date/time values the time is included in the comparison even if it does not matter. To eliminate time from the comparison you would need to set both to the same value (i.e. 00:00:00). Another example is conversion to a string. A date/time value will include the time so you have to use a format string to eliminate it. Yet another issue is that creating specific dates using DateTime isn’t exactly readable (i.e. July 4th, 2000 is new DateTime(2000, 4, 4)).


In this post we’ll create a pure date type based upon code I have been using for a while. When originally creating this type I had the following goals.


  • Unobtrusive – It should work interchangeably with DateTime. It should also be immutable like DateTime.
  • Performant – There should be no noticeable performance difference between using it and a regular DateTime.
  • Flexible – It should behave like any other type which means it should be easy to add extension methods to handle business-specific date requirements.
  • Readable – In the rare cases where an absolute date is required (i.e. unit testing) then it should be readable.

Caveat 1: This new type will conflict with the existing Visual Basic keyword. The VB keyword is backed by a DateTime value.


Caveat 2: This type is designed to work with American-style dates. For other cultures some changes may be necessary to support the culture-specific requirements.


Introducing the Date Type


We will start with the base type definition. Because performance is important and it is immutable we will make it a value type. Here is the basic definition.


[Serializable]
public struct Date
{
    public Date ( DateTime value )
    {
        m_dt = value.Date;
    }

    public Date ( int year, int month, int day )
    {
        m_dt = new DateTime(year, month, day);
    }

    public int Day
    {
        get { return m_dt.Day; }
    }

    public int Month
    {
        get { return m_dt.Month; }
    }

    public int Year 
    {
        get { return m_dt.Year; }
    }

    private readonly DateTime m_dt;
}

The type is little more than a wrapper around a DateTime with a couple of properties to expose the date information. An alternative approach to creating the type would be to use a simple integral value to represent the date. This would reduce the size of the type but at significant expense. Implementing add/subtract functionality would be harder as we would need to take into account month lengths and leap year. We would also need to implement more complicated parsing and formatting. Furthermore we lose the interoperability with DateTime during serialization/deserialization. In my opinion the additional size overhead is worth the cost. Yet another approach would be to store the number of days since a fixed date. This would simplify the math but the other problems remain.


Date provides a couple of constructors to build a date either from its parts or an existing DateTime. The type is also serializable so it can be used more easily. One of the core assumptions used in the code is that the time is always zero. Therefore whenever we receive a DateTime as a parameter we will always ensure we remove the time component.


Here’s how it might be used.


[TestMethod]
public void Ctor_FromDateTime ()
{
    var expected = new DateTime(2013, 12, 10);

    var actual = new Date(expected);

    actual.Day.Should().Be(expected.Day);
    actual.Month.Should().Be(expected.Month);
    actual.Year.Should().Be(expected.Year);
}

[TestMethod]
public void Ctor_FromParts ()
{
    int expectedDay = 10, expectedMonth = 12, expectedYear = 2013;

    var actual = new Date(expectedYear, expectedMonth, expectedDay);

    actual.Day.Should().Be(expectedDay);
    actual.Month.Should().Be(expectedMonth);
    actual.Year.Should().Be(expectedYear);
}

Moving Functionality to Date


Now that the base type is in place we can move date specific functionality from DateTime to the new type. This will make DateTime more interchangeable with DateTime.


public static readonly Date MaxValue = new Date(DateTime.MaxValue);
public static readonly Date MinValue = new Date(DateTime.MinValue);
public static readonly Date None = new Date();

public DayOfWeek DayOfWeek
{
    get { return m_dt.DayOfWeek; }
}
public int DayOfYear
{
    get { return m_dt.DayOfYear; }
}

public bool IsLeapYear
{
    get { return DateTime.IsLeapYear(Year); }
}

public Date AddDays ( int value )
{
    return new Date(m_dt.AddDays(value));
}

public Date AddMonths ( int value )
{
    return new Date(m_dt.AddMonths(value));
}

public Date AddYears ( int value )
{
    return new Date(m_dt.AddYears(value));
}

Interoperability with DateTime


While we have a constructor that can accept a DateTime we should make it easier to convert to and from DateTime. However there is one problem, converting from DateTime is losing data (the time). Therefore we will make the conversion from Date to DateTime implicit but the conversion from DateTime to Date has to be explicit.


public static implicit operator DateTime ( Date value )
{
    return value.m_dt;
}

public static explicit operator Date ( DateTime value )
{
    return new Date(value);
}

public DateTime At ( TimeSpan time )
{
    return m_dt.Add(time);
}

public DateTime At ( int hours, int minutes, int seconds )
{
    return m_dt.Add(new TimeSpan(hours, minutes, seconds));
}

Here’s how you might use this functionality.


var someDt = new DateTime(2013, 10, 15);
var someDate = (Date)someDt;

DateTime anotherDt = someDate;
var withTime = someDate.At(12, 22, 45);

Because DateTime to Date is explicit we will add an extension method to DateTime to convert it to a date.


public static Date ToDate ( this DateTime source )
{
    return new Date(source);
}

//Usage
var target = new DateTime(2013, 5, 10, 12, 20, 30);

var actual = target.ToDate();

Implementing Equality


With the base type and interoperability with DateTime out of the way we can flesh out some standard value type functionality like IEquatable. Equality for a date is simply confirming the date part matches. Unlike DateTime the time portion is completely ignored. For interoperability we will implement equality for both Date and DateTime. When comparing a Date to a DateTime the time portion will be ignored. Following standard rules for implementing equality we ultimately just need to implement the core Equals method. The code sample contains the full implementation but here is the core method.


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

public bool Equals ( Date other )
{
    return m_dt.Equals(other.m_dt);
}

public bool Equals ( DateTime other )
{
    return m_dt == other.Date;
}

Implementing Comparison


Comparing dates is incredibly useful, and simple, so we can implement IComparable as well. As with equality we will implement it for both Date and DateTime. The core methods are shown here.


public int CompareTo ( Date other )
{
    return m_dt.CompareTo(other.m_dt);
}

public int CompareTo ( DateTime other )
{
    return m_dt.CompareTo(other.Date);
}

Formatting and Parsing


One of the issues with trying to use DateTime for dates is that it will, by default, print out the time as well. So for Date we will override this behavior and have it only print out the date portion. Additionally we will implement IFormattable so that a developer can customize the behavior. We could go out of our way and define a custom formatter along with format specifiers but dates are complex and we don’t really want to reinvent the wheel. Therefore we will simply defer to the underlying DateTime. This means that a developer could specify a time format if they wanted. But this is a reasonable tradeoff given the complexity of formatting.


public string ToLongDateString ()
{
    return m_dt.ToLongDateString();
}

public string ToShortDateString ()
{
    return m_dt.ToShortDateString();
}

public override string ToString ()
{
    return ToLongDateString();
}

public string ToString ( string format )
{
    return m_dt.ToString(format);
}

public string ToString ( string format, IFormatProvider formatProvider )
{
    return m_dt.ToString(format, formatProvider);
}

Parsing is a little harder simply because parsing a date requires understanding the various formats. As with formatting we will simply defer to DateTime and truncate any time that comes back. The core implementation is shown below.


public static Date Parse ( string value )
{
    return new Date(DateTime.Parse(value));
}

public static Date ParseExact ( string value, string format, IFormatProvider formatProvider )
{
    return new Date(DateTime.ParseExact(value, format, formatProvider));
}

Here is how it would be used.


var target = "10/20/2012";

var actual = Date.Parse(target);

Creating Dates Fluently


One issue that I have with DateTime is that it is not clear, when creating values, what the actual date is. I much prefer a fluent interface. For Date we can create a fluent interface that allows us to create dates in a more readable format. This is mostly useful for unit tests where fixed dates are useful but it can be used anywhere.  Here’s how it might be used.


var left = new Date(2010, 5, 1);
var right = new DateTime(2010, 5, 31);

var target = Dates.May(15, 2010);

var actual = target.IsBetween(left, right);

actual.Should().BeTrue();

This is far more readable than the constructor. While we could add all the various functionality to the Date type I feel that the core type is already large enough. Furthermore the usefulness of defining exact dates in non-testing code is limited so it is a clean separation to have the fluent functionality in its own type. Dates exposes a method for each month of the year. For each month you can specify a day and year. This, of course, follows the American approach to dates but you could extend it to use other styles as well. For example if you prefer the European approach where day precedes month you could do this.


public static class EuropeanDates
{
    public static Date January ( this int source, int year )
    {
        return new Date(year, 1, source);
    }
}

//Usage
return 10.January(2013);

There are other methods in the type as well that allow for the creation of partial dates. This is useful in a few cases where the year or day is not yet available. To support this some support types (-Part) have been added to represent partial dates. They are not designed for general use but as part of creating a full date.


var target = Dates.January()
                    .OfYear(2013)
                    .Day(10);

target.Day.Should().Be(10);
target.Month.Should().Be(1);
target.Year.Should().Be(2013);

One interesting benefit of this is that extension methods can be created to build specific dates from partial information. For example you can define an extension method that returns the first weekend of a month and year.


public static Date GetFirstWeekend ( this MonthYearPart source )
{
    switch (source.FirstDayOfMonth().DayOfWeek)
    {                
        case DayOfWeek.Monday: return source.Day(6);
        case DayOfWeek.Tuesday: return source.Day(5);
        case DayOfWeek.Wednesday: return source.Day(4);
        case DayOfWeek.Thursday: return source.Day(3);
        case DayOfWeek.Friday: return source.Day(2);
    };

    return source.Day(1);
}

There is also a Months type that represents the standard months in an (American) year. It is interesting to note that a standard static class with constant integral members is used in lieu of an enumeration to eliminate the need for a cast when using with the standard Date methods.


Extending the Type


One of the key goals was to make the type extensible. This allows for business and application extensions to be added easily. For example we could add an extension that determines if a Date is on a weekend like this.


public static bool IsWeekend ( this Date source )
{
    return source.DayOfWeek == DayOfWeek.Saturday || source.DayOfWeek == DayOfWeek.Sunday;
}

For the core type a few extensions have already been added (to the core type). Here’s a summary.


  • FromDayOfYear – Static method to create dates from the day of the year
  • IsLeapDay – Determines if the date is a leap day
  • IsSet – Determines if the date is set
  • AddWeeks – Adds a number of weeks to the date
  • Difference – Determines the difference between 2 Dates or DateTimes
  • DifferenceAbsolute – Determines the absolute difference between 2 Dates or DateTimes
  • FirstDayOfMonth/LastDayOfMonth
  • Yesterday/Tomorrow
  • LastMonth/NextMonth
  • IsBetween – Various methods to determine ordering of dates

Finally, date ranges are very common in programs (start/end of pay periods, reports, etc). Included is a simple DateRange type that can be used to represent a date range using the Date type.


Dates.zip

6 Responses to Adding Dates to C# Comments (RSS) Comments (RSS)

  1. You should also make sure that the underlying dates are always the same Kind (either Local or UTC). Your code should also do the conversion to either one of them since the date given to you might be with the time component that converted to the other kind will result in a different date.

  2. Error on the 4th July initialisation… and IMHO “new DateTime(2014, 7, 4);” is actually very readable and avoids all issues about whether 4/7/2014 or 7/4/2014 is correct. Although since it is 4th of July, I’d claim Europe is right on that layout! ;-)