TDDSetup.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import CoreData
  2. import Foundation
  3. /// Represents statistical data about Total Daily Dose for a specific time period
  4. struct TDDStats: Identifiable {
  5. let id = UUID()
  6. /// The date representing this time period
  7. let date: Date
  8. /// Total insulin in units
  9. let amount: Double
  10. }
  11. extension Stat.StateModel {
  12. /// Sets up TDD statistics by fetching and processing insulin data
  13. func setupTDDStats() {
  14. Task {
  15. let (hourly, daily) = await fetchTDDStats()
  16. await MainActor.run {
  17. self.hourlyTDDStats = hourly
  18. self.dailyTDDStats = daily
  19. }
  20. // Initially calculate and cache daily averages
  21. await calculateAndCacheTDDAverages()
  22. }
  23. }
  24. /// Fetches and processes Total Daily Dose (TDD) statistics from CoreData
  25. /// - Returns: A tuple containing hourly and daily TDD statistics arrays
  26. /// - Note: Processes both hourly statistics for the last 10 days and complete daily statistics
  27. private func fetchTDDStats() async -> (hourly: [TDDStats], daily: [TDDStats]) {
  28. // Fetch temp basal records from CoreData
  29. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  30. ofType: TempBasalStored.self,
  31. onContext: tddTaskContext,
  32. predicate: NSPredicate.pumpHistoryForStats,
  33. key: "pumpEvent.timestamp",
  34. ascending: true,
  35. batchSize: 100
  36. )
  37. var hourlyStats: [TDDStats] = []
  38. var dailyStats: [TDDStats] = []
  39. await tddTaskContext.perform {
  40. guard let fetchedResults = results as? [TempBasalStored] else {
  41. return
  42. }
  43. let calendar = Calendar.current
  44. // Calculate date range for hourly statistics (last 10 days)
  45. // TODO: - Introduce paging to also be able to show complete history
  46. let now = Date()
  47. let tenDaysAgo = Calendar.current.date(byAdding: .day, value: -10, to: now) ?? now
  48. // Group entries by hour for hourly statistics, filtering for last 10 days only
  49. let hourlyGrouped = Dictionary(grouping: fetchedResults.filter { entry in
  50. guard let date = entry.pumpEvent?.timestamp else { return false }
  51. return date >= tenDaysAgo && date <= now
  52. }) { entry in
  53. // Create date components for hour-level grouping
  54. let components = calendar.dateComponents(
  55. [.year, .month, .day, .hour],
  56. from: entry.pumpEvent?.timestamp ?? Date()
  57. )
  58. return calendar.date(from: components) ?? Date()
  59. }
  60. // Group entries by day for complete daily statistics
  61. let dailyGrouped = Dictionary(grouping: fetchedResults) { entry in
  62. calendar.startOfDay(for: entry.pumpEvent?.timestamp ?? Date())
  63. }
  64. // Process hourly statistics
  65. hourlyStats = hourlyGrouped.keys.sorted().map { timePoint in
  66. let entries = hourlyGrouped[timePoint, default: []]
  67. // Calculate total insulin for each hour
  68. return TDDStats(
  69. date: timePoint,
  70. amount: entries.reduce(0.0) { sum, entry in
  71. sum + (entry.rate?.doubleValue ?? 0) * Double(entry.duration) / 60.0
  72. }
  73. )
  74. }
  75. // Process daily statistics
  76. dailyStats = dailyGrouped.keys.sorted().map { timePoint in
  77. let entries = dailyGrouped[timePoint, default: []]
  78. // Calculate total insulin for each day
  79. return TDDStats(
  80. date: timePoint,
  81. amount: entries.reduce(0.0) { sum, entry in
  82. sum + (entry.rate?.doubleValue ?? 0) * Double(entry.duration) / 60.0
  83. }
  84. )
  85. }
  86. }
  87. return (hourlyStats, dailyStats)
  88. }
  89. /// Calculates and caches the daily averages of Total Daily Dose (TDD) insulin values
  90. /// - Note: This function runs asynchronously and updates the tddAveragesCache on the main actor
  91. private func calculateAndCacheTDDAverages() async {
  92. // Get calendar for date calculations
  93. let calendar = Calendar.current
  94. // Calculate daily averages on background context
  95. let dailyAverages = await tddTaskContext.perform { [dailyTDDStats] in
  96. // Group TDD stats by calendar day
  97. let groupedByDay = Dictionary(grouping: dailyTDDStats) { stat in
  98. calendar.startOfDay(for: stat.date)
  99. }
  100. // Calculate average TDD for each day
  101. var averages: [Date: Double] = [:]
  102. for (day, stats) in groupedByDay {
  103. // Sum up all TDD values for the day
  104. let total = stats.reduce(0.0) { $0 + $1.amount }
  105. let count = Double(stats.count)
  106. // Store average in dictionary
  107. averages[day] = total / count
  108. }
  109. return averages
  110. }
  111. // Update cache on main actor
  112. await MainActor.run {
  113. self.tddAveragesCache = dailyAverages
  114. }
  115. }
  116. /// Gets the cached average Total Daily Dose (TDD) of insulin for a specified date range
  117. /// - Parameter range: A tuple containing the start and end dates to get averages for
  118. /// - Returns: The average TDD in units for the specified date range
  119. func getCachedTDDAverages(for range: (start: Date, end: Date)) -> Double {
  120. // Calculate and return the TDD averages for the given date range using cached values
  121. calculateTDDAveragesForDateRange(from: range.start, to: range.end)
  122. }
  123. /// Calculates the average Total Daily Dose (TDD) of insulin for a specified date range
  124. /// - Parameters:
  125. /// - startDate: The start date of the range to calculate averages for
  126. /// - endDate: The end date of the range to calculate averages for
  127. /// - Returns: The average TDD in units for the specified date range. Returns 0.0 if no data exists.
  128. private func calculateTDDAveragesForDateRange(from startDate: Date, to endDate: Date) -> Double {
  129. // Filter cached TDD values to only include those within the date range
  130. let relevantStats = tddAveragesCache.filter { date, _ in
  131. date >= startDate && date <= endDate
  132. }
  133. // Return 0 if no data exists for the specified range
  134. guard !relevantStats.isEmpty else { return 0.0 }
  135. // Calculate total TDD by summing all values
  136. let total = relevantStats.values.reduce(0.0, +)
  137. // Convert count to Double for floating point division
  138. let count = Double(relevantStats.count)
  139. // Return average TDD
  140. return total / count
  141. }
  142. }