{"id":658,"date":"2017-02-16T15:57:26","date_gmt":"2017-02-16T14:57:26","guid":{"rendered":"https:\/\/solidt.eu\/site\/?p=658"},"modified":"2025-09-08T11:06:14","modified_gmt":"2025-09-08T10:06:14","slug":"c-overlapping-time-ranges","status":"publish","type":"post","link":"https:\/\/solidt.eu\/site\/c-overlapping-time-ranges\/","title":{"rendered":"C# overlapping time ranges"},"content":{"rendered":"\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"csharp\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">public interface ITimeRange\n{\n    DateTime? Start { get; }\n    DateTime? End { get; }\n}\n\n\npublic static class TimeRangeExtensions\n{\n    public static bool HasOverlap(this ITimeRange timeRange, ITimeRange outer) => HasOverlapSimple(timeRange, outer);\n    public static TimeSpan GetOverlap(this ITimeRange timeRange, ITimeRange outer) => DateRangeOverlap(timeRange.Start, timeRange.End, outer.Start, outer.End);\n    private static TimeSpan DateRangeOverlap(DateTime? min1, DateTime? max1, DateTime? min2, DateTime? max2)\n    {\n        return DateRangeOverlap(\n            min1 ?? DateTime.MinValue,\n            max1 ?? DateTime.MaxValue,\n            min2 ?? DateTime.MinValue,\n            max2 ?? DateTime.MaxValue);\n    }\n    private static TimeSpan DateRangeOverlap(DateTime min1, DateTime max1, DateTime min2, DateTime max2)\n    {\n        if (min1 > max1 || min2 > max2)\n            return new TimeSpan(0);\n        if (min1 == min2 &amp;&amp; max1 == max2)\n            return new TimeSpan(0);\n        if (max2 &lt;= min1 || min2 >= max1)\n            return new TimeSpan(0);\n\n        if (min1 &lt;= min2 &amp;&amp; max1 >= max2)\n            return (max2 - min2).Duration();\n        if (min1 >= min2 &amp;&amp; max1 &lt;= max2)\n            return (max1 - min1).Duration();\n        if (min1 >= min2 &amp;&amp; max1 >= max2 &amp;&amp; max2 >= min1)\n            return (max2 - min1).Duration();\n        if (min1 &lt;= min2 &amp;&amp; max1 >= min2 &amp;&amp; max2 >= max1)\n            return (max1 - min2).Duration();\n        return new TimeSpan(0);\n    }\n\n    private static bool HasOverlapSimple(this ITimeRange timeRange, ITimeRange outer) => HasOverlapSimple(timeRange.Start, timeRange.End, outer.Start, outer.End);\n    private static bool HasOverlapSimple(DateTime? min1, DateTime? max1, DateTime? min2, DateTime? max2)\n    {\n        return HasOverlapSimple(\n           min1 ?? DateTime.MinValue,\n           max1 ?? DateTime.MaxValue,\n           min2 ?? DateTime.MinValue,\n           max2 ?? DateTime.MaxValue);\n    }\n    private static bool HasOverlapSimple(DateTime min1, DateTime max1, DateTime min2, DateTime max2)\n    {\n        var isExact = min1 == min2 &amp;&amp; max1 == max2;\n        if (isExact) return true;\n\n        \/\/ if one of the periods is a single point in time\n        \/\/ then match inclusive at the start or end (as overlap)\n        var isInclusive = min1 == max1 || min2 == max2;\n        if (isInclusive)\n            return min1 &lt;= max2 &amp;&amp; max1 >= min2;\n\n        return min1 &lt; max2 &amp;&amp; max1 > min2;\n    }\n}\n\npublic sealed class TimeRange : ITimeRange\n{\n    public TimeRange(DateTime? start, DateTime? end)\n    {\n        Start = start;\n        End = end;\n    }\n    public DateTime? Start { get; set; }\n    public DateTime? End { get; set; }\n    public static ITimeRange Today => new TimeRange(DateTime.Today, DateTime.Today.AddDays(1).AddSeconds(-1));\n\n    public static ITimeRange From(DateTime? start, DateTime? end) => new TimeRange(start, end);\n    public static ITimeRange GetRelativeYear(int relative = 0)\n    {\n        var month = DateTime.Now.AddYears(relative);\n        var start = new DateTime(month.Year, 1, 1);\n        var end = start.AddYears(1).AddSeconds(-1);\n        return new TimeRange(start, end);\n    }\n    public static ITimeRange GetRelativeMonth(int relative = 0)\n    {\n        var month = DateTime.Now.AddMonths(relative);\n        var start = new DateTime(month.Year, month.Month, 1);\n        var end = start.AddMonths(1).AddSeconds(-1);\n        return new TimeRange(start, end);\n    }\n    public static ITimeRange GetRelativeWeek(int relative)\n    {\n        var date = DateTime.Now;\n        while (date.DayOfWeek != DayOfWeek.Monday)\n            date = date.AddDays(-1);\n        date = date.AddDays(relative * 7);\n        return new TimeRange(date, date.AddDays(6));\n    }\n    public static ITimeRange GetRelativeDay(int relative)\n    {\n        var date = DateTime.Today;\n        date = date.AddDays(relative);\n        return new TimeRange(date, date.AddDays(1).AddSeconds(-1));\n    }\n\n    public static string AsString(ITimeRange period) => $\"{period.Start:s}-{period.End:s}\";\n\n    \/\/ [---------------]     [----------] &lt;= children\n    \/\/            [---------------]       &lt;= children  \n    \/\/ ==>\n    \/\/     [-------------------------]    &lt;= parent\n    \/\/     [-----][----][---][-------]\n    public static IEnumerable&lt;TimeRange> GetPeriodsFlattened(ITimeRange parent, IEnumerable&lt;ITimeRange> children)\n    {\n        if (!children.Any())\n        {\n            yield return new TimeRange(parent.Start, parent.End);\n            yield break;\n        }\n\n        var allTimesInParent = children.SelectMany(x => LinqHelper.Select(x.Start, x.End))\n            .Concat(LinqHelper.Select(parent.Start, parent.End))\n            .Where(x => x.HasValue &amp;&amp; x >= parent.Start &amp;&amp; x &lt;= parent.End)\n            .OrderBy(x => x).Distinct().ToList();\n\n        if (allTimesInParent.Count &lt; 2)\n        {\n            yield return new TimeRange(parent.Start, parent.End);\n            yield break;\n        }\n\n        DateTime? prev = null;\n        foreach (var curr in allTimesInParent)\n        {\n            if (prev != null)\n                yield return new TimeRange(prev, curr);\n            prev = curr;\n        }\n    }\n}\n<\/pre><\/div>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"csharp\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">\/\/ With number ranges:\nprivate static bool HasOverlapSimple(int? min1, int? max1, int? min2, int? max2)\n{\n    return HasOverlapSimple(\n       min1 ?? int.MinValue,\n       max1 ?? int.MaxValue,\n       min2 ?? int.MinValue,\n       max2 ?? int.MaxValue);\n}\nprivate static bool HasOverlapSimple(int min1, int max1, int min2, int max2)\n{\n    var isExact = min1 == min2 &amp;&amp; max1 == max2;\n    if (isExact) return true;\n\n    \/\/ if one of the ranges is a single point\n    \/\/ then match inclusive at the start or end (as overlap)\n    var isInclusive = min1 == max1 || min2 == max2;\n    if (isInclusive)\n        return min1 &lt;= max2 &amp;&amp; max1 >= min2;\n\n    return min1 &lt; max2 &amp;&amp; max1 > min2;\n}\n\nprivate static bool HasOverlapSimple(double? min1, double? max1, double? min2, double? max2)\n{\n    return HasOverlapSimple(\n       min1 ?? double.MinValue,\n       max1 ?? double.MaxValue,\n       min2 ?? double.MinValue,\n       max2 ?? double.MaxValue);\n}\nprivate static bool HasOverlapSimple(double min1, double max1, double min2, double max2)\n{\n    var isExact = min1 == min2 &amp;&amp; max1 == max2;\n    if (isExact) return true;\n\n    \/\/ if one of the ranges is a single point\n    \/\/ then match inclusive at the start or end (as overlap)\n    var isInclusive = min1 == max1 || min2 == max2;\n    if (isInclusive)\n        return min1 &lt;= max2 &amp;&amp; max1 >= min2;\n\n    return min1 &lt; max2 &amp;&amp; max1 > min2;\n}<\/pre><\/div>\n\n\n\n<div style=\"height: 250px; position:relative; margin-bottom: 50px;\" class=\"wp-block-simple-code-block-ace\"><pre class=\"wp-block-simple-code-block-ace\" style=\"position:absolute;top:0;right:0;bottom:0;left:0\" data-mode=\"csharp\" data-theme=\"monokai\" data-fontsize=\"14\" data-lines=\"Infinity\" data-showlines=\"true\" data-copy=\"false\">using System;\nusing Domain.TimeRanges;\nusing NUnit.Framework;\n\nnamespace Domain.Tests\n{\n    [TestFixture]\n    public class TimeRangeOverlapTests\n    {\n        [Test]\n        public void TestSomeNonOverlappingRanges()\n        {\n            var firstDate = new DateTime(2022, 6, 15);\n            var SecondDate = new DateTime(2022, 6, 18);\n\n            var range1 = new TimeRange(null, firstDate);\n            var range2 = new TimeRange(firstDate, SecondDate);\n            var range3 = new TimeRange(SecondDate, null);\n\n            Assert.That(!range1.HasOverlap(range2));\n            Assert.That(!range1.HasOverlap(range3));\n\n            Assert.That(!range2.HasOverlap(range1));\n            Assert.That(!range2.HasOverlap(range3));\n\n            Assert.That(!range3.HasOverlap(range1));\n            Assert.That(!range3.HasOverlap(range2));\n        }\n\n        [Test]\n        public void TestSomeOverlappingRanges()\n        {\n            var firstDate = new DateTime(2022, 6, 15);\n            var SecondDate = new DateTime(2022, 6, 18);\n\n            var range1 = new TimeRange(null, SecondDate);\n            var range2 = new TimeRange(firstDate, null);\n\n            var range3 = new TimeRange(firstDate, firstDate);\n            var range4 = new TimeRange(firstDate, firstDate);\n\n            var range5 = new TimeRange(firstDate, SecondDate);\n            var range6 = new TimeRange(SecondDate, SecondDate);\n\n            Assert.That(range1.HasOverlap(range2));\n            Assert.That(range3.HasOverlap(range4));\n            Assert.That(range5.HasOverlap(range6));\n        }\n    }\n}<\/pre><\/div>\n\n\n\n<p>Old code:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public static TimeSpan DateRangeOverlapTimeSpan(DateTime min1, DateTime max1, DateTime min2, DateTime max2)\n{\n    if (min1 > max1 || min2 > max2)\n    {\n        throw new Exception(\"Invalid date range\");\n    }\n\n    \/\/ Cases: \n    \/\/            [---- Date1 ----]\n    \/\/                [--2C--]  \n    \/\/      [------------2D------------]   \n    \/\/      [----2A----]    [----2B----]\n    \/\/ [--E--]                        [--F--]\n\n    \/\/ 2E, 2F: skip\n    if (max2 &lt;= min1 || min2 >= max1)\n    {\n        return new TimeSpan(0);\n    }\n\n    \/\/ 2C\n    if (min1 &lt;= min2 &amp;&amp; max1 >= max2)\n    {\n        return (max2 - min2).Duration();\n    }\n    \/\/ 2D\n    if (min1 >= min2 &amp;&amp; max1 &lt;= max2) { return (max1 - min1).Duration(); } \/\/ 2A if (min1 >= min2 &amp;&amp; max1 >= max2 &amp;&amp; max2 >= min1)\n    {\n        return (max2 - min1).Duration();\n    }\n    \/\/ 2B\n    if (min1 &lt;= min2 &amp;&amp; max1 >= min2 &amp;&amp; max2 >= max1)\n    {\n        return (max1 - min2).Duration();\n    }\n\n    return new TimeSpan(0);\n}\n\npublic static TimeSpan TimespanRangeOverlapTimeSpan(TimeSpan min1, TimeSpan max1, TimeSpan min2, TimeSpan max2)\n{\n    if (min1 > max1 || min2 > max2)\n    {\n        throw new Exception(\"Invalid date range\");\n    }\n\n    \/\/ Cases: \n    \/\/            [---- Date1 ----]\n    \/\/                [--2C--]  \n    \/\/      [------------2D------------]   \n    \/\/      [----2A----]    [----2B----]\n    \/\/ [--E--]                        [--F--]\n\n    \/\/ 2E, 2F: skip\n    if (max2 &lt;= min1 || min2 >= max1) {\n        return new TimeSpan(0);\n    }\n\n    \/\/ 2C\n    if (min1 &lt;= min2 &amp;&amp; max1 >= max2)\n    {\n        return (max2 - min2).Duration();\n    }\n    \/\/ 2D\n    if (min1 >= min2 &amp;&amp; max1 &lt;= max2) { return (max1 - min1).Duration(); } \/\/ 2A if (min1 >= min2 &amp;&amp; max1 >= max2 &amp;&amp; max2 >= min1)\n    {\n        return (max2 - min1).Duration();\n    }\n    \/\/ 2B\n    if (min1 &lt;= min2 &amp;&amp; max1 >= min2 &amp;&amp; max2 >= max1)\n    {\n        return (max1 - min2).Duration();\n    }            \n    \n    return new TimeSpan(0);\n}\n\npublic void TestTimespanRangeOverlapTimeSpan()\n{\n    \/\/ Cases: \n    \/\/            [---- Date1 ----]\n    \/\/                [--2C--]  \n    \/\/      [------------2D------------]   \n    \/\/      [----2A----]    [----2B----]\n    \/\/ [--E--]                        [--F--]\n\n    TimeSpan min1 = new TimeSpan(8, 0, 0);\n    TimeSpan max1 = new TimeSpan(17, 0, 0);\n\n    TimeSpan min2;\n    TimeSpan max2;\n\n    \/\/ Case C:\n    min2 = new TimeSpan(10, 0, 0);\n    max2 = new TimeSpan(11, 0, 0);\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(1));\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(1));\n\n    \/\/ Case D:\n    min2 = new TimeSpan(7, 0, 0);\n    max2 = new TimeSpan(18, 0, 0);\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(9));\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(9));\n\n    \/\/ Case A:\n    min2 = new TimeSpan(7, 0, 0);\n    max2 = new TimeSpan(10, 0, 0);\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(2));\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(2));\n\n    \/\/ Case B:\n    min2 = new TimeSpan(16, 0, 0);\n    max2 = new TimeSpan(18, 0, 0);\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(1));\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(1));\n\n    \/\/ Case E:\n    min2 = new TimeSpan(4, 0, 0);\n    max2 = new TimeSpan(8, 0, 0);\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(0));\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(0));\n\n    \/\/ Case F:\n    min2 = new TimeSpan(17, 0, 0);\n    max2 = new TimeSpan(18, 0, 0);\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min1, max1, min2, max2), TimeSpan.FromHours(0));\n    Assert.AreEqual(DateTimeHelper.TimespanRangeOverlapTimeSpan(min2, max2, min1, max1), TimeSpan.FromHours(0));\n}<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Old code:<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[6],"tags":[],"class_list":["post-658","post","type-post","status-publish","format-standard","hentry","category-dotnet"],"_links":{"self":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/658","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/comments?post=658"}],"version-history":[{"count":7,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/658\/revisions"}],"predecessor-version":[{"id":9741,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/posts\/658\/revisions\/9741"}],"wp:attachment":[{"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/media?parent=658"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/categories?post=658"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/solidt.eu\/site\/wp-json\/wp\/v2\/tags?post=658"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}