UnfinalizedDose.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. //
  2. // UnfinalizedDose.swift
  3. // MinimedKit
  4. //
  5. // Created by Pete Schwamb on 7/31/19.
  6. // Copyright © 2019 Pete Schwamb. All rights reserved.
  7. //
  8. import Foundation
  9. import LoopKit
  10. public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConvertible {
  11. public typealias RawValue = [String: Any]
  12. enum DoseType: Int {
  13. case bolus = 0
  14. case tempBasal
  15. case suspend
  16. case resume
  17. }
  18. let doseType: DoseType
  19. public var units: Double
  20. var programmedUnits: Double? // Set when finalized; tracks programmed units
  21. var programmedTempRate: Double? // Set when finalized; tracks programmed temp rate
  22. let startTime: Date
  23. var duration: TimeInterval
  24. var isReconciledWithHistory: Bool
  25. var uuid: UUID
  26. let insulinType: InsulinType?
  27. let automatic: Bool?
  28. var finishTime: Date {
  29. get {
  30. return startTime.addingTimeInterval(duration)
  31. }
  32. set {
  33. duration = newValue.timeIntervalSince(startTime)
  34. }
  35. }
  36. public var progress: Double {
  37. let elapsed = -startTime.timeIntervalSinceNow
  38. return min(elapsed / duration, 1)
  39. }
  40. public var isFinished: Bool {
  41. return progress >= 1
  42. }
  43. // Units per hour
  44. public var rate: Double {
  45. guard duration.hours > 0 else {
  46. return 0
  47. }
  48. return units / duration.hours
  49. }
  50. public var finalizedUnits: Double? {
  51. guard isFinished else {
  52. return nil
  53. }
  54. return units
  55. }
  56. init(bolusAmount: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool, isReconciledWithHistory: Bool = false) {
  57. self.doseType = .bolus
  58. self.units = bolusAmount
  59. self.startTime = startTime
  60. self.duration = duration
  61. self.programmedUnits = nil
  62. self.insulinType = insulinType
  63. self.uuid = UUID()
  64. self.isReconciledWithHistory = isReconciledWithHistory
  65. self.automatic = automatic
  66. }
  67. init(tempBasalRate: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType?, automatic: Bool = true, isReconciledWithHistory: Bool = false) {
  68. self.doseType = .tempBasal
  69. self.units = tempBasalRate * duration.hours
  70. self.startTime = startTime
  71. self.duration = duration
  72. self.programmedUnits = nil
  73. self.insulinType = insulinType
  74. self.automatic = automatic
  75. self.isReconciledWithHistory = isReconciledWithHistory
  76. self.uuid = UUID()
  77. }
  78. init(suspendStartTime: Date, isReconciledWithHistory: Bool = false) {
  79. self.doseType = .suspend
  80. self.units = 0
  81. self.startTime = suspendStartTime
  82. self.duration = 0
  83. self.isReconciledWithHistory = isReconciledWithHistory
  84. self.insulinType = nil
  85. self.automatic = false
  86. self.uuid = UUID()
  87. }
  88. init(resumeStartTime: Date, insulinType: InsulinType, isReconciledWithHistory: Bool = false) {
  89. self.doseType = .resume
  90. self.units = 0
  91. self.startTime = resumeStartTime
  92. self.duration = 0
  93. self.insulinType = insulinType
  94. self.isReconciledWithHistory = isReconciledWithHistory
  95. self.automatic = false
  96. self.uuid = UUID()
  97. }
  98. public mutating func cancel(at date: Date, pumpModel: PumpModel) {
  99. guard date < finishTime else {
  100. return
  101. }
  102. let programmedUnits = units
  103. self.programmedUnits = programmedUnits
  104. let newDuration = date.timeIntervalSince(startTime)
  105. switch doseType {
  106. case .bolus:
  107. (units,_) = pumpModel.estimateBolusProgress(elapsed: newDuration, programmedUnits: programmedUnits)
  108. case .tempBasal:
  109. programmedTempRate = rate
  110. (units,_) = pumpModel.estimateTempBasalProgress(unitsPerHour: rate, duration: duration, elapsed: newDuration)
  111. default:
  112. break
  113. }
  114. duration = newDuration
  115. }
  116. public var description: String {
  117. switch doseType {
  118. case .bolus:
  119. return "Bolus units:\(programmedUnits ?? units) \(startTime)"
  120. case .tempBasal:
  121. return "TempBasal rate:\(programmedTempRate ?? rate) \(startTime) duration:\(String(describing: duration))"
  122. default:
  123. return "\(String(describing: doseType).capitalized) \(startTime)"
  124. }
  125. }
  126. public var eventTitle: String {
  127. switch doseType {
  128. case .bolus:
  129. return LocalizedString("Bolus", comment: "Pump Event title for UnfinalizedDose with doseType of .bolus")
  130. case .resume:
  131. return LocalizedString("Resume", comment: "Pump Event title for UnfinalizedDose with doseType of .resume")
  132. case .suspend:
  133. return LocalizedString("Suspend", comment: "Pump Event title for UnfinalizedDose with doseType of .suspend")
  134. case .tempBasal:
  135. return LocalizedString("Temp Basal", comment: "Pump Event title for UnfinalizedDose with doseType of .tempBasal")
  136. }
  137. }
  138. public mutating func reconcile(with event: NewPumpEvent) {
  139. isReconciledWithHistory = true
  140. if let dose = event.dose {
  141. switch dose.type {
  142. case .bolus:
  143. if programmedUnits == nil {
  144. programmedUnits = units
  145. }
  146. let doseDuration = dose.endDate.timeIntervalSince(dose.startDate)
  147. if doseDuration > 0 && doseDuration < duration {
  148. duration = doseDuration
  149. }
  150. if let deliveredUnits = dose.deliveredUnits {
  151. units = deliveredUnits
  152. }
  153. default:
  154. break
  155. }
  156. }
  157. }
  158. // MARK: - RawRepresentable
  159. public init?(rawValue: RawValue) {
  160. guard
  161. let rawDoseType = rawValue["doseType"] as? Int,
  162. let doseType = DoseType(rawValue: rawDoseType),
  163. let units = rawValue["units"] as? Double,
  164. let startTime = rawValue["startTime"] as? Date,
  165. let duration = rawValue["duration"] as? Double
  166. else {
  167. return nil
  168. }
  169. self.doseType = doseType
  170. self.units = units
  171. self.startTime = startTime
  172. self.duration = duration
  173. if let scheduledUnits = rawValue["scheduledUnits"] as? Double {
  174. self.programmedUnits = scheduledUnits
  175. }
  176. if let scheduledTempRate = rawValue["scheduledTempRate"] as? Double {
  177. self.programmedTempRate = scheduledTempRate
  178. }
  179. if let uuidString = rawValue["uuid"] as? String {
  180. if let uuid = UUID(uuidString: uuidString) {
  181. self.uuid = uuid
  182. } else {
  183. return nil
  184. }
  185. } else {
  186. self.uuid = UUID()
  187. }
  188. if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) {
  189. self.insulinType = insulinType
  190. } else {
  191. self.insulinType = nil
  192. }
  193. self.isReconciledWithHistory = rawValue["isReconciledWithHistory"] as? Bool ?? false
  194. let defaultAutomaticState = doseType == .tempBasal
  195. self.automatic = rawValue["automatic"] as? Bool ?? defaultAutomaticState
  196. }
  197. public var rawValue: RawValue {
  198. var rawValue: RawValue = [
  199. "doseType": doseType.rawValue,
  200. "units": units,
  201. "startTime": startTime,
  202. "duration": duration,
  203. "isReconciledWithHistory": isReconciledWithHistory,
  204. "uuid": uuid.uuidString,
  205. ]
  206. if let scheduledUnits = programmedUnits {
  207. rawValue["scheduledUnits"] = scheduledUnits
  208. }
  209. if let scheduledTempRate = programmedTempRate {
  210. rawValue["scheduledTempRate"] = scheduledTempRate
  211. }
  212. if let insulinType = insulinType {
  213. rawValue["insulinType"] = insulinType.rawValue
  214. }
  215. if let automatic = automatic {
  216. rawValue["automatic"] = automatic
  217. }
  218. return rawValue
  219. }
  220. }
  221. // MARK: - UnfinalizedDose
  222. extension UnfinalizedDose {
  223. func newPumpEvent(forceFinalization: Bool = false) -> NewPumpEvent {
  224. return NewPumpEvent(self, forceFinalization: forceFinalization)
  225. }
  226. }
  227. // MARK: - NewPumpEvent
  228. extension NewPumpEvent {
  229. init(_ dose: UnfinalizedDose, forceFinalization: Bool = false) {
  230. let entry = DoseEntry(dose, forceFinalization: forceFinalization)
  231. let raw = dose.uuid.asRaw
  232. self.init(date: dose.startTime, dose: entry, raw: raw, title: dose.eventTitle)
  233. }
  234. }
  235. // MARK: - DoseEntry
  236. extension DoseEntry {
  237. init (_ dose: UnfinalizedDose, forceFinalization: Bool = false) {
  238. switch dose.doseType {
  239. case .bolus:
  240. self = DoseEntry(type: .bolus, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedUnits ?? dose.units, unit: .units, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: !dose.isReconciledWithHistory && !forceFinalization)
  241. case .tempBasal:
  242. self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic, isMutable: !dose.isReconciledWithHistory && !forceFinalization)
  243. case .suspend:
  244. self = DoseEntry(suspendDate: dose.startTime, isMutable: !dose.isReconciledWithHistory)
  245. case .resume:
  246. self = DoseEntry(resumeDate: dose.startTime, insulinType: dose.insulinType, isMutable: !dose.isReconciledWithHistory)
  247. }
  248. }
  249. }
  250. extension Collection where Element == NewPumpEvent {
  251. /// find matching entry
  252. func firstMatchingIndex(for dose: UnfinalizedDose, within: TimeInterval) -> Self.Index? {
  253. return firstIndex(where: { (event) -> Bool in
  254. guard let type = event.type, let eventDose = event.dose, abs(eventDose.startDate.timeIntervalSince(dose.startTime)) < within else {
  255. return false
  256. }
  257. switch dose.doseType {
  258. case .bolus:
  259. return type == .bolus && eventDose.programmedUnits == dose.programmedUnits ?? dose.units
  260. case .tempBasal:
  261. return type == .tempBasal && eventDose.unitsPerHour == dose.programmedTempRate ?? dose.rate
  262. case .suspend:
  263. return type == .suspend
  264. case .resume:
  265. return type == .resume
  266. }
  267. })
  268. }
  269. }
  270. extension UUID {
  271. var asRaw: Data {
  272. return withUnsafePointer(to: self) {
  273. Data(bytes: $0, count: MemoryLayout.size(ofValue: self))
  274. }
  275. }
  276. }