October 2, 2015 · Code

Code Problem of the Week - Calculating Average Bed Times

One of the features of the software I work on is it's ability to tell you how well/poor you slept during the night. The software can also automatically calculate when you went to sleep and when you woke up.

We're currently working on a way to automatically calculate bed times and metrics on those bed times for thousands of files at the same time. One of the more interesting metrics we are calculating is an average time in bed (and out of bed). It's not as easy as you'd think.

My first attempt was to average the list of DateTimes. It works GREAT if you have a bed time on every night.

Given bed times of:

  1. 2015-01-01 10:00pm to 2015-01-02 6:00am
  2. 2015-01-02 11:00pm to 2015-01-03 5:00am
  3. 2015-01-04 12:00am to 2015-01-04 7:00am

We would expect an average in-bed time of 11:00pm

var dates = new List<DateTime>(3)  
{
    new DateTime(2015, 1, 1, 22, 0, 0),
    new DateTime(2015, 1, 2, 23, 0, 0),
    new DateTime(2015, 1, 4, 0, 0, 0)
};

var avgticks = dates.Average(x => x.Ticks);  
var avgtickDateTime = new DateTime((long)avgticks);  
avgtickDateTime.TimeOfDay.Dump();

//results in 11:00pm average time

This code works great because there are an ODD number of bed times and the nights are consecutive. If we only had the first two in-bed times, it would results in an average of 10:30AM!! It's halfway between 10pm on the first night an 11pm on the second.

var dates = new List<DateTime>(4)  
{
    new DateTime(2015, 1, 1, 22, 0, 0),
    new DateTime(2015, 1, 2, 23, 0, 0),
};

var avgticks = dates.Average(x => x.Ticks);  
var avgtickDateTime = new DateTime((long)avgticks);  
avgtickDateTime.TimeOfDay.Dump();

//results in 10:30am average time

So we know we can't use an average DateTime to calculate the average time in bed.

Then I tried using the average of the DateTime.TimeOfDay for each time-in bed. It works great when all of the in-bed times are before midnight:

var dates = new List<DateTime>(3)  
{
    new DateTime(2015, 1, 1, 22, 0, 0),
    new DateTime(2015, 1, 2, 23, 0, 0)
};

double doubleAverageTicks = dates.Select(x => x.TimeOfDay).Average(timeSpan => timeSpan.Ticks);  
long longAverageTicks = Convert.ToInt64(doubleAverageTicks);

var average = new TimeSpan(longAverageTicks);  
average.Dump();  
\\results in 10:30pm average time

Now let's use the same averaging method with mixing times before and after midnight:

var dates = new List<DateTime>(3)  
{
    new DateTime(2015, 1, 1, 21, 0, 0),
    new DateTime(2015, 1, 2, 22, 0, 0),
    new DateTime(2015, 1, 4, 0, 0, 0)
};

double doubleAverageTicks = dates.Select(x => x.TimeOfDay).Average(timeSpan => timeSpan.Ticks);  
long longAverageTicks = Convert.ToInt64(doubleAverageTicks);

var average = new TimeSpan(longAverageTicks);  
average.Dump();  
\\results in 2:20pm average time

We know 2:20pm isn't correct. Averaging using TimeOfDay isn't a good solution either.

The ultimate solution I came up with uses a mix of the TimeOfDay property and accounting for times that cross over midnight. To handle this, I use the opposite of a 24-hour clock. 10pm in 24-hour time is 2200. Instead, I use a value as 1000. And for 1am, I use a value of 1300. If you average those two numbers (1000, 1300) you get 1130. Then convert that result BACK using the same opposite 24-hour clock: 2330 or 11:30pm.

var dates = new List<DateTime>(3)  
{
    new DateTime(2015, 1, 1, 22, 0, 0),
    new DateTime(2015, 1, 2, 0, 0, 0),
    new DateTime(2015, 1, 4, 2, 0, 0)
};

GetAverageTimeFromTimespans(dates.Select(x => x.TimeOfDay));

public TimeSpan GetAverageTimeFromTimespans(IEnumerable<TimeSpan> timespans)  
{
    if (timespans == null)
        throw new NullReferenceException("timespans can't be null");

    if (!timespans.Any())
        throw new ArgumentException("timespans must contain at least one value");

    var adjustedDates = new List<TimeSpan>(3);
    foreach (var timespan in timespans)
    {       
        if (timespan.Hours >= 12)
            adjustedDates.Add(timespan - TimeSpan.FromHours(12));
        else
            adjustedDates.Add(timespan + TimeSpan.FromHours(12));
    }

    //calculate average Time for opposite times
    double doubleAverageTicks = adjustedDates.Average(timeSpan => timeSpan.Ticks);
    long longAverageTicks = Convert.ToInt64(doubleAverageTicks);    
    var averageTimespan = new TimeSpan(longAverageTicks);

    //convert average BACK to normal time
    TimeSpan averageBedTime;    
    if (averageTimespan.Hours >= 12)
        averageBedTime = averageTimespan - TimeSpan.FromHours(12);
    else
        averageBedTime = averageTimespan + TimeSpan.FromHours(12);

    return averageBedTime;
}

This method works for the following scenarios:

  1. One or more dates
  2. Dates that are all PM
  3. Dates that are all AM
  4. Dates that are combination of AM and PM

The one scenario that causes issues for any average bed time calculation is someone who is on shift work and has a mixture of night-time and day-time sleep periods. I don't know if there is a way to solve that unless we calculate an average night-time and a separate average day-time version.

Let me know if you have a better way to calculate bed times!

Comments powered by Disqus