StatsDataService.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. // LoopFollow
  2. // StatsDataService.swift
  3. import Foundation
  4. class StatsDataService {
  5. /// The injected reference can be nil when the stats UI is built before
  6. /// MainViewController.bootstrap() has run (cold launch with Statistics as
  7. /// the selected tab), so resolve lazily and fall back to the shared engine.
  8. private weak var injectedMainViewController: MainViewController?
  9. var mainViewController: MainViewController? {
  10. injectedMainViewController ?? MainViewController.shared
  11. }
  12. var daysToAnalyze: Int = 14 // Keep for backward compatibility
  13. var startDate: Date = dateTimeUtils.displayCalendar().date(byAdding: .day, value: -14, to: Date()) ?? Date()
  14. var endDate: Date = .init()
  15. private let dataFetcher: StatsDataFetcher
  16. /// Stores raw temp basal entries with rate and duration
  17. var tempBasalEntries: [TempBasalEntry] = []
  18. /// Structure to hold temp basal data for calculations
  19. struct TempBasalEntry {
  20. let rate: Double // U/hr (absolute rate)
  21. let startTime: TimeInterval // Unix timestamp
  22. let durationMinutes: Double // Duration in minutes
  23. }
  24. init(mainViewController: MainViewController?) {
  25. injectedMainViewController = mainViewController
  26. dataFetcher = StatsDataFetcher(mainViewController: mainViewController)
  27. dataFetcher.dataService = self
  28. }
  29. /// Update the date range for analysis
  30. func updateDateRange(start: Date, end: Date) {
  31. startDate = start
  32. endDate = end
  33. let calendar = dateTimeUtils.displayCalendar()
  34. let startDay = calendar.startOfDay(for: start)
  35. let endDay = calendar.startOfDay(for: end)
  36. let daysBetween = calendar.dateComponents([.day], from: startDay, to: endDay).day ?? 13
  37. daysToAnalyze = daysBetween + 1
  38. }
  39. func ensureDataAvailable(onProgress: @escaping () -> Void, completion: @escaping () -> Void) {
  40. guard let mainVC = mainViewController else {
  41. completion()
  42. return
  43. }
  44. let cutoffTime = startDate.timeIntervalSince1970
  45. let endTime = endDate.timeIntervalSince1970
  46. let oldestBG = mainVC.statsBGData.filter { $0.date >= cutoffTime && $0.date <= endTime }.min(by: { $0.date < $1.date })?.date
  47. let oldestBolus = mainVC.statsBolusData.filter { $0.date >= cutoffTime && $0.date <= endTime }.min(by: { $0.date < $1.date })?.date
  48. let oldestCarb = mainVC.statsCarbData.filter { $0.date >= cutoffTime && $0.date <= endTime }.min(by: { $0.date < $1.date })?.date
  49. let oldestBasal = mainVC.statsBasalData.filter { $0.date >= cutoffTime && $0.date <= endTime }.min(by: { $0.date < $1.date })?.date
  50. let bgDataCount = mainVC.statsBGData.filter { $0.date >= cutoffTime && $0.date <= endTime }.count
  51. let bolusDataCount = mainVC.statsBolusData.filter { $0.date >= cutoffTime && $0.date <= endTime }.count
  52. let carbDataCount = mainVC.statsCarbData.filter { $0.date >= cutoffTime && $0.date <= endTime }.count
  53. let basalDataCount = mainVC.statsBasalData.filter { $0.date >= cutoffTime && $0.date <= endTime }.count
  54. let minExpectedBGEntries = max(daysToAnalyze * 6, 12)
  55. let hasEnoughBGData = bgDataCount >= minExpectedBGEntries && (oldestBG ?? endTime) <= cutoffTime + (24 * 60 * 60)
  56. let minExpectedTreatmentEntries = max(daysToAnalyze, 1)
  57. let hasEnoughTreatmentData = (bolusDataCount + carbDataCount + basalDataCount) >= minExpectedTreatmentEntries &&
  58. (oldestBolus ?? endTime) <= cutoffTime + (24 * 60 * 60) &&
  59. (oldestCarb ?? endTime) <= cutoffTime + (24 * 60 * 60) &&
  60. (oldestBasal ?? endTime) <= cutoffTime + (24 * 60 * 60)
  61. if !hasEnoughBGData {
  62. dataFetcher.fetchBGData(days: daysToAnalyze) {
  63. DispatchQueue.main.async {
  64. onProgress()
  65. }
  66. if !hasEnoughTreatmentData {
  67. self.dataFetcher.fetchTreatmentsData(days: self.daysToAnalyze) {
  68. DispatchQueue.main.async {
  69. onProgress()
  70. completion()
  71. }
  72. }
  73. } else {
  74. completion()
  75. }
  76. }
  77. } else if !hasEnoughTreatmentData {
  78. dataFetcher.fetchTreatmentsData(days: daysToAnalyze) {
  79. DispatchQueue.main.async {
  80. onProgress()
  81. completion()
  82. }
  83. }
  84. } else {
  85. completion()
  86. }
  87. }
  88. func getBGData() -> [ShareGlucoseData] {
  89. guard let mainVC = mainViewController else { return [] }
  90. let startTime = startDate.timeIntervalSince1970
  91. let endTime = endDate.timeIntervalSince1970
  92. return mainVC.statsBGData.filter { $0.date >= startTime && $0.date <= endTime }
  93. }
  94. func getBolusData() -> [MainViewController.bolusGraphStruct] {
  95. guard let mainVC = mainViewController else { return [] }
  96. let startTime = startDate.timeIntervalSince1970
  97. let endTime = endDate.timeIntervalSince1970
  98. return mainVC.statsBolusData.filter { $0.date >= startTime && $0.date <= endTime }
  99. }
  100. func getSMBData() -> [MainViewController.bolusGraphStruct] {
  101. guard let mainVC = mainViewController else { return [] }
  102. let startTime = startDate.timeIntervalSince1970
  103. let endTime = endDate.timeIntervalSince1970
  104. return mainVC.statsSMBData.filter { $0.date >= startTime && $0.date <= endTime }
  105. }
  106. func getCarbData() -> [MainViewController.carbGraphStruct] {
  107. guard let mainVC = mainViewController else { return [] }
  108. let startTime = startDate.timeIntervalSince1970
  109. let endTime = endDate.timeIntervalSince1970
  110. return mainVC.statsCarbData.filter { $0.date >= startTime && $0.date <= endTime }
  111. }
  112. func getBasalData() -> [MainViewController.basalGraphStruct] {
  113. guard let mainVC = mainViewController else { return [] }
  114. let startTime = startDate.timeIntervalSince1970
  115. let endTime = endDate.timeIntervalSince1970
  116. return mainVC.statsBasalData.filter { $0.date >= startTime && $0.date <= endTime }
  117. }
  118. func getBasalProfile() -> [MainViewController.basalProfileStruct] {
  119. guard let mainVC = mainViewController else { return [] }
  120. return mainVC.basalProfile
  121. }
  122. func getTempBasalData() -> [TempBasalEntry] {
  123. let startTime = startDate.timeIntervalSince1970
  124. let endTime = endDate.timeIntervalSince1970
  125. return tempBasalEntries.filter { $0.startTime >= startTime && $0.startTime <= endTime }
  126. }
  127. /// Calculate data availability for the current date range
  128. func getDataAvailability() -> DataAvailabilityInfo {
  129. let bgData = getBGData()
  130. return DataAvailabilityCalculator.calculateAvailability(
  131. bgData: bgData,
  132. startDate: startDate,
  133. endDate: endDate
  134. )
  135. }
  136. }