TDDStorage.swift 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import Foundation
  2. import Swinject
  3. protocol TDDStorage {
  4. func calculateTDD(pumpHistory: [PumpHistoryEvent], basalProfile: [BasalProfileEntry]) async -> TDDResult
  5. }
  6. /// Structure containing the results of TDD calculations
  7. struct TDDResult {
  8. let total: Decimal
  9. let bolus: Decimal
  10. let tempBasal: Decimal
  11. let scheduledBasal: Decimal
  12. let weightedAverage: Decimal?
  13. let hoursOfData: Double
  14. }
  15. /// Implementation of the TDD Calculator
  16. final class BaseTDDStorage: TDDStorage, Injectable {
  17. init(resolver: Resolver) {
  18. injectServices(resolver)
  19. }
  20. /// Main function to calculate TDD from pump history and basal profile
  21. /// - Parameters:
  22. /// - pumpHistory: Array of pump history events
  23. /// - basalProfile: Array of basal profile entries
  24. /// - Returns: TDDResult containing all calculated values
  25. func calculateTDD(pumpHistory: [PumpHistoryEvent], basalProfile: [BasalProfileEntry]) async -> TDDResult {
  26. debug(.apsManager, "Starting TDD calculation with \(pumpHistory.count) pump events")
  27. var bolusInsulin: Decimal = 0
  28. var tempInsulin: Decimal = 0
  29. var scheduledBasalInsulin: Decimal = 0
  30. let pumpData = calculatePumpDataHours(pumpHistory)
  31. debug(.apsManager, "Hours of pump data available: \(pumpData)")
  32. if pumpData < 23.9, pumpData > 21 {
  33. let missingHours = 24 - pumpData
  34. debug(.apsManager, "Filling \(missingHours) missing hours with scheduled basals")
  35. if let lastEntry = pumpHistory.last {
  36. let endDate = lastEntry.timestamp
  37. let endDateAdjusted = endDate.addingTimeInterval(-missingHours * 3600)
  38. scheduledBasalInsulin = calculateScheduledBasalInsulin(
  39. from: endDate,
  40. to: endDateAdjusted,
  41. basalProfile: basalProfile
  42. )
  43. debug(.apsManager, "Added scheduled basal insulin: \(scheduledBasalInsulin)U")
  44. }
  45. }
  46. bolusInsulin = calculateBolusInsulin(pumpHistory)
  47. debug(.apsManager, "Total bolus insulin: \(bolusInsulin)U")
  48. tempInsulin = calculateTempBasalInsulin(pumpHistory)
  49. debug(.apsManager, "Total temp basal insulin: \(tempInsulin)U")
  50. let total = bolusInsulin + tempInsulin + scheduledBasalInsulin
  51. let weightedAverage = calculateWeightedAverage()
  52. debug(.apsManager, """
  53. TDD Summary:
  54. - Total: \(total)U
  55. - Bolus: \(bolusInsulin)U (\((bolusInsulin / total * 100).rounded(toPlaces: 1))%)
  56. - Temp Basal: \(tempInsulin)U (\((tempInsulin / total * 100).rounded(toPlaces: 1))%)
  57. - Scheduled Basal: \(scheduledBasalInsulin)U (\((scheduledBasalInsulin / total * 100).rounded(toPlaces: 1))%)
  58. - weightedAverage: \(weightedAverage ?? 0)
  59. - Hours of Data: \(pumpData)
  60. """)
  61. return TDDResult(
  62. total: total,
  63. bolus: bolusInsulin,
  64. tempBasal: tempInsulin,
  65. scheduledBasal: scheduledBasalInsulin,
  66. weightedAverage: weightedAverage,
  67. hoursOfData: pumpData
  68. )
  69. }
  70. /// Calculates the number of hours of available pump history data
  71. /// - Parameter pumpHistory: Array of pump history events
  72. /// - Returns: Number of hours of available data
  73. private func calculatePumpDataHours(_ pumpHistory: [PumpHistoryEvent]) -> Double {
  74. guard let firstEvent = pumpHistory.last, // we are fetching in a descending order
  75. let lastEvent = pumpHistory.first
  76. else {
  77. return 0
  78. }
  79. let startDate = firstEvent.timestamp
  80. var endDate = lastEvent.timestamp
  81. // If last event is a temp basal, use current time
  82. if lastEvent.type == .tempBasalDuration {
  83. endDate = Date()
  84. }
  85. return Double(endDate.timeIntervalSince(startDate)) / 3600.0
  86. }
  87. /// Calculates total bolus insulin from pump history
  88. /// - Parameter pumpHistory: Array of pump history events
  89. /// - Returns: Total bolus insulin
  90. private func calculateBolusInsulin(_ pumpHistory: [PumpHistoryEvent]) -> Decimal {
  91. pumpHistory
  92. .filter { $0.type == .bolus }
  93. .reduce(Decimal(0)) { sum, event in
  94. sum + (event.amount ?? 0)
  95. }
  96. }
  97. /// Calculates insulin delivered via temporary basal rates
  98. /// - Parameter pumpHistory: Array of pump history events
  99. /// - Returns: Total temporary basal insulin
  100. private func calculateTempBasalInsulin(_ pumpHistory: [PumpHistoryEvent]) -> Decimal {
  101. var totalInsulin: Decimal = 0
  102. for (index, event) in pumpHistory.enumerated() {
  103. guard event.type == .tempBasal,
  104. let rate = event.amount,
  105. rate > 0,
  106. index > 0 else { continue }
  107. let duration = Decimal(pumpHistory[index - 1].duration ?? 0) / 60 // Convert to hours
  108. let insulin = accountForIncrements(rate * duration)
  109. totalInsulin += insulin
  110. debug(.apsManager, "Temp basal: \(rate)U/hr for \(duration)hr = \(insulin)U")
  111. }
  112. return totalInsulin
  113. }
  114. /// Calculates insulin delivered via scheduled basal rates
  115. /// - Parameters:
  116. /// - from: Start date
  117. /// - to: End date
  118. /// - basalProfile: Array of basal profile entries
  119. /// - Returns: Total scheduled basal insulin
  120. private func calculateScheduledBasalInsulin(from: Date, to: Date, basalProfile: [BasalProfileEntry]) -> Decimal {
  121. var totalInsulin: Decimal = 0
  122. var currentDate = from
  123. while currentDate < to {
  124. let timeString = makeBaseString(from: currentDate)
  125. guard let basalRate = findBasalRate(for: timeString, in: basalProfile) else { continue }
  126. let nextScheduleTime = findNextScheduleTime(after: timeString, in: basalProfile)
  127. let duration = calculateDuration(currentTime: timeString, nextScheduleTime: nextScheduleTime, endDate: to)
  128. let insulin = accountForIncrements(basalRate * Decimal(duration))
  129. totalInsulin += insulin
  130. currentDate = currentDate.addingTimeInterval(duration * 3600)
  131. }
  132. return totalInsulin
  133. }
  134. /// Rounds insulin amounts according to pump increment constraints
  135. /// - Parameter insulin: Raw insulin amount
  136. /// - Returns: Rounded insulin amount
  137. private func accountForIncrements(_ insulin: Decimal) -> Decimal {
  138. let minimalDose: Decimal = 0.05 // For Omnipod, 0.1 for other pumps
  139. let incrementsRaw = insulin / minimalDose
  140. if incrementsRaw >= 1 {
  141. // Convert to NSDecimalNumber to use its rounding capabilities
  142. let nsIncrements = NSDecimalNumber(decimal: incrementsRaw)
  143. let roundedIncrements = nsIncrements.rounding(
  144. accordingToBehavior:
  145. NSDecimalNumberHandler(
  146. roundingMode: .down,
  147. scale: 0,
  148. raiseOnExactness: false,
  149. raiseOnOverflow: false,
  150. raiseOnUnderflow: false,
  151. raiseOnDivideByZero: false
  152. )
  153. )
  154. return (roundedIncrements.decimalValue * minimalDose).rounded(toPlaces: 3)
  155. }
  156. return 0
  157. }
  158. /// Formats a date to time string in "HH:mm:ss" format
  159. /// - Parameter date: Date to format
  160. /// - Returns: Formatted time string
  161. private func makeBaseString(from date: Date) -> String {
  162. let formatter = DateFormatter()
  163. formatter.dateFormat = "HH:mm:ss"
  164. return formatter.string(from: date)
  165. }
  166. /// Finds the basal rate for a specific time in the profile
  167. /// - Parameters:
  168. /// - timeString: Time string in "HH:mm:ss" format
  169. /// - profile: Array of basal profile entries
  170. /// - Returns: Basal rate if found
  171. private func findBasalRate(for timeString: String, in profile: [BasalProfileEntry]) -> Decimal? {
  172. profile.first { $0.start == timeString }?.rate
  173. }
  174. /// Finds the next scheduled time in the basal profile
  175. /// - Parameters:
  176. /// - time: Current time string
  177. /// - profile: Array of basal profile entries
  178. /// - Returns: Next scheduled time
  179. private func findNextScheduleTime(after time: String, in profile: [BasalProfileEntry]) -> String {
  180. guard let currentIndex = profile.firstIndex(where: { $0.start == time }) else {
  181. return profile[0].start
  182. }
  183. let nextIndex = (currentIndex + 1) % profile.count
  184. return profile[nextIndex].start
  185. }
  186. /// Calculates duration between two schedule times
  187. /// - Parameters:
  188. /// - currentTime: Current time string
  189. /// - nextScheduleTime: Next schedule time string
  190. /// - endDate: End date for calculations
  191. /// - Returns: Duration in hours
  192. private func calculateDuration(currentTime: String, nextScheduleTime: String, endDate _: Date) -> Double {
  193. let formatter = DateFormatter()
  194. formatter.dateFormat = "HH:mm:ss"
  195. guard let time1 = formatter.date(from: currentTime),
  196. let time2 = formatter.date(from: nextScheduleTime)
  197. else {
  198. return 0
  199. }
  200. var difference = time2.timeIntervalSince(time1) / 3600
  201. if difference < 0 {
  202. difference += 24
  203. }
  204. return difference
  205. }
  206. /// Calculates weighted average of TDD from historical data
  207. /// - Returns: Weighted average if available
  208. private func calculateWeightedAverage() -> Decimal? {
  209. // Implementation of weighted average calculation
  210. // Would use historical TDD data from Core Data
  211. nil
  212. }
  213. }
  214. /// Extension for rounding Decimal numbers
  215. extension Decimal {
  216. /// Rounds a decimal to specified number of places
  217. /// - Parameter places: Number of decimal places
  218. /// - Returns: Rounded decimal
  219. func rounded(toPlaces places: Int) -> Decimal {
  220. var value = self
  221. var result = Decimal()
  222. NSDecimalRound(&result, &value, places, .plain)
  223. return result
  224. }
  225. }