Should I make a DateRange object?

A few of my domain objects contain date ranges as a pair of start and end date properties:

public class Period {
  public DateTime EffectiveDate { get; set; }
  public DateTime ThroughDate { get; set; }
}

public class Timeline {
  public DateTime StartDate { get; set; }
  public DateTime EndDate { get; set; }
}

And I find myself with a lot of this:

abstract public int Foo(DateTime startDate, DateTime endDate);
abstract public decimal Bar(DateTime startDate, DateTime endDate);
abstract public ICollection<C5.Rec<DateTime, DateTime>> FooBar(DateTime startDate, DateTime endDate);

The last one made me wonder ... Should I implement a DateRange class? I'm not aware of one in the BCL.

In my experience, making the object hierarchy deeper often complicates things. These objects do get sent to RDLC reports displayed by the ReportViewer control, but that's secondary. I'll bend the view to the model rather than vice versa. We aren't tied to the property names, though, and would be willing to compromise with something like:

public class DateRange {
  public DateTime StartDate { get; set; }
  public DateTime EndDate { get; set; }
}

Period p = new Period();
DateTime t = p.EffectiveDateRange.StartDate;

A benefit of a DateRange class would be centralized validation of the end date coming after the start date, and it will simplify my method signatures:

abstract public int Foo(DateRange dateRange);
abstract public decimal Bar(DateRange dateRange);
abstract public ICollection<DateRange> FooBar(DateRange dateRange);

I'm just not sure that a DateRange class won't get me into more trouble than its worth. Opinions?

Side question: Did I miss a generic general-purpose tuple class in the BCL somewhere? I know there's some very specific ones floating around in various namespaces. Polluting my public domain method signatures with C5 types feels very, very dirty.


Solution 1:

No, you didn't miss a general purpose class.

I have a Range type in MiscUtil which you may be interested in - and it certainly makes for simple DateTime manipulation. Referring to Marc's answer, I can't remember whether this is a struct or a class - you'd be welcome to change it of course.

It's nice and easy to step through, due to Marc's generics shenanigans (assuming you're using .NET 3.5, at least - it's feasible with 2.0 but not supported at the moment);

Range<DateTime> range = 19.June(1976).To(DateTime.Today);

foreach (DateTime date in range.Step(1.Days())
{
    // I was alive in this day
}

(That's also using a bunch of extension methods - more useful for test than production.)

To address the other point in Marc's answer, Noda Time will certainly be able to express the concept of a date more appropriately than the .NET API, but we don't have anything like a range at the moment... It's a nice idea though - I've added a feature request.

Solution 2:

In .NET 4.0 or higher the Tuple<> type was added to handle multiple values.

With the tuple type you can define your own combination of values on the fly. Your problem is very common and is similar to when a function wants to return multiple values. Previously you had to use out variables or create a new class just for the function's response.

Tuple<DateTime, DateTime> dateRange =
    new Tuple<DateTime, DateTime>(DateTime.Today, DateTime.Now);

Whichever route you take, I think you are definitely taking the correct approach. You are giving real meaning to what two dates paired together are. That's self-documenting code and in the greatest way, right in the structure of the code.

Solution 3:

If you do a lot of work with dates, yes - a range can be handy. This is actually one of those oh-so-rare cases where you should probably write it as a struct (immutable). Note, however, that "Noda Time" will probably give you all of this and more (when it is complete). I've done scheduling software before; I had a couple of such structs (for slightly different jobs).

Note, there isn't a handy BCL construct for this.

Also - think of all the wonderful methods (and possibly operators) that you can centralise when you have a range; "contains" (of a datetime? of another range? including/excluding limits?), "intersects", offset-by (a timespan), etc. A definite case for having a type to handle it. Note that at the ORM level, this is easier if your ORM supports composite values - I believe NHibernate does, and possibly EF 4.0.