TDDSetup.swift 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import CoreData
  2. import Foundation
  3. extension Stat.StateModel {
  4. /// Initializes and fetches Total Daily Dose (TDD) statistics
  5. ///
  6. /// This function:
  7. /// 1. Fetches TDD determinations from CoreData
  8. /// 2. Maps the determinations to TDD records
  9. /// 3. Updates the tddStats array on the main thread
  10. func setupTDDs() {
  11. Task {
  12. let tddStats = await fetchAndMapDeterminations()
  13. await MainActor.run {
  14. self.tddStats = tddStats
  15. }
  16. }
  17. }
  18. /// Fetches and processes OpenAPS determinations to calculate Total Daily Doses
  19. /// - Returns: Array of TDD records sorted by date
  20. ///
  21. /// This function:
  22. /// 1. Fetches OpenAPS determinations from CoreData
  23. /// 2. Groups determinations by time period (day or hour based on selected duration)
  24. /// 3. Calculates average insulin doses for each time period
  25. ///
  26. /// The grouping logic:
  27. /// - For Day view: Groups by hour to show hourly distribution
  28. /// - For other views: Groups by day to show daily totals
  29. func fetchAndMapDeterminations() async -> [TDD] {
  30. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  31. ofType: OrefDetermination.self,
  32. onContext: determinationFetchContext,
  33. predicate: NSPredicate.determinationsForStats,
  34. key: "deliverAt",
  35. ascending: true,
  36. propertiesToFetch: ["objectID", "timestamp", "deliverAt", "totalDailyDose"]
  37. )
  38. return await determinationFetchContext.perform {
  39. guard let fetchedResults = results as? [[String: Any]] else { return [] }
  40. let calendar = Calendar.current
  41. // Group determinations by day or hour
  42. let groupedByTime = Dictionary(grouping: fetchedResults) { result -> Date in
  43. guard let deliverAt = result["deliverAt"] as? Date else { return Date() }
  44. if self.selectedDurationForInsulinStats == .Day {
  45. // For Day view, group by hour
  46. let components = calendar.dateComponents([.year, .month, .day, .hour], from: deliverAt)
  47. return calendar.date(from: components) ?? Date()
  48. } else {
  49. // For other views, group by day
  50. return calendar.startOfDay(for: deliverAt)
  51. }
  52. }
  53. // Get all unique time points
  54. let timePoints = groupedByTime.keys.sorted()
  55. // Calculate totals for each time point
  56. return timePoints.map { timePoint in
  57. let determinations = groupedByTime[timePoint, default: []]
  58. let totalDose = determinations.reduce(Decimal.zero) { sum, determination in
  59. sum + (determination["totalDailyDose"] as? Decimal ?? 0)
  60. }
  61. // Calculate average dose for the time period
  62. let count = Decimal(determinations.count)
  63. let averageDose = count > 0 ? totalDose / count : 0
  64. return TDD(
  65. totalDailyDose: averageDose,
  66. timestamp: timePoint
  67. )
  68. }
  69. }
  70. }
  71. /// Calculates the average Total Daily Dose for the currently selected time period
  72. ///
  73. /// Time periods and their ranges:
  74. /// - Day: Last 3 days
  75. /// - Week: Last 7 days
  76. /// - Month: Last 30 days
  77. /// - Total: Last 3 months
  78. ///
  79. /// Returns 0 if no TDD records are available for the selected period
  80. var averageTDD: Decimal {
  81. let calendar = Calendar.current
  82. let now = Date()
  83. // Filter TDDs based on selected time frame
  84. let filteredTDDs: [TDD] = tddStats.filter { tdd in
  85. guard let timestamp = tdd.timestamp else { return false }
  86. switch selectedDurationForInsulinStats {
  87. case .Day:
  88. // Last 3 days
  89. let threeDaysAgo = calendar.date(byAdding: .day, value: -3, to: now)!
  90. return timestamp >= threeDaysAgo
  91. case .Week:
  92. // Last week
  93. let weekAgo = calendar.date(byAdding: .weekOfYear, value: -1, to: now)!
  94. return timestamp >= weekAgo
  95. case .Month:
  96. // Last month
  97. let monthAgo = calendar.date(byAdding: .month, value: -1, to: now)!
  98. return timestamp >= monthAgo
  99. case .Total:
  100. // Last 3 months
  101. let threeMonthsAgo = calendar.date(byAdding: .month, value: -3, to: now)!
  102. return timestamp >= threeMonthsAgo
  103. }
  104. }
  105. let sum = filteredTDDs.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
  106. return filteredTDDs.isEmpty ? 0 : sum / Decimal(filteredTDDs.count)
  107. }
  108. /// Calculates the average Total Daily Dose for a specified date range
  109. /// - Parameters:
  110. /// - startDate: Start date of the range
  111. /// - endDate: End date of the range
  112. /// - Returns: Average TDD value for the period
  113. ///
  114. /// The function:
  115. /// 1. Filters TDD records within the specified date range
  116. /// 2. Calculates the sum of all TDDs in the range
  117. /// 3. Returns the average (sum divided by number of records)
  118. /// 4. Returns 0 if no records are found
  119. func calculateAverageTDD(from startDate: Date, to endDate: Date) async -> Decimal {
  120. let filteredTDDs = tddStats.filter { tdd in
  121. guard let timestamp = tdd.timestamp else { return false }
  122. return timestamp >= startDate && timestamp <= endDate
  123. }
  124. let sum = filteredTDDs.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
  125. return filteredTDDs.isEmpty ? 0 : sum / Decimal(filteredTDDs.count)
  126. }
  127. /// Calculates the median Total Daily Dose for a specified date range
  128. /// - Parameters:
  129. /// - startDate: Start date of the range
  130. /// - endDate: End date of the range
  131. /// - Returns: Median TDD value for the period
  132. ///
  133. /// The calculation process:
  134. /// 1. Filters TDD records within the date range
  135. /// 2. Sorts all TDD values
  136. /// 3. For odd number of values: Returns the middle value
  137. /// 4. For even number of values: Returns average of two middle values
  138. /// 5. Returns 0 if no records are found
  139. func calculateMedianTDD(from startDate: Date, to endDate: Date) async -> Decimal {
  140. let filteredTDDs = tddStats.filter { tdd in
  141. guard let timestamp = tdd.timestamp else { return false }
  142. return timestamp >= startDate && timestamp <= endDate
  143. }
  144. let sortedDoses = filteredTDDs.compactMap(\.totalDailyDose).sorted()
  145. guard !sortedDoses.isEmpty else { return 0 }
  146. let middle = sortedDoses.count / 2
  147. if sortedDoses.count % 2 == 0 {
  148. return (sortedDoses[middle - 1] + sortedDoses[middle]) / 2
  149. } else {
  150. return sortedDoses[middle]
  151. }
  152. }
  153. }