LoopChartSetup.swift 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import CoreData
  2. import Foundation
  3. /// Represents statistical data about loop execution success/failure for a specific time period
  4. struct LoopStatsByPeriod: Identifiable {
  5. /// The date representing this time period
  6. let period: Date
  7. /// Number of successful loop executions in this period
  8. let successful: Int
  9. /// Number of failed loop executions in this period
  10. let failed: Int
  11. /// Median duration of loop executions in this period
  12. let medianDuration: Double
  13. /// Number of glucose measurements in this period
  14. let glucoseCount: Int
  15. /// Total number of loop executions in this period
  16. var total: Int { successful + failed }
  17. /// Percentage of successful loops (0-100)
  18. var successPercentage: Double { total > 0 ? Double(successful) / Double(total) * 100 : 0 }
  19. /// Percentage of failed loops (0-100)
  20. var failurePercentage: Double { total > 0 ? Double(failed) / Double(total) * 100 : 0 }
  21. /// Unique identifier for this period, using the period date
  22. var id: Date { period }
  23. }
  24. extension Stat.StateModel {
  25. /// Initiates the process of fetching and processing loop statistics
  26. /// This function coordinates three main tasks:
  27. /// 1. Fetching loop stat record IDs for the selected duration
  28. /// 2. Calculating grouped statistics for the Loop stats chart
  29. /// 3. Updating loop stat records on the main thread (!) for the Loop duration chart
  30. func setupLoopStatRecords() {
  31. Task {
  32. let recordIDs = await self.fetchLoopStatRecords(for: selectedDurationForLoopStats)
  33. // Used for the Loop stats chart (success/failure percentages)
  34. let stats = await calculateLoopStats(from: recordIDs)
  35. // Update property on main thread to avoid data races
  36. await MainActor.run {
  37. self.groupedLoopStats = stats
  38. }
  39. // Used for the Loop duration chart (execution times)
  40. await self.updateLoopStatRecords(from: recordIDs)
  41. }
  42. }
  43. /// Fetches loop stat record IDs from Core Data based on the selected time duration
  44. /// - Parameter duration: The time period to fetch records for (Today, Day, Week, Month, or Total)
  45. /// - Returns: Array of NSManagedObjectIDs for the matching loop stat records
  46. func fetchLoopStatRecords(for duration: Duration) async -> [NSManagedObjectID] {
  47. // Create compound predicate combining duration and non-nil constraints
  48. let predicate: NSCompoundPredicate
  49. let durationPredicate: NSPredicate
  50. let nonNilDurationPredicate = NSPredicate(format: "duration != nil AND duration != 0")
  51. // Set up date-based predicate based on selected duration
  52. switch duration {
  53. case .Day,
  54. .Today:
  55. durationPredicate = NSPredicate(
  56. format: "end >= %@",
  57. Calendar.current.date(
  58. byAdding: .day,
  59. value: -2,
  60. to: Calendar.current.startOfDay(for: Date())
  61. )! as CVarArg
  62. )
  63. case .Week:
  64. durationPredicate = NSPredicate(
  65. format: "end >= %@",
  66. Calendar.current.date(
  67. byAdding: .day,
  68. value: -7,
  69. to: Calendar.current.startOfDay(for: Date())
  70. )! as CVarArg
  71. )
  72. case .Month:
  73. durationPredicate = NSPredicate(
  74. format: "end >= %@",
  75. Calendar.current.date(
  76. byAdding: .month,
  77. value: -1,
  78. to: Calendar.current.startOfDay(for: Date())
  79. )! as CVarArg
  80. )
  81. case .Total:
  82. durationPredicate = NSPredicate(
  83. format: "end >= %@",
  84. Calendar.current.date(
  85. byAdding: .month,
  86. value: -3,
  87. to: Calendar.current.startOfDay(for: Date())
  88. )! as CVarArg
  89. )
  90. }
  91. predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [durationPredicate, nonNilDurationPredicate])
  92. // Fetch records using the constructed predicate
  93. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  94. ofType: LoopStatRecord.self,
  95. onContext: loopTaskContext,
  96. predicate: predicate,
  97. key: "end",
  98. ascending: false,
  99. batchSize: 100
  100. )
  101. return await loopTaskContext.perform {
  102. guard let fetchedResults = results as? [LoopStatRecord] else { return [] }
  103. return fetchedResults.map(\.objectID)
  104. }
  105. }
  106. /// Calculates statistics for loop executions grouped by time periods
  107. /// - Parameter ids: Array of NSManagedObjectIDs for loop stat records
  108. /// - Returns: Array of LoopStatsByPeriod containing success/failure statistics
  109. func calculateLoopStats(from ids: [NSManagedObjectID]) async -> [LoopStatsByPeriod] {
  110. await loopTaskContext.perform {
  111. let calendar = Calendar.current
  112. let now = Date()
  113. // Convert IDs to LoopStatRecord objects
  114. let records = ids.compactMap { id -> LoopStatRecord? in
  115. do {
  116. return try self.loopTaskContext.existingObject(with: id) as? LoopStatRecord
  117. } catch {
  118. debugPrint("\(DebuggingIdentifiers.failed) Error fetching loop stat: \(error)")
  119. return nil
  120. }
  121. }
  122. // Determine start date based on selected duration
  123. let startDate: Date
  124. switch self.selectedDurationForLoopStats {
  125. case .Day,
  126. .Today:
  127. startDate = calendar.date(byAdding: .day, value: -2, to: calendar.startOfDay(for: now))!
  128. case .Week:
  129. startDate = calendar.date(byAdding: .day, value: -7, to: calendar.startOfDay(for: now))!
  130. case .Month:
  131. startDate = calendar.date(byAdding: .month, value: -1, to: calendar.startOfDay(for: now))!
  132. case .Total:
  133. startDate = calendar.date(byAdding: .month, value: -3, to: calendar.startOfDay(for: now))!
  134. }
  135. // Create array of all dates in the range
  136. var dates: [Date] = []
  137. var currentDate = startDate
  138. while currentDate <= now {
  139. dates.append(calendar.startOfDay(for: currentDate))
  140. currentDate = calendar.date(byAdding: .day, value: 1, to: currentDate)!
  141. }
  142. // Group records by day
  143. let recordsByDay = Dictionary(grouping: records) { record in
  144. guard let date = record.start else { return Date() }
  145. return calendar.startOfDay(for: date)
  146. }
  147. // Create stats for each day, including days with no data
  148. return dates.map { date in
  149. let dayRecords = recordsByDay[date, default: []]
  150. let successful = dayRecords.filter { $0.loopStatus?.contains("Success") ?? false }.count
  151. let failed = dayRecords.count - successful
  152. // Calculate glucose count for the period
  153. let glucoseFetchRequest = GlucoseStored.fetchRequest()
  154. let periodStart = date
  155. let periodEnd = calendar.date(byAdding: .day, value: 1, to: date)!
  156. glucoseFetchRequest.predicate = NSPredicate(
  157. format: "date >= %@ AND date < %@",
  158. periodStart as NSDate,
  159. periodEnd as NSDate
  160. )
  161. var glucoseCount = 0
  162. do {
  163. glucoseCount = try self.loopTaskContext.count(for: glucoseFetchRequest)
  164. } catch {
  165. debugPrint("\(DebuggingIdentifiers.failed) Error counting glucose readings: \(error)")
  166. }
  167. return LoopStatsByPeriod(
  168. period: date,
  169. successful: successful,
  170. failed: failed,
  171. medianDuration: BareStatisticsView
  172. .medianCalculationDouble(array: dayRecords.compactMap { $0.duration as Double? }),
  173. glucoseCount: glucoseCount
  174. )
  175. }.sorted { $0.period < $1.period }
  176. }
  177. }
  178. /// Updates the loopStatRecords array on the main thread with records from the provided IDs
  179. /// - Parameter ids: Array of NSManagedObjectIDs for loop stat records
  180. @MainActor func updateLoopStatRecords(from ids: [NSManagedObjectID]) {
  181. loopStatRecords = ids.compactMap { id -> LoopStatRecord? in
  182. do {
  183. return try viewContext.existingObject(with: id) as? LoopStatRecord
  184. } catch {
  185. debugPrint("Error fetching loop stat: \(error)")
  186. return nil
  187. }
  188. }
  189. }
  190. }