Skip to content

Commit 10f21e3

Browse files
committed
Cleanup and add Modal Day lesson
1 parent 3abfc9f commit 10f21e3

File tree

9 files changed

+479
-80
lines changed

9 files changed

+479
-80
lines changed

Learn/AppDelegate.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2626
dataManager.authorize({
2727
DispatchQueue.main.async {
2828
lessonsVC.lessons = [
29-
TimeInRangeLesson(dataManager: dataManager)
29+
TimeInRangeLesson(dataManager: dataManager),
30+
ModalDayLesson(dataManager: dataManager),
3031
]
3132
}
3233
})
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//
2+
// DateIntervalEntry.swift
3+
// Learn
4+
//
5+
// Copyright © 2019 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import UIKit
9+
10+
11+
class DateIntervalEntry: LessonSectionProviding {
12+
let headerTitle: String?
13+
14+
let footerTitle: String?
15+
16+
let dateEntry: DateEntry
17+
let numberEntry: NumberEntry
18+
19+
let cells: [LessonCellProviding]
20+
21+
init(headerTitle: String? = nil, footerTitle: String? = nil, start: Date, weeks: Int) {
22+
self.headerTitle = headerTitle
23+
self.footerTitle = footerTitle
24+
25+
self.dateEntry = DateEntry(date: start, title: NSLocalizedString("Start Date", comment: "Title of config entry"), mode: .date)
26+
self.numberEntry = NumberEntry.integerEntry(value: weeks, unitString: NSLocalizedString("Weeks", comment: "Unit string for a count of calendar weeks"))
27+
28+
self.cells = [
29+
self.dateEntry,
30+
self.numberEntry
31+
]
32+
}
33+
}
34+
35+
extension DateIntervalEntry {
36+
convenience init(headerTitle: String? = nil, footerTitle: String? = nil, end: Date, weeks: Int, calendar: Calendar = .current) {
37+
let start = calendar.date(byAdding: DateComponents(weekOfYear: -weeks), to: end)!
38+
self.init(headerTitle: headerTitle, footerTitle: footerTitle, start: calendar.startOfDay(for: start), weeks: weeks)
39+
}
40+
41+
var dateInterval: DateInterval? {
42+
let start = dateEntry.date
43+
44+
guard let weeks = numberEntry.number?.intValue,
45+
let end = Calendar.current.date(byAdding: DateComponents(weekOfYear: weeks), to: start)
46+
else {
47+
return nil
48+
}
49+
50+
return DateInterval(start: start, end: end)
51+
}
52+
}

Learn/Extensions/DateIntervalFormatter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Foundation
99

1010

1111
extension DateIntervalFormatter {
12-
convenience init(dateStyle: DateIntervalFormatter.Style, timeStyle: DateIntervalFormatter.Style) {
12+
convenience init(dateStyle: DateIntervalFormatter.Style = .none, timeStyle: DateIntervalFormatter.Style = .none) {
1313
self.init()
1414
self.dateStyle = dateStyle
1515
self.timeStyle = timeStyle

Learn/Extensions/OSLog.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// OSLog.swift
3+
// Learn
4+
//
5+
// Copyright © 2019 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import os.log
9+
10+
11+
extension OSLog {
12+
convenience init(category: String) {
13+
self.init(subsystem: "com.loopkit.Learn", category: category)
14+
}
15+
}

Learn/Lessons/ModalDayLesson.swift

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
//
2+
// ModalDayLesson.swift
3+
// Learn
4+
//
5+
// Copyright © 2019 LoopKit Authors. All rights reserved.
6+
//
7+
8+
import Foundation
9+
import HealthKit
10+
import LoopCore
11+
import LoopKit
12+
import os.log
13+
14+
final class ModalDayLesson: Lesson {
15+
let title = NSLocalizedString("Modal Day", comment: "Lesson title")
16+
17+
let subtitle = NSLocalizedString("Visualizes the most frequent glucose values by time of day", comment: "Lesson subtitle")
18+
19+
let configurationSections: [LessonSectionProviding]
20+
21+
private let dataManager: DataManager
22+
23+
private let dateIntervalEntry: DateIntervalEntry
24+
25+
private let glucoseUnit: HKUnit
26+
27+
init(dataManager: DataManager) {
28+
self.dataManager = dataManager
29+
self.glucoseUnit = dataManager.glucoseStore.preferredUnit ?? .milligramsPerDeciliter
30+
31+
dateIntervalEntry = DateIntervalEntry(
32+
end: Date(),
33+
weeks: 2
34+
)
35+
36+
self.configurationSections = [
37+
dateIntervalEntry
38+
]
39+
}
40+
41+
func execute(completion: @escaping ([LessonSectionProviding]) -> Void) {
42+
guard let dates = dateIntervalEntry.dateInterval else {
43+
// TODO: Cleaner error presentation
44+
completion([LessonSection(headerTitle: "Error: Please fill out all fields", footerTitle: nil, cells: [])])
45+
return
46+
}
47+
48+
let calendar = Calendar.current
49+
50+
let calculator = ModalDayCalculator(dataManager: dataManager, dates: dates, bucketSize: .minutes(60), unit: glucoseUnit, calendar: calendar)
51+
calculator.execute { (result) in
52+
switch result {
53+
case .failure(let error):
54+
completion([
55+
LessonSection(cells: [TextCell(text: String(describing: error))])
56+
])
57+
case .success(let buckets):
58+
guard buckets.count > 0 else {
59+
completion([
60+
LessonSection(cells: [TextCell(text: NSLocalizedString("No data available", comment: "Lesson result text for no data"))])
61+
])
62+
return
63+
}
64+
65+
let dateFormatter = DateIntervalFormatter(timeStyle: .short)
66+
let glucoseFormatter = QuantityFormatter()
67+
glucoseFormatter.setPreferredNumberFormatter(for: self.glucoseUnit)
68+
69+
completion([
70+
LessonSection(cells: buckets.compactMap({ (bucket) -> TextCell? in
71+
guard let start = calendar.date(from: bucket.time.lowerBound.dateComponents),
72+
let end = calendar.date(from: bucket.time.upperBound.dateComponents),
73+
let time = dateFormatter.string(from: DateInterval(start: start, end: end)),
74+
let median = bucket.median,
75+
let medianString = glucoseFormatter.string(from: median, for: bucket.unit)
76+
else {
77+
return nil
78+
}
79+
80+
return TextCell(text: time, detailText: medianString)
81+
}))
82+
])
83+
}
84+
}
85+
}
86+
}
87+
88+
89+
fileprivate extension TextCell {
90+
91+
}
92+
93+
94+
fileprivate struct ModalDayBucket {
95+
let time: Range<TimeComponents>
96+
let orderedValues: [Double]
97+
let unit: HKUnit
98+
99+
init(time: Range<TimeComponents>, unorderedValues: [Double], unit: HKUnit) {
100+
self.time = time
101+
self.orderedValues = unorderedValues.sorted()
102+
self.unit = unit
103+
}
104+
105+
var median: HKQuantity? {
106+
let count = orderedValues.count
107+
guard count > 0 else {
108+
return nil
109+
}
110+
111+
if count % 2 == 1 {
112+
return HKQuantity(unit: unit, doubleValue: orderedValues[count / 2])
113+
} else {
114+
let mid = count / 2
115+
let lower = orderedValues[mid - 1]
116+
let upper = orderedValues[mid]
117+
return HKQuantity(unit: unit, doubleValue: (lower + upper) / 2)
118+
}
119+
}
120+
}
121+
122+
123+
fileprivate struct ModalDayBuilder {
124+
let calendar: Calendar
125+
let bucketSize: TimeInterval
126+
let unit: HKUnit
127+
private(set) var unorderedValuesByBucket: [Range<TimeComponents>: [Double]]
128+
129+
init(calendar: Calendar, bucketSize: TimeInterval, unit: HKUnit) {
130+
self.calendar = calendar
131+
self.bucketSize = bucketSize
132+
self.unit = unit
133+
self.unorderedValuesByBucket = [:]
134+
}
135+
136+
mutating func add(_ value: Double, at time: TimeComponents) {
137+
let bucket = time.bucket(withBucketSize: bucketSize)
138+
var values = unorderedValuesByBucket[bucket] ?? []
139+
values.append(value)
140+
unorderedValuesByBucket[bucket] = values
141+
}
142+
143+
mutating func add(_ value: Double, at date: DateComponents) {
144+
guard let time = TimeComponents(dateComponents: date) else {
145+
return
146+
}
147+
add(value, at: time)
148+
}
149+
150+
mutating func add(_ value: Double, at date: Date) {
151+
add(value, at: calendar.dateComponents([.hour, .minute], from: date))
152+
}
153+
154+
mutating func add(_ quantity: HKQuantity, at date: Date) {
155+
add(quantity.doubleValue(for: unit), at: date)
156+
}
157+
158+
var allBuckets: [ModalDayBucket] {
159+
return unorderedValuesByBucket.sorted(by: { $0.0.lowerBound < $1.0.lowerBound }).map { pair -> ModalDayBucket in
160+
return ModalDayBucket(time: pair.key, unorderedValues: pair.value, unit: unit)
161+
}
162+
}
163+
}
164+
165+
166+
fileprivate class ModalDayCalculator {
167+
typealias ResultType = ModalDayBuilder
168+
let calculator: DayCalculator<ResultType>
169+
let bucketSize: TimeInterval
170+
let calendar: Calendar
171+
private let log: OSLog
172+
173+
init(dataManager: DataManager, dates: DateInterval, bucketSize: TimeInterval, unit: HKUnit, calendar: Calendar) {
174+
self.calculator = DayCalculator(dataManager: dataManager, dates: dates, initial: ModalDayBuilder(calendar: calendar, bucketSize: bucketSize, unit: unit))
175+
self.bucketSize = bucketSize
176+
self.calendar = calendar
177+
178+
log = OSLog(category: String(describing: type(of: self)))
179+
}
180+
181+
func execute(completion: @escaping (_ result: Result<[ModalDayBucket]>) -> Void) {
182+
os_log(.default, log: log, "Computing Modal day in %{public}@", String(describing: calculator.dates))
183+
184+
calculator.execute(calculator: { (dataManager, day, mutableResult, completion) in
185+
os_log(.default, log: self.log, "Fetching samples in %{public}@", String(describing: day))
186+
187+
dataManager.glucoseStore.getGlucoseSamples(start: day.start, end: day.end, completion: { (result) in
188+
switch result {
189+
case .failure(let error):
190+
os_log(.error, log: self.log, "Failed to fetch samples: %{public}@", String(describing: error))
191+
completion(error)
192+
case .success(let samples):
193+
os_log(.error, log: self.log, "Found %d samples", samples.count)
194+
195+
for sample in samples {
196+
_ = mutableResult.mutate({ (result) in
197+
result.add(sample.quantity, at: sample.startDate)
198+
})
199+
}
200+
completion(nil)
201+
}
202+
})
203+
}, completion: { (result) in
204+
switch result {
205+
case .failure(let error):
206+
completion(.failure(error))
207+
case .success(let builder):
208+
completion(.success(builder.allBuckets))
209+
}
210+
})
211+
}
212+
}

0 commit comments

Comments
 (0)