Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Learn/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
dataManager.authorize({
DispatchQueue.main.async {
lessonsVC.lessons = [
TimeInRangeLesson(dataManager: dataManager)
TimeInRangeLesson(dataManager: dataManager),
ModalDayLesson(dataManager: dataManager),
]
}
})
Expand Down
52 changes: 52 additions & 0 deletions Learn/Configuration/DateIntervalEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// DateIntervalEntry.swift
// Learn
//
// Copyright © 2019 LoopKit Authors. All rights reserved.
//

import UIKit


class DateIntervalEntry: LessonSectionProviding {
let headerTitle: String?

let footerTitle: String?

let dateEntry: DateEntry
let numberEntry: NumberEntry

let cells: [LessonCellProviding]

init(headerTitle: String? = nil, footerTitle: String? = nil, start: Date, weeks: Int) {
self.headerTitle = headerTitle
self.footerTitle = footerTitle

self.dateEntry = DateEntry(date: start, title: NSLocalizedString("Start Date", comment: "Title of config entry"), mode: .date)
self.numberEntry = NumberEntry.integerEntry(value: weeks, unitString: NSLocalizedString("Weeks", comment: "Unit string for a count of calendar weeks"))

self.cells = [
self.dateEntry,
self.numberEntry
]
}
}

extension DateIntervalEntry {
convenience init(headerTitle: String? = nil, footerTitle: String? = nil, end: Date, weeks: Int, calendar: Calendar = .current) {
let start = calendar.date(byAdding: DateComponents(weekOfYear: -weeks), to: end)!
self.init(headerTitle: headerTitle, footerTitle: footerTitle, start: calendar.startOfDay(for: start), weeks: weeks)
}

var dateInterval: DateInterval? {
let start = dateEntry.date

guard let weeks = numberEntry.number?.intValue,
let end = Calendar.current.date(byAdding: DateComponents(weekOfYear: weeks), to: start)
else {
return nil
}

return DateInterval(start: start, end: end)
}
}
2 changes: 1 addition & 1 deletion Learn/Extensions/DateIntervalFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation


extension DateIntervalFormatter {
convenience init(dateStyle: DateIntervalFormatter.Style, timeStyle: DateIntervalFormatter.Style) {
convenience init(dateStyle: DateIntervalFormatter.Style = .none, timeStyle: DateIntervalFormatter.Style = .none) {
self.init()
self.dateStyle = dateStyle
self.timeStyle = timeStyle
Expand Down
15 changes: 15 additions & 0 deletions Learn/Extensions/OSLog.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// OSLog.swift
// Learn
//
// Copyright © 2019 LoopKit Authors. All rights reserved.
//

import os.log


extension OSLog {
convenience init(category: String) {
self.init(subsystem: "com.loopkit.Learn", category: category)
}
}
212 changes: 212 additions & 0 deletions Learn/Lessons/ModalDayLesson.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
//
// ModalDayLesson.swift
// Learn
//
// Copyright © 2019 LoopKit Authors. All rights reserved.
//

import Foundation
import HealthKit
import LoopCore
import LoopKit
import os.log

final class ModalDayLesson: Lesson {
let title = NSLocalizedString("Modal Day", comment: "Lesson title")

let subtitle = NSLocalizedString("Visualizes the most frequent glucose values by time of day", comment: "Lesson subtitle")

let configurationSections: [LessonSectionProviding]

private let dataManager: DataManager

private let dateIntervalEntry: DateIntervalEntry

private let glucoseUnit: HKUnit

init(dataManager: DataManager) {
self.dataManager = dataManager
self.glucoseUnit = dataManager.glucoseStore.preferredUnit ?? .milligramsPerDeciliter

dateIntervalEntry = DateIntervalEntry(
end: Date(),
weeks: 2
)

self.configurationSections = [
dateIntervalEntry
]
}

func execute(completion: @escaping ([LessonSectionProviding]) -> Void) {
guard let dates = dateIntervalEntry.dateInterval else {
// TODO: Cleaner error presentation
completion([LessonSection(headerTitle: "Error: Please fill out all fields", footerTitle: nil, cells: [])])
return
}

let calendar = Calendar.current

let calculator = ModalDayCalculator(dataManager: dataManager, dates: dates, bucketSize: .minutes(60), unit: glucoseUnit, calendar: calendar)
calculator.execute { (result) in
switch result {
case .failure(let error):
completion([
LessonSection(cells: [TextCell(text: String(describing: error))])
])
case .success(let buckets):
guard buckets.count > 0 else {
completion([
LessonSection(cells: [TextCell(text: NSLocalizedString("No data available", comment: "Lesson result text for no data"))])
])
return
}

let dateFormatter = DateIntervalFormatter(timeStyle: .short)
let glucoseFormatter = QuantityFormatter()
glucoseFormatter.setPreferredNumberFormatter(for: self.glucoseUnit)

completion([
LessonSection(cells: buckets.compactMap({ (bucket) -> TextCell? in
guard let start = calendar.date(from: bucket.time.lowerBound.dateComponents),
let end = calendar.date(from: bucket.time.upperBound.dateComponents),
let time = dateFormatter.string(from: DateInterval(start: start, end: end)),
let median = bucket.median,
let medianString = glucoseFormatter.string(from: median, for: bucket.unit)
else {
return nil
}

return TextCell(text: time, detailText: medianString)
}))
])
}
}
}
}


fileprivate extension TextCell {

}


fileprivate struct ModalDayBucket {
let time: Range<TimeComponents>
let orderedValues: [Double]
let unit: HKUnit

init(time: Range<TimeComponents>, unorderedValues: [Double], unit: HKUnit) {
self.time = time
self.orderedValues = unorderedValues.sorted()
self.unit = unit
}

var median: HKQuantity? {
let count = orderedValues.count
guard count > 0 else {
return nil
}

if count % 2 == 1 {
return HKQuantity(unit: unit, doubleValue: orderedValues[count / 2])
} else {
let mid = count / 2
let lower = orderedValues[mid - 1]
let upper = orderedValues[mid]
return HKQuantity(unit: unit, doubleValue: (lower + upper) / 2)
}
}
}


fileprivate struct ModalDayBuilder {
let calendar: Calendar
let bucketSize: TimeInterval
let unit: HKUnit
private(set) var unorderedValuesByBucket: [Range<TimeComponents>: [Double]]

init(calendar: Calendar, bucketSize: TimeInterval, unit: HKUnit) {
self.calendar = calendar
self.bucketSize = bucketSize
self.unit = unit
self.unorderedValuesByBucket = [:]
}

mutating func add(_ value: Double, at time: TimeComponents) {
let bucket = time.bucket(withBucketSize: bucketSize)
var values = unorderedValuesByBucket[bucket] ?? []
values.append(value)
unorderedValuesByBucket[bucket] = values
}

mutating func add(_ value: Double, at date: DateComponents) {
guard let time = TimeComponents(dateComponents: date) else {
return
}
add(value, at: time)
}

mutating func add(_ value: Double, at date: Date) {
add(value, at: calendar.dateComponents([.hour, .minute], from: date))
}

mutating func add(_ quantity: HKQuantity, at date: Date) {
add(quantity.doubleValue(for: unit), at: date)
}

var allBuckets: [ModalDayBucket] {
return unorderedValuesByBucket.sorted(by: { $0.0.lowerBound < $1.0.lowerBound }).map { pair -> ModalDayBucket in
return ModalDayBucket(time: pair.key, unorderedValues: pair.value, unit: unit)
}
}
}


fileprivate class ModalDayCalculator {
typealias ResultType = ModalDayBuilder
let calculator: DayCalculator<ResultType>
let bucketSize: TimeInterval
let calendar: Calendar
private let log: OSLog

init(dataManager: DataManager, dates: DateInterval, bucketSize: TimeInterval, unit: HKUnit, calendar: Calendar) {
self.calculator = DayCalculator(dataManager: dataManager, dates: dates, initial: ModalDayBuilder(calendar: calendar, bucketSize: bucketSize, unit: unit))
self.bucketSize = bucketSize
self.calendar = calendar

log = OSLog(category: String(describing: type(of: self)))
}

func execute(completion: @escaping (_ result: Result<[ModalDayBucket]>) -> Void) {
os_log(.default, log: log, "Computing Modal day in %{public}@", String(describing: calculator.dates))

calculator.execute(calculator: { (dataManager, day, mutableResult, completion) in
os_log(.default, log: self.log, "Fetching samples in %{public}@", String(describing: day))

dataManager.glucoseStore.getGlucoseSamples(start: day.start, end: day.end, completion: { (result) in
switch result {
case .failure(let error):
os_log(.error, log: self.log, "Failed to fetch samples: %{public}@", String(describing: error))
completion(error)
case .success(let samples):
os_log(.error, log: self.log, "Found %d samples", samples.count)

for sample in samples {
_ = mutableResult.mutate({ (result) in
result.add(sample.quantity, at: sample.startDate)
})
}
completion(nil)
}
})
}, completion: { (result) in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let builder):
completion(.success(builder.allBuckets))
}
})
}
}
Loading