C# overlapping time ranges

Date: 2017-02-16
public interface ITimeRange
{
    DateTime? Start { get; }
    DateTime? End { get; }
}


public static class TimeRangeExtensions
{
    public static bool HasOverlap(this ITimeRange timeRange, ITimeRange outer) => HasOverlapSimple(timeRange, outer);
    public static TimeSpan GetOverlap(this ITimeRange timeRange, ITimeRange outer) => DateRangeOverlap(timeRange.Start, timeRange.End, outer.Start, outer.End);
    private static TimeSpan DateRangeOverlap(DateTime? min1, DateTime? max1, DateTime? min2, DateTime? max2)
    {
        return DateRangeOverlap(
            min1 ?? DateTime.MinValue,
            max1 ?? DateTime.MaxValue,
            min2 ?? DateTime.MinValue,
            max2 ?? DateTime.MaxValue);
    }
    private static TimeSpan DateRangeOverlap(DateTime min1, DateTime max1, DateTime min2, DateTime max2)
    {
        if (min1 > max1 || min2 > max2)
            return new TimeSpan(0);
        if (min1 == min2 && max1 == max2)
            return new TimeSpan(0);
        if (max2 <= min1 || min2 >= max1)
            return new TimeSpan(0);

        if (min1 <= min2 && max1 >= max2)
            return (max2 - min2).Duration();
        if (min1 >= min2 && max1 <= max2)
            return (max1 - min1).Duration();
        if (min1 >= min2 && max1 >= max2 && max2 >= min1)
            return (max2 - min1).Duration();
        if (min1 <= min2 && max1 >= min2 && max2 >= max1)
            return (max1 - min2).Duration();
        return new TimeSpan(0);
    }

    private static bool HasOverlapSimple(this ITimeRange timeRange, ITimeRange outer) => HasOverlapSimple(timeRange.Start, timeRange.End, outer.Start, outer.End);
    private static bool HasOverlapSimple(DateTime? min1, DateTime? max1, DateTime? min2, DateTime? max2)
    {
        return HasOverlapSimple(
           min1 ?? DateTime.MinValue,
           max1 ?? DateTime.MaxValue,
           min2 ?? DateTime.MinValue,
           max2 ?? DateTime.MaxValue);
    }
    private static bool HasOverlapSimple(DateTime min1, DateTime max1, DateTime min2, DateTime max2)
    {
        var isExact = min1 == min2 && max1 == max2;
        if (isExact) return true;

        // if one of the periods is a single point in time
        // then match inclusive at the start or end (as overlap)
        var isInclusive = min1 == max1 || min2 == max2;
        if (isInclusive)
            return min1 <= max2 && max1 >= min2;

        return min1 < max2 && max1 > min2;
    }
}

public sealed class TimeRange : ITimeRange
{
    public TimeRange(DateTime? start, DateTime? end)
    {
        Start = start;
        End = end;
    }
    public DateTime? Start { get; set; }
    public DateTime? End { get; set; }
    public static ITimeRange Today => new TimeRange(DateTime.Today, DateTime.Today.AddDays(1).AddSeconds(-1));

    public static ITimeRange From(DateTime? start, DateTime? end) => new TimeRange(start, end);
    public static ITimeRange GetRelativeYear(int relative = 0)
    {
        var month = DateTime.Now.AddYears(relative);
        var start = new DateTime(month.Year, 1, 1);
        var end = start.AddYears(1).AddSeconds(-1);
        return new TimeRange(start, end);
    }
    public static ITimeRange GetRelativeMonth(int relative = 0)
    {
        var month = DateTime.Now.AddMonths(relative);
        var start = new DateTime(month.Year, month.Month, 1);
        var end = start.AddMonths(1).AddSeconds(-1);
        return new TimeRange(start, end);
    }
    public static ITimeRange GetRelativeWeek(int relative)
    {
        var date = DateTime.Now;
        while (date.DayOfWeek != DayOfWeek.Monday)
            date = date.AddDays(-1);
        date = date.AddDays(relative * 7);
        return new TimeRange(date, date.AddDays(6));
    }
    public static ITimeRange GetRelativeDay(int relative)
    {
        var date = DateTime.Today;
        date = date.AddDays(relative);
        return new TimeRange(date, date.AddDays(1).AddSeconds(-1));
    }

    public static string AsString(ITimeRange period) => $"{period.Start:s}-{period.End:s}";

    // [---------------]     [----------] <= children
    //            [---------------]       <= children  
    // ==>
    //     [-------------------------]    <= parent
    //     [-----][----][---][-------]
    public static IEnumerable<TimeRange> GetPeriodsFlattened(ITimeRange parent, IEnumerable<ITimeRange> children)
    {
        if (!children.Any())
        {
            yield return new TimeRange(parent.Start, parent.End);
            yield break;
        }

        var allTimesInParent = children.SelectMany(x => LinqHelper.Select(x.Start, x.End))
            .Concat(LinqHelper.Select(parent.Start, parent.End))
            .Where(x => x.HasValue && x >= parent.Start && x <= parent.End)
            .OrderBy(x => x).Distinct().ToList();

        if (allTimesInParent.Count < 2)
        {
            yield return new TimeRange(parent.Start, parent.End);
            yield break;
        }

        DateTime? prev = null;
        foreach (var curr in allTimesInParent)
        {
            if (prev != null)
                yield return new TimeRange(prev, curr);
            prev = curr;
        }
    }
}
// With number ranges:
private static bool HasOverlapSimple(int? min1, int? max1, int? min2, int? max2)
{
    return HasOverlapSimple(
       min1 ?? int.MinValue,
       max1 ?? int.MaxValue,
       min2 ?? int.MinValue,
       max2 ?? int.MaxValue);
}
private static bool HasOverlapSimple(int min1, int max1, int min2, int max2)
{
    var isExact = min1 == min2 && max1 == max2;
    if (isExact) return true;

    // if one of the ranges is a single point
    // then match inclusive at the start or end (as overlap)
    var isInclusive = min1 == max1 || min2 == max2;
    if (isInclusive)
        return min1 <= max2 && max1 >= min2;

    return min1 < max2 && max1 > min2;
}

private static bool HasOverlapSimple(double? min1, double? max1, double? min2, double? max2)
{
    return HasOverlapSimple(
       min1 ?? double.MinValue,
       max1 ?? double.MaxValue,
       min2 ?? double.MinValue,
       max2 ?? double.MaxValue);
}
private static bool HasOverlapSimple(double min1, double max1, double min2, double max2)
{
    var isExact = min1 == min2 && max1 == max2;
    if (isExact) return true;

    // if one of the ranges is a single point
    // then match inclusive at the start or end (as overlap)
    var isInclusive = min1 == max1 || min2 == max2;
    if (isInclusive)
        return min1 <= max2 && max1 >= min2;

    return min1 < max2 && max1 > min2;
}

Old code:

public static TimeSpan DateRangeOverlapTimeSpan(DateTime min1, DateTime max1, DateTime min2, DateTime max2)
{
    if (min1 > max1 || min2 > max2)
    {
        throw new Exception("Invalid date range");
    }

    // Cases: 
    //            [---- Date1 ----]
    //                [--2C--]  
    //      [------------2D------------]   
    //      [----2A----]    [----2B----]
    // [--E--]                        [--F--]

    // 2E, 2F: skip
    if (max2 <= min1 || min2 >= max1)
    {
        return new TimeSpan(0);
    }

    // 2C
    if (min1 <= min2 && max1 >= max2)
    {
        return (max2 - min2).Duration();
    }
    // 2D
    if (min1 >= min2 && max1 <= max2) { return (max1 - min1).Duration(); } // 2A if (min1 >= min2 && max1 >= max2 && max2 >= min1)
    {
        return (max2 - min1).Duration();
    }
    // 2B
    if (min1 <= min2 && max1 >= min2 && max2 >= max1)
    {
        return (max1 - min2).Duration();
    }

    return new TimeSpan(0);
}

public static TimeSpan TimespanRangeOverlapTimeSpan(TimeSpan min1, TimeSpan max1, TimeSpan min2, TimeSpan max2)
{
    if (min1 > max1 || min2 > max2)
    {
        throw new Exception("Invalid date range");
    }

    // Cases: 
    //            [---- Date1 ----]
    //                [--2C--]  
    //      [------------2D------------]   
    //      [----2A----]    [----2B----]
    // [--E--]                        [--F--]

    // 2E, 2F: skip
    if (max2 <= min1 || min2 >= max1) {
        return new TimeSpan(0);
    }

    // 2C
    if (min1 <= min2 && max1 >= max2)
    {
        return (max2 - min2).Duration();
    }
    // 2D
    if (min1 >= min2 && max1 <= max2) { return (max1 - min1).Duration(); } // 2A if (min1 >= min2 && max1 >= max2 && max2 >= min1)
    {
        return (max2 - min1).Duration();
    }
    // 2B
    if (min1 <= min2 && max1 >= min2 && max2 >= max1)
    {
        return (max1 - min2).Duration();
    }            
    
    return new TimeSpan(0);
}

public void TestTimespanRangeOverlapTimeSpan()
{
    // Cases: 
    //            [---- Date1 ----]
    //                [--2C--]  
    //      [------------2D------------]   
    //      [----2A----]    [----2B----]
    // [--E--]                        [--F--]

    TimeSpan min1 = new TimeSpan(8, 0, 0);
    TimeSpan max1 = new TimeSpan(17, 0, 0);

    TimeSpan min2;
    TimeSpan max2;

    // Case C:
    min2 = new TimeSpan(10, 0, 0);
    max2 = new TimeSpan(11, 0, 0);
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(1));
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(1));

    // Case D:
    min2 = new TimeSpan(7, 0, 0);
    max2 = new TimeSpan(18, 0, 0);
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(9));
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(9));

    // Case A:
    min2 = new TimeSpan(7, 0, 0);
    max2 = new TimeSpan(10, 0, 0);
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(2));
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(2));

    // Case B:
    min2 = new TimeSpan(16, 0, 0);
    max2 = new TimeSpan(18, 0, 0);
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(1));
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(1));

    // Case E:
    min2 = new TimeSpan(4, 0, 0);
    max2 = new TimeSpan(8, 0, 0);
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(0));
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(0));

    // Case F:
    min2 = new TimeSpan(17, 0, 0);
    max2 = new TimeSpan(18, 0, 0);
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(0));
    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(0));
}
6580cookie-checkC# overlapping time ranges