TDDSetup.swift 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import CoreData
  2. import Foundation
  3. extension Decimal {
  4. func rounded(scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
  5. var result = Decimal()
  6. var mutableSelf = self
  7. NSDecimalRound(&result, &mutableSelf, scale, roundingMode)
  8. return result
  9. }
  10. }
  11. extension Stat.StateModel {
  12. /// Represents different time ranges for Total Daily Dose calculations
  13. enum TDDTimeRange {
  14. /// Today
  15. case today
  16. /// Yesterday
  17. case yesterday
  18. /// Custom range with specified number of days and end date
  19. case customRange(days: Int, endDate: Date)
  20. /// Calculates the start and end dates for the time range
  21. var dateRange: (start: Date, end: Date) {
  22. let calendar = Calendar.current
  23. let now = Date()
  24. switch self {
  25. case .today:
  26. let startOfToday = calendar.startOfDay(for: now)
  27. return (startOfToday, now)
  28. case .yesterday:
  29. let startOfToday = calendar.startOfDay(for: now)
  30. let startOfYesterday = calendar.date(byAdding: .day, value: -1, to: startOfToday)!
  31. let endOfYesterday = calendar.date(byAdding: .second, value: -1, to: startOfToday)!
  32. return (startOfYesterday, endOfYesterday)
  33. case let .customRange(days, endDate):
  34. let endOfDay = calendar.date(
  35. bySettingHour: 23,
  36. minute: 59,
  37. second: 59,
  38. of: endDate
  39. )!
  40. let startDate = calendar.date(byAdding: .day, value: -(days - 1), to: endOfDay)!
  41. return (startDate, endOfDay)
  42. }
  43. }
  44. }
  45. /// Configuration for TDD display and calculations
  46. struct TDDConfiguration {
  47. /// Number of days to display in the TDD chart (default: 7)
  48. var requestedDays: Int = 7
  49. /// End date for the TDD chart, defaults to end of current day
  50. var endDate: Date = Calendar.current.date(
  51. bySettingHour: 23,
  52. minute: 59,
  53. second: 59,
  54. of: Date()
  55. ) ?? Date()
  56. }
  57. /// Result structure containing TDD calculations for a specific time range
  58. struct TDDResult: Sendable {
  59. /// Array of daily doses for the period
  60. let dailyDoses: [TDD]
  61. /// Average TDD for non-zero values
  62. let average: Decimal
  63. /// Time range for which the result was calculated
  64. let period: TDDTimeRange
  65. /// Total insulin dose for the period
  66. var totalDose: Decimal {
  67. dailyDoses.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
  68. }
  69. }
  70. /// Updates all TDD values concurrently: today, yesterday, and custom range
  71. /// This method fetches and processes TDD data for all time ranges in parallel
  72. /// and updates the UI state with the results.
  73. func updateTDDValues() async {
  74. // Fetch all required TDD ranges
  75. async let today = fetchTDDForRange(.today)
  76. async let yesterday = fetchTDDForRange(.yesterday)
  77. async let customRange = fetchTDDForRange(.customRange(
  78. days: tddConfig.requestedDays,
  79. endDate: tddConfig.endDate
  80. ))
  81. // Await all results
  82. let (todayResult, yesterdayResult, customRangeResult) = await (
  83. today, yesterday, customRange
  84. )
  85. // Update UI state
  86. await MainActor.run {
  87. currentTDD = todayResult.totalDose
  88. ytdTDDValue = yesterdayResult.totalDose
  89. averageTDD = customRangeResult.average
  90. dailyTotalDoses = customRangeResult.dailyDoses
  91. }
  92. }
  93. /// Fetches and processes TDD data for a specific time range
  94. /// - Parameter range: The time range for which to fetch TDD data
  95. /// - Returns: A TDDResult containing processed TDD data for the specified range
  96. private func fetchTDDForRange(_ range: TDDTimeRange) async -> TDDResult {
  97. let dateRange = range.dateRange
  98. let determinationIDs = await fetchDeterminations(
  99. from: dateRange.start,
  100. to: dateRange.end
  101. )
  102. let doses = await processDeterminations(determinationIDs, in: dateRange)
  103. let average = calculateAverage(from: doses)
  104. return TDDResult(
  105. dailyDoses: doses,
  106. average: average,
  107. period: range
  108. )
  109. }
  110. /// Fetches determination object IDs from Core Data for a given date range
  111. /// - Parameters:
  112. /// - startDate: Start date of the range
  113. /// - endDate: End date of the range
  114. /// - Returns: Array of NSManagedObjectIDs for matching determinations
  115. private func fetchDeterminations(from startDate: Date, to endDate: Date) async -> [NSManagedObjectID] {
  116. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  117. ofType: OrefDetermination.self,
  118. onContext: determinationFetchContext,
  119. predicate: NSPredicate.determinationPeriod(from: startDate, to: endDate),
  120. key: "deliverAt",
  121. ascending: false,
  122. propertiesToFetch: ["objectID", "timestamp", "deliverAt", "totalDailyDose"]
  123. )
  124. return await determinationFetchContext.perform {
  125. guard let fetchedResults = results as? [[String: Any]] else { return [] }
  126. return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }
  127. }
  128. }
  129. /// Processes determination objects into TDD records
  130. /// - Parameters:
  131. /// - determinationIDs: Array of determination object IDs to process
  132. /// - dateRange: Date range for context (unused but kept for future use)
  133. /// - Returns: Array of processed TDD records, sorted by date descending
  134. private func processDeterminations(
  135. _ determinationIDs: [NSManagedObjectID],
  136. in _: (start: Date, end: Date)
  137. ) async -> [TDD] {
  138. await determinationFetchContext.perform {
  139. let calendar = Calendar.current
  140. // Convert IDs to OrefDetermination objects
  141. let determinations = determinationIDs.compactMap { id -> OrefDetermination? in
  142. do {
  143. return try self.determinationFetchContext.existingObject(with: id) as? OrefDetermination
  144. } catch {
  145. debugPrint("Error fetching determination: \(error)")
  146. return nil
  147. }
  148. }
  149. // Group by day
  150. let groupedByDay = Dictionary(grouping: determinations) { determination in
  151. calendar.startOfDay(for: determination.timestamp ?? determination.deliverAt ?? Date())
  152. }
  153. // Get latest determination for each day
  154. return groupedByDay.compactMap { _, dayDeterminations in
  155. guard let latestDetermination = dayDeterminations.max(by: {
  156. ($0.timestamp ?? $0.deliverAt ?? Date()) < ($1.timestamp ?? $1.deliverAt ?? Date())
  157. }),
  158. let dose = latestDetermination.totalDailyDose as? Decimal
  159. else { return nil }
  160. return TDD(
  161. totalDailyDose: dose,
  162. timestamp: latestDetermination.deliverAt
  163. )
  164. }.sorted { ($0.timestamp ?? Date()) > ($1.timestamp ?? Date()) }
  165. }
  166. }
  167. /// Calculates the average TDD from an array of TDD records
  168. /// - Parameter tdds: Array of TDD records to average
  169. /// - Returns: Average TDD rounded to 1 decimal place, or 0 if no records
  170. private func calculateAverage(from tdds: [TDD]) -> Decimal {
  171. let totalSum = tdds.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
  172. let count = Decimal(tdds.count)
  173. guard count > 0 else { return 0 }
  174. return (totalSum / count).rounded(scale: 1, roundingMode: .plain)
  175. }
  176. }