DDD: making the Time Period concept explicit

One of the applications I work on is a planning system, used for managing the operations of the business over the next week, month and financial year.

Almost every entity in this application has a fixed ‘applicable period’ — a lifetime that begins and ends at certain dates. For example:

  • An employee’s applicable period lasts as long as they are employed
  • A business unit’s lifetime lasts from the day it’s formed, to the day it disbands
  • A policy lasts from the day it comes into effect to the day it ends
  • A shift starts at 8am and finishes at 6pm

Previous incarnations of the application simply added StartDate and EndDate properties to every object, and evaluated them ad-hoc as required. This resulted in a lot of code duplication — date and time logic around overlaps, contiguous blocks etc were repeated all over the place.

As we’ve been carving off bounded contexts and reimplementing them using DDD, I’m proud to say this concept has been identified and separated out into an explicit value type with encapsulated behaviour. We call it a Time Period:

It’s sort of like a .NET TimeSpan but represents a specific period of time, e.g. seven days starting from yesterday morning — not seven days in general.

Here’s the behaviour we’ve implemented so far, taking care of things like comparisons and overlapping periods:

/// <summary>
/// A value type to represent a period of time with known end points (as
/// opposed to just a period like a timespan that could happen anytime).
/// The end point of a TimeRange can be infinity. 
/// </summary>
public class TimePeriod : IEquatable<TimePeriod>
{
    public DateTime Start { get; }
    public DateTime? End { get; }
    public bool IsInfinite { get; }
    public TimeSpan Duration { get; }
    public bool Includes(DateTime date);
    public bool StartsBefore(TimePeriod other);
    public bool StartsAfter(TimePeriod other);
    public bool EndsBefore(TimePeriod other);
    public bool EndsAfter(TimePeriod other);
    public bool ImmediatelyPrecedes(TimePeriod other);
    public bool ImmediatelyFollows(TimePeriod other);
    public bool Overlaps(TimePeriod other);
    public TimePeriod GetRemainingSlice();
    public TimePeriod GetRemainingSliceAsAt(DateTime when);
    public bool HasPassed();
    public bool HasPassedAsAt(DateTime when);
    public float GetPercentageElapsed();
    public float GetPercentageElapsedAsAt(DateTime when);
}

Encapsulating logic all in one place means we can get rid of all that ugly duplication (DRY), and it still maps cleanly to StartDate/EndDate columns in the database as an NHibernate component or IValueType.

You can grab our initial implementation here:

7 thoughts on “DDD: making the Time Period concept explicit

  1. Very nice stuff. This class looks remarkably similar to CalendarInterval, a class I wrote for my Java applications.

  2. It makes me nervous that you represent semi-infinite intervals going forwards but not going backwards (“before next Tuesday” as opposed to “from now on”). I suppose you could use the beginning of the epoch, but it seems ugly.

    Then again, I can’t see much use for (\infty, n] intervals in a business context.

  3. Very nice.
    Something alike should be included in .net.

    I’ve added 4 more methods, what do you think of them?

    ///
    /// Splits TimePeriod
    ///
    /// Any Time (within or out of TimePeriod)
    /// 1 or 2 new Elements
    public IEnumerable Split(DateTime splitTime)
    {
    if (splitTime > Start && (!End.HasValue || End.Value > splitTime))
    return new TimePeriod[] { new TimePeriod(Start, splitTime), new TimePeriod(splitTime, End) };
    else
    return new TimePeriod[] { (TimePeriod)this.MemberwiseClone() };
    }

    ///
    /// Removes/Substracts onther Timeperiod
    ///
    /// To be substracted
    /// returns 0, 1 or 2 new Elements
    public IEnumerable Remove(TimePeriod other)
    {
    List result = new List();

    // everything left over
    if (!Overlaps(other))
    result.Add(new TimePeriod(this.Start, this.End));
    // left side or right side or both sides or nothing left over
    else
    {
    // left ok
    if (this.Start other.End))
    result.Add(new TimePeriod(other.End.Value, this.End));
    }
    return result;
    }

    ///
    /// Compares 2 timelines and returns overlaps
    ///
    /// Timeline1
    /// Timeline2
    /// new timeline of overlaps
    public static IEnumerable GetOverlaps(IEnumerable timePeriods1, IEnumerable timePeriods2)
    {
    List result = new List();

    foreach (TimePeriod t1 in timePeriods1)
    {
    foreach (TimePeriod t2 in timePeriods2)
    {
    // t1 eats t2
    if (t1.Start = t2.End))
    result.Add(new TimePeriod(t2.Start, t2.End));
    // t2 eats t1
    else if (t1.Start >= t2.Start && (t2.IsInfinite || t1.End <= t2.End))
    result.Add(new TimePeriod(t1.Start, t1.End));
    // t1 touches t2 on t1's right side
    else if (t1.Start t2.Start)
    result.Add(new TimePeriod(t2.Start, t1.End));
    // t1 touches t2 on t1’s left side
    else if (t1.Start > t2.Start && t1.Start < t2.End)
    result.Add(new TimePeriod(t1.Start, t2.End));
    }
    }
    return result;
    }

    ///
    /// Removes TimePeriods from a timeline
    ///
    /// Timeline
    /// TimePeriods that (might) overlap and shall be removed from timeline
    /// new timeline without overlaps
    public static IEnumerable RemoveOverlaps(IEnumerable timePeriods, IEnumerable overlaps)
    {
    List result = timePeriods.ToList();

    for (int i = 0; i < result.Count; i++)
    {
    foreach (TimePeriod overlap in overlaps)
    {
    if (result[i].Overlaps(overlap))
    {
    // Add rest of clash (if exists) to end to be processed as well
    result.AddRange(result[i].Remove(overlap));

    // Remove overlapping TimePeriod
    result.RemoveAt(i);
    }
    }
    }
    return result;
    }

Comments are closed.