Nedo2490
Nedo2490

Reputation: 51

How to calculate total seconds of many overlapping DateTimes

I am trying to find a way to calculate the total amount of seconds a specific device has been faulty due to different kinds of errors that have a StartTime and and EndTime and can be happening in the same moment but in different durations.

That's the collection of DateTimes I have:

private class TimeLapse
{
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
}

Dictionary<string, List<TimeLapse>> _devices = new Dictionary<string, List<TimeLapse>>();

where the string in the dictionary is the name of the device;

but I don't undertand where to start not to build a disgusting code to do that. Anyone had the same problem to solve?

Upvotes: 2

Views: 246

Answers (3)

tinudu
tinudu

Reputation: 1199

Demonstration of the beauty of LINQ by writing an extension method.

/// <summary>
/// Gets the duration of the set union of the specified intervals.
/// </summary>
/// <param name="timeLapses">Sequence of <see cref="TimeLapse"/> ordered by <see cref="TimeLapse.StartTime"/>.</param>
public static TimeSpan UnionDurations(this IEnumerable<TimeLapse> timeLapses)
{
    using (var e = timeLapses.GetEnumerator())
    {
        if (!e.MoveNext()) // no items, no duration
            return TimeSpan.Zero;

        var prev = e.Current;
        var total = prev.EndTime - prev.StartTime; // set running total to duration of 1st interval

        while (e.MoveNext())
        {
            var curr = e.Current;
            if (curr.StartTime < prev.StartTime) throw new Exception($"{nameof(timeLapses)} are not in ascending {nameof(TimeLapse.StartTime)} order.");

            var increase = curr.EndTime - (curr.StartTime > prev.EndTime ? curr.StartTime : prev.EndTime);
            if (increase <= TimeSpan.Zero) continue;
            total += increase;
            prev = curr;
        }

        return total;
    }
}

Test code:

var input = new Dictionary<string, IList<TimeLapse>>
{
    {
        "A",
        new[]
        {
            new TimeLapse{ StartTime = new DateTime(2019, 1, 17, 0, 0, 0), EndTime = new DateTime(2019, 1, 17, 3, 0, 0)},
            new TimeLapse{ StartTime = new DateTime(2019, 1, 17, 1, 0, 0), EndTime = new DateTime(2019, 1, 17, 2, 0, 0)},
            new TimeLapse{ StartTime = new DateTime(2019, 1, 17, 1, 0, 0), EndTime = new DateTime(2019, 1, 17, 4, 0, 0)},
            new TimeLapse{ StartTime = new DateTime(2019, 1, 17, 5, 0, 0), EndTime = new DateTime(2019, 1, 17, 7, 0, 0)}
        }
    },
    {
        "B",
        new TimeLapse [0]
    }
};
var result = input
    .Select(kv => new
    {
        Device = kv.Key,
        FaultyDuration = kv.Value
            // .OrderBy(tl => tl.StartTime) // this line can be removed if already ordered by StartTime
            .UnionDurations()
    })
    .ToList();
// { Device = A, FaultyDuration = 06:00:00 }
// { Device = B, FaultyDuration = 00:00:00 }

Upvotes: 1

izzy255
izzy255

Reputation: 21

I am not sure what you would consider "disgusting", but if you just want the amount of seconds for each timelapse you could create a method for your TimeLapse class that returns just that.

private class TimeLapse
{
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }

    public double GetSecondsPassed() {
        return (EndTime - StartTime).TotalSeconds
    }
}

The difference between two DateTime objects returns a TimeSpan object. And instead of having a dictionary of your TimeLapse objects you can have a dictionary of doubles (which represents seconds).

var _devices = new Dictionary<string, double>()

Upvotes: 0

Rufus L
Rufus L

Reputation: 37020

One way to do this would be to extend your class with an additional method that will Merge a list of TimeLapse objects by taking any that overlap and combining them into a single TimeLapse, and then returning this set. If we do this, then we can just add up the duration of each item in the set. You could also add a property that exposes the Duration of the TimeLapse object:

private class TimeLapse
{
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
    public TimeSpan Duration => (EndTime - StartTime).Duration();

    public static List<TimeLapse> Merge(List<TimeLapse> items)
    {
        if (items == null || items.Count < 2) return items;

        var results = new List<TimeLapse>();

        foreach (var item in items)
        {
            var overlappingItem = results.FirstOrDefault(item.OverlapsWith);
            if (overlappingItem == null) results.Add(item);
            else overlappingItem.CombineWith(item);
        }

        return results;
    }

    private bool OverlapsWith(TimeLapse other)
    {
        return other != null &&
               other.StartTime <= EndTime &&
               other.EndTime >= StartTime;
    }

    private void CombineWith(TimeLapse other)
    {
        if (!OverlapsWith(other)) return;
        if (other.StartTime < StartTime) StartTime = other.StartTime;
        if (other.EndTime > EndTime) EndTime = other.EndTime;
    }
}

Below is an example of how you can display the durations for each item in your dictionary.

I included a method to generate a dummy list of devices, so I'm using Days because it was easier to write and to validate that the results are correct, but since Duration is a TimeSpan, you can get just about any unit of measurement you want (like TotalSeconds in your case):

private static void Main()
{
    Dictionary<string, List<TimeLapse>> devices = GetDeviceList();

    foreach (var device in devices)
    {
        Console.WriteLine("{0}: {1} total days", device.Key,
            TimeLapse.Merge(device.Value).Sum(value => value.Duration.TotalDays));
    }

    GetKeyFromUser("Done! Press any key to exit...");
}

private static Dictionary<string, List<TimeLapse>> GetDeviceList()
{
    return new Dictionary<string, List<TimeLapse>>
    {
        // device1 total should be 4 days (1/1 - 1/5)
        {"device1", new List<TimeLapse>{
            new TimeLapse {StartTime = DateTime.Parse("1/1/2019"),
                EndTime = DateTime.Parse("1/3/2019")},
            new TimeLapse {StartTime = DateTime.Parse("1/2/2019"),
                EndTime = DateTime.Parse("1/3/2019")},
            new TimeLapse {StartTime = DateTime.Parse("1/3/2019"),
                EndTime = DateTime.Parse("1/5/2019")}}},

        // device2 total should be 7 days (1/1 - 1/4 plus 1/6 - 1/10)
        {"device2", new List<TimeLapse>{
            new TimeLapse {StartTime = DateTime.Parse("1/1/2019"),
                EndTime = DateTime.Parse("1/3/2019")},
            new TimeLapse {StartTime = DateTime.Parse("1/3/2019"),
                EndTime = DateTime.Parse("1/4/2019")},
            new TimeLapse {StartTime = DateTime.Parse("1/6/2019"),
                EndTime = DateTime.Parse("1/10/2019")}}},

        // device3 total should be 2 days (1/1 - 1/2 plus 1/6 - 1/7)
        {"device3", new List<TimeLapse>{
            new TimeLapse {StartTime = DateTime.Parse("1/1/2019"),
                EndTime = DateTime.Parse("1/2/2019")},
            new TimeLapse {StartTime = DateTime.Parse("1/6/2019"),
                EndTime = DateTime.Parse("1/7/2019")}}},

        // device4 total should be 2 days (1/1 - 1/3)
        {"device4", new List<TimeLapse>{
            new TimeLapse {StartTime = DateTime.Parse("1/1/2019"),
                EndTime = DateTime.Parse("1/3/2019")}}},
    };
}

Output

enter image description here

Upvotes: 4

Related Questions