swiftdatetimehealthkit

Duration of array of DateIntevals that excludes overlapping times


I am trying to calculate sleep duration time from HealthKit data. For sleep, HealthKit returns all samples from different sources, even if the samples overlap. (Unlike things like steps, where you can use an HKStatisticsQuery and it de-duplicates the data for you).

Each sample contains a start and end date. I've put these start/end dates into a [DateInterval] array, the results of which are below (the time is in seconds).

start 2020-08-26 02:55:00 +0000 end 2020-08-26 03:00:00 +0000 time 300.0
start 2020-08-26 03:40:00 +0000 end 2020-08-26 05:00:00 +0000 time 4800.0
start 2020-08-26 05:15:00 +0000 end 2020-08-26 07:15:00 +0000 time 7200.0
start 2020-08-26 07:25:00 +0000 end 2020-08-26 08:00:00 +0000 time 2100.0
start 2020-08-26 08:10:00 +0000 end 2020-08-26 08:50:00 +0000 time 2400.0
start 2020-08-26 03:05:45 +0000 end 2020-08-26 11:51:47 +0000 time 31562.0
start 2020-08-26 11:51:50 +0000 end 2020-08-26 11:51:53 +0000 time 3.0
start 2020-08-26 12:10:39 +0000 end 2020-08-26 12:10:40 +0000 time 1.0

I need an algorithm or some sort of logic that doesn't duplicate the sleep time. In this example, adding all the durations of the time intervals together returns 13 hrs and 26 minutes. Deduplicating the data, it should add up to 8 hrs and 51 mins (according to what the Health app is displaying).


Solution

  • Haven't tested it properly, but you get the idea. Sort them and skip the ones that overlap

     func calculateSpentTime(for intervals: [DateInterval]) -> TimeInterval {  
        guard intervals.count > 1 else {
            return intervals.first?.duration ?? 0
        }
        
        let sorted = intervals.sorted { $0.start < $1.start }
        
        var total: TimeInterval = 0
        var start = sorted[0].start
        var end = sorted[0].end
        
        for i in 1..<sorted.count {
            
            if sorted[i].start > end {
                total += end.timeIntervalSince(start)
                start = sorted[i].start
                end = sorted[i].end
            } else if sorted[i].end > end {
                end = sorted[i].end
            }
        }
        
        total += end.timeIntervalSince(start)
        return total
    }