0

I have a list of objects which have StartAt and EndAt, properties of type DateTime, representing the start and end times of a reading session. I want to calculate the total duration of reading for each day, considering that a reading session can span across two days. For example, if a reading session starts on May 1 at 11:00 PM and ends on May 2 at 1:30 AM, I want to count 1 hour of reading for May 1 and 1.5 hours of reading for May 2. How can I achieve this using LINQ in C#?

DateTime currentDate  = DateTime.UtcNow;
DateTime previousDate = currentDate.AddDays(-1);

var result = readingSessions
    .Where(x => x.StartAt.Date == currentDate.Date || x.StartAt.Date == previousDate.Date)
    .GroupBy(x => x.StartAt.Date)
    .Select(x => new
        {
            Date = x.Key,
            Sum = TimeSpan.FromSeconds(x.Sum(y => (y.EndAt - y.StartAt).TotalSeconds))
        })
    .ToList();
public class ReadingSession
{
   public DateTime StartAt {get;set;}
   public DateTime EndAt {get;set;}
}

Details

StartAt EndAt
2023-05-01 08:00:00 PM 2023-05-01 09:00:00 PM
2023-05-01 11:00:00 PM 2023-05-02 01:30:00 AM

How to calculate ?

Let's assume that we want to calculate the total reading duration for May 1st and May 2nd based on the given StartAt and EndAt times.

For the first row, the user started reading at 8:00 PM on May 1st and ended at 9:00 PM on the same day. Therefore, the total reading duration on May 1st is 1 hour.

For the second row, the user started reading at 11:00 PM on May 1st and ended at 1:30 AM on May 2nd. In this case, we need to split the reading duration into two parts: one for May 1st and one for May 2nd.

On May 1st, the user read from 11:00 PM to 11:59 PM, which is a total of 1 hour. On May 2nd, the user read from 12:00 AM to 1:30 AM, which is a total of 1.5 hours. Therefore, the total reading duration on May 1st is 1 hour and the total reading duration on May 2nd is 1.5 hours.

What should the result look like?

On May 1st, the reader started at 8:00 PM and ended at 9:00 PM, which gives us a duration of 1 hour. They also started reading at 11:00 PM and continued until 12:00 AM, which adds another hour, giving a total of 2 hours for May 1st.

On May 2nd, the reader started reading at 12:00 AM and ended at 1:30 AM, which gives us a duration of 1.5 hours.

Therefore, the total reading duration for May 1st is 2 hours and for May 2nd it is 1.5 hours.

byteram
  • 125
  • 8
  • 1
    What do you mean by `list with two columns`? – Lior v May 05 '23 at 14:04
  • Have tried just using date subtraction? Apparently, you'd need to retrieve the dates using LINQ and do subtraction afterwards. Please post code about how you might achieve this.... See also https://stackoverflow.com/questions/570858/linq-to-entities-for-subtracting-2-dates – JosephDoggie May 05 '23 at 14:04
  • Also, if iterating through a list, you could try just subtracting the values at each index in a for or foreach loop... – JosephDoggie May 05 '23 at 14:05
  • I dont get it. When a session starts on May 1 and ends on May2 why 1hour for May 1 and 1.5 for May 2 ? Is there a daily start and end time or how is this calculated? – jeb May 05 '23 at 14:21
  • 1
    @jeb If a reading session starts on May 1 and ends on May 2, we need to calculate the duration for each day separately. For May 1, we only count the hours from the start time until the end of the day (which is usually at 11:59:59 PM). For May 2, we count the hours from the start of the day (usually at 12:00:00 AM) until the end time of the reading session. – byteram May 05 '23 at 14:26
  • 2
    Lists don't have columns. Do you mean you have a list of objects which have `StartAt` and `ReadAt` properties of type `DateTime`? Please show sample copy-pastable code that shows what `readingSessions` is and how it's populated. Also showing the initialization of `currentDate` and `previousDate` would be helpful. Also, I don't think that `Where` clause would compile without adding a `x =>`. – Rufus L May 05 '23 at 14:45
  • The code you've written correctly returns `03:14:02` as the sum of the reading session durations for all reading sessions that started on `2023-05-01` (the first one lasts `02:37:01` and the second lasts `00:37:01`). What do you want it to return instead? – Rufus L May 05 '23 at 14:51
  • Thank you all for your replies. I have updated the question with more details about the issue. – byteram May 05 '23 at 15:16

4 Answers4

0

Here is how i solved it: First I determine if a ReadingSession spans multiple days. If it spans multiple days I split it. The rest is pretty much the same as you already had.

  internal class Program
    {
        static void Main(string[] args)
        {
            List<ReadingSession> readingSessions = new List<ReadingSession>()
            {
                new ()
                { 
                    StartAt = new DateTime(2023, 5, 1, 23, 0, 0),
                    EndAt = new DateTime(2023, 5, 2, 1, 0, 0)
                },
                new ()
                {
                    StartAt = new DateTime(2023, 5, 1, 22, 0, 0),
                    EndAt = new DateTime(2023, 5, 1, 23, 0, 0)
                },
            };

            var dailyDurations = readingSessions
                .SelectMany(rs => 
                {
                    if (rs.SingleDay)
                    { 
                        return new[] { rs };
                    }
                    else
                    {
                        // split it into two sessions,
                        // one for each day
                        return new[]
                        {
                            new ReadingSession { StartAt = rs.StartAt, EndAt = rs.StartAt.Date.AddDays(1) },
                            new ReadingSession { StartAt = rs.EndAt.Date, EndAt = rs.EndAt }
                        };
                    }
                })
                .GroupBy(rs => rs.StartAt.Date)
                .Select(g => new
                {
                    Date = g.Key, Sum = g.Sum(rs => (rs.EndAt - rs.StartAt).TotalMinutes)
                })
                .ToList();
        }

        public class ReadingSession
        {
            public DateTime StartAt { get; set; }
            public DateTime EndAt { get; set; }
            public bool SingleDay => EndAt.Date == StartAt.Date;
        }
jeb
  • 848
  • 5
  • 16
0

Here is my solution:

var readingSessions = new List<ReadingSession>()
        {
            new ReadingSession(){StartAt = DateTime.Parse("2023-05-01 10:22:59 PM"), EndAt = DateTime.Parse("2023-05-02 01:00:00 AM")},
            new ReadingSession(){StartAt = DateTime.Parse("2023-05-01 10:22:59 PM"), EndAt = DateTime.Parse("2023-05-01 11:00:00 PM")}
        };

        IEnumerable <(int DayOfYear,TimeSpan TotalDuration)> result = readingSessions.Select(session => 
        (
            session.EndAt.DayOfYear,
            new TimeSpan(
                            readingSessions.Where(r => r.EndAt.DayOfYear == session.EndAt.DayOfYear)
                                           .Sum(rs => rs.EndAt.DayOfYear == session.StartAt.DayOfYear?
                                                        rs.EndAt.Subtract(rs.StartAt).Ticks :
                                                        rs.EndAt.TimeOfDay.Ticks)
                        )
        ));
        result.ToList()
              .ForEach(r => Console.WriteLine($"DayOfYear: {r.DayOfYear} TotalDuration: {r.TotalDuration}"));

A couple of points to consider:

  1. It is better to extract the sum into a different function
  2. If the reading session spans more than 2 days, the code would only sum the starting date and end date duration
0

Thanks for clarifying your question. I think what you want is a method that will take a single ReadingSession and split it into daily reading sessions. This will then give you a list of sessions that only occur in a single day.

For example, if a reading session lasted over a period of 5 days, the following would return a list of 5 separate reading sessions, one for each day, with the first one starting at the start time and the last one ending on the end time, and the others all starting/ending at the beginning of a day:

static List<ReadingSession> SplitIntoDailySessions(ReadingSession session)
{
    if (session == null) return null;

    // Capture initial start value (so as not to modify 'session' later)
    var start = session.StartAt;
    var result = new List<ReadingSession>();

    // While the end date is greater than the start date, create a new session 
    // from the start date until the beginning of the next day and add it to
    // our reault, then reset the start date to the beginning of the next day.
    while(session.EndAt.Date > start.Date)
    {
        result.Add(new ReadingSession { StartAt = start, 
            EndAt = start.Date.AddDays(1) });

        start = start.Date.AddDays(1);
    }

    // Now our start and end are on the same date, so we can add the final result
    result.Add(new ReadingSession { StartAt = start, EndAt = session.EndAt });

    return result;
}

Now we can just take our initial list of reading sessions and split it into daily sessions like so:

List<ReadingSession> dailyReadingSessions = readingSessions
    .SelectMany(rs => SplitIntoDailySessions(rs))
    .ToList();

Now your original code should work correctly, since each reading session in dailyReadingSessions spans only a single Date.

Rufus L
  • 36,127
  • 5
  • 30
  • 43
0

Assuming a reading session is always less than 24 hours (e.g. spans two days at most), then you can just split any reading sessions that are across midnight, then use your code to group and compute the result. I left out the Where method, as I wasn't sure how you wanted the final filter to be:

var result = readingSessions
                .SelectMany(r => r.EndAt.Date > r.StartAt.Date // spans two days?
                                ? new[] { // reading time on first day
                                          new ReadingSession { StartAt = r.StartAt, EndAt = r.EndAt.Date },
                                          // reading time on second day
                                          new ReadingSession { StartAt = r.EndAt.Date, EndAt = r.EndAt }
                                        }
                                : new[] { r })
                .GroupBy(x => x.StartAt.Date) // group all reading time for each day
                .Select(x => new {
                    Date = x.Key,
                    Sum = TimeSpan.FromSeconds(x.Sum(y => (y.EndAt - y.StartAt).TotalSeconds))
                })
                .ToList();

NOTE: This isn't terribly efficient as it generates an array for every one day reading session and then throws it away - you could be a little more space efficient at the expense of time by scanning the list twice and using union to combine the single day sessions with the split up two day sessions, but that probably isn't worth the complexity. That makes me think a SplitUnion method might be nice...

NetMage
  • 26,163
  • 3
  • 34
  • 55