LoopChartSetup.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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, failedRecordIDs) = await self.fetchLoopStatRecords(for: selectedDurationForLoopStats)
  33. // Update loop records for duration chart
  34. await self.updateLoopStatRecords(allLoopIds: recordIDs)
  35. // Calculate statistics and update on main thread
  36. let stats = await self.getLoopStats(
  37. allLoopIds: recordIDs,
  38. failedLoopIds: failedRecordIDs,
  39. duration: selectedDurationForLoopStats
  40. )
  41. await MainActor.run {
  42. self.loopStats = stats
  43. }
  44. }
  45. }
  46. /// Fetches loop statistics records for the specified duration
  47. /// - Parameter duration: The time period to fetch records for
  48. /// - Returns: A tuple containing arrays of NSManagedObjectIDs for (all loops, failed loops)
  49. func fetchLoopStatRecords(for duration: Duration) async -> ([NSManagedObjectID], [NSManagedObjectID]) {
  50. // Calculate the date range based on selected duration
  51. let now = Date()
  52. let startDate: Date
  53. switch duration {
  54. case .Day:
  55. startDate = Calendar.current.startOfDay(for: now)
  56. case .Today:
  57. startDate = now.addingTimeInterval(-24.hours.timeInterval)
  58. case .Week:
  59. startDate = now.addingTimeInterval(-7.days.timeInterval)
  60. case .Month:
  61. startDate = now.addingTimeInterval(-30.days.timeInterval)
  62. case .Total:
  63. startDate = now.addingTimeInterval(-90.days.timeInterval)
  64. }
  65. // Perform both fetches asynchronously
  66. async let allLoopsResult = CoreDataStack.shared.fetchEntitiesAsync(
  67. ofType: LoopStatRecord.self,
  68. onContext: loopTaskContext,
  69. predicate: NSPredicate(format: "start > %@", startDate as NSDate),
  70. key: "start",
  71. ascending: false
  72. )
  73. async let failedLoopsResult = CoreDataStack.shared.fetchEntitiesAsync(
  74. ofType: LoopStatRecord.self,
  75. onContext: loopTaskContext,
  76. predicate: NSPredicate(
  77. format: "start > %@ AND loopStatus != %@",
  78. startDate as NSDate,
  79. "Success"
  80. ),
  81. key: "start",
  82. ascending: false
  83. )
  84. // Wait for both results and convert to object IDs
  85. let (allLoops, failedLoops) = await (allLoopsResult, failedLoopsResult)
  86. return (
  87. (allLoops as? [LoopStatRecord] ?? []).map(\.objectID),
  88. (failedLoops as? [LoopStatRecord] ?? []).map(\.objectID)
  89. )
  90. }
  91. /// Updates the loopStatRecords array on the main thread with records from the provided IDs
  92. /// - Parameters:
  93. /// - allLoopIds: Array of NSManagedObjectIDs for all loop records
  94. @MainActor func updateLoopStatRecords(allLoopIds: [NSManagedObjectID]) {
  95. loopStatRecords = allLoopIds.compactMap { id -> LoopStatRecord? in
  96. do {
  97. return try viewContext.existingObject(with: id) as? LoopStatRecord
  98. } catch {
  99. debugPrint("\(DebuggingIdentifiers.failed) Error fetching loop stat: \(error)")
  100. return nil
  101. }
  102. }
  103. }
  104. /// Calculates loop and glucose statistics based on the provided record IDs
  105. /// - Parameters:
  106. /// - allLoopIds: Array of NSManagedObjectIDs for all loop records
  107. /// - failedLoopIds: Array of NSManagedObjectIDs for failed loop records
  108. /// - duration: The time period for statistics calculation
  109. /// - Returns: Array of tuples containing category, count and percentage for each statistic
  110. func getLoopStats(
  111. allLoopIds: [NSManagedObjectID],
  112. failedLoopIds: [NSManagedObjectID],
  113. duration: Duration
  114. ) async -> [(category: String, count: Int, percentage: Double)] {
  115. // Calculate the date range for glucose readings
  116. let now = Date()
  117. let startDate: Date
  118. switch duration {
  119. case .Day:
  120. startDate = Calendar.current.startOfDay(for: now)
  121. case .Today:
  122. startDate = now.addingTimeInterval(-24.hours.timeInterval)
  123. case .Week:
  124. startDate = now.addingTimeInterval(-7.days.timeInterval)
  125. case .Month:
  126. startDate = now.addingTimeInterval(-30.days.timeInterval)
  127. case .Total:
  128. startDate = now.addingTimeInterval(-90.days.timeInterval)
  129. }
  130. // Get glucose statistics
  131. let totalGlucose = await calculateGlucoseStats(from: startDate, to: now)
  132. // Get NSManagedObject
  133. let allLoops = await CoreDataStack.shared.getNSManagedObject(with: allLoopIds, context: loopTaskContext)
  134. let failedLoops = await CoreDataStack.shared.getNSManagedObject(with: failedLoopIds, context: loopTaskContext)
  135. return await loopTaskContext.perform {
  136. let totalLoopsCount = allLoops.count
  137. let failedLoopsCount = failedLoops.count
  138. let successfulLoops = totalLoopsCount - failedLoopsCount
  139. let maxLoopsPerDay = 288.0 // Maximum possible loops per day (every 5 minutes)
  140. switch duration {
  141. case .Day:
  142. // For Day view: Calculate percentage based on maximum possible loops per day
  143. let loopPercentage = (Double(successfulLoops) / maxLoopsPerDay) * 100
  144. let glucosePercentage = (Double(totalGlucose) / maxLoopsPerDay) * 100
  145. return [
  146. ("Loop Success Rate", successfulLoops, loopPercentage),
  147. ("Glucose Count", totalGlucose, glucosePercentage)
  148. ]
  149. case .Month,
  150. .Today,
  151. .Total,
  152. .Week:
  153. // For other views: Calculate average per day
  154. let numberOfDays = max(1, Calendar.current.dateComponents([.day], from: startDate, to: now).day ?? 1)
  155. let averageLoopsPerDay = Double(successfulLoops) / Double(numberOfDays)
  156. let averageGlucosePerDay = Double(totalGlucose) / Double(numberOfDays)
  157. let loopPercentage = (averageLoopsPerDay / maxLoopsPerDay) * 100
  158. let glucosePercentage = (averageGlucosePerDay / maxLoopsPerDay) * 100
  159. return [
  160. ("Successful Loops", Int(round(averageLoopsPerDay)), loopPercentage),
  161. ("Glucose Count", Int(round(averageGlucosePerDay)), glucosePercentage)
  162. ]
  163. }
  164. }
  165. }
  166. /// Fetches and calculates glucose statistics for the given time period
  167. /// - Parameters:
  168. /// - startDate: The start date of the period to analyze
  169. /// - now: The current date (end of period)
  170. /// - Returns: Number of glucose readings in the period
  171. private func calculateGlucoseStats(
  172. from startDate: Date,
  173. to _: Date
  174. ) async -> Int {
  175. // Create predicate for glucose readings
  176. let glucosePredicate = NSPredicate(format: "date >= %@", startDate as NSDate)
  177. // Fetch glucose readings asynchronously
  178. let glucoseResult = await CoreDataStack.shared.fetchEntitiesAsync(
  179. ofType: GlucoseStored.self,
  180. onContext: loopTaskContext,
  181. predicate: glucosePredicate,
  182. key: "date",
  183. ascending: false
  184. )
  185. return await loopTaskContext.perform {
  186. guard let readings = glucoseResult as? [GlucoseStored] else {
  187. return 0
  188. }
  189. return readings.count
  190. }
  191. }
  192. }