UnfinalizedDose.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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 mutating func reconcile(with event: NewPumpEvent) {
  127. isReconciledWithHistory = true
  128. if let dose = event.dose {
  129. switch dose.type {
  130. case .bolus:
  131. if programmedUnits == nil {
  132. programmedUnits = units
  133. }
  134. let doseDuration = dose.endDate.timeIntervalSince(dose.startDate)
  135. if doseDuration > 0 && doseDuration < duration {
  136. duration = doseDuration
  137. }
  138. if let deliveredUnits = dose.deliveredUnits {
  139. units = deliveredUnits
  140. }
  141. default:
  142. break
  143. }
  144. }
  145. }
  146. // MARK: - RawRepresentable
  147. public init?(rawValue: RawValue) {
  148. guard
  149. let rawDoseType = rawValue["doseType"] as? Int,
  150. let doseType = DoseType(rawValue: rawDoseType),
  151. let units = rawValue["units"] as? Double,
  152. let startTime = rawValue["startTime"] as? Date,
  153. let duration = rawValue["duration"] as? Double
  154. else {
  155. return nil
  156. }
  157. self.doseType = doseType
  158. self.units = units
  159. self.startTime = startTime
  160. self.duration = duration
  161. if let scheduledUnits = rawValue["scheduledUnits"] as? Double {
  162. self.programmedUnits = scheduledUnits
  163. }
  164. if let scheduledTempRate = rawValue["scheduledTempRate"] as? Double {
  165. self.programmedTempRate = scheduledTempRate
  166. }
  167. if let uuidString = rawValue["uuid"] as? String {
  168. if let uuid = UUID(uuidString: uuidString) {
  169. self.uuid = uuid
  170. } else {
  171. return nil
  172. }
  173. } else {
  174. self.uuid = UUID()
  175. }
  176. if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) {
  177. self.insulinType = insulinType
  178. } else {
  179. self.insulinType = nil
  180. }
  181. self.isReconciledWithHistory = rawValue["isReconciledWithHistory"] as? Bool ?? false
  182. let defaultAutomaticState = doseType == .tempBasal
  183. self.automatic = rawValue["automatic"] as? Bool ?? defaultAutomaticState
  184. }
  185. public var rawValue: RawValue {
  186. var rawValue: RawValue = [
  187. "doseType": doseType.rawValue,
  188. "units": units,
  189. "startTime": startTime,
  190. "duration": duration,
  191. "isReconciledWithHistory": isReconciledWithHistory,
  192. "uuid": uuid.uuidString,
  193. ]
  194. if let scheduledUnits = programmedUnits {
  195. rawValue["scheduledUnits"] = scheduledUnits
  196. }
  197. if let scheduledTempRate = programmedTempRate {
  198. rawValue["scheduledTempRate"] = scheduledTempRate
  199. }
  200. if let insulinType = insulinType {
  201. rawValue["insulinType"] = insulinType.rawValue
  202. }
  203. if let automatic = automatic {
  204. rawValue["automatic"] = automatic
  205. }
  206. return rawValue
  207. }
  208. }
  209. // MARK: - UnfinalizedDose
  210. extension UnfinalizedDose {
  211. var newPumpEvent: NewPumpEvent {
  212. return NewPumpEvent(self)
  213. }
  214. }
  215. // MARK: - NewPumpEvent
  216. extension NewPumpEvent {
  217. init(_ dose: UnfinalizedDose) {
  218. let title = String(describing: dose)
  219. let entry = DoseEntry(dose)
  220. let raw = dose.uuid.asRaw
  221. self.init(date: dose.startTime, dose: entry, isMutable: !dose.isFinished || !dose.isReconciledWithHistory, raw: raw, title: title)
  222. }
  223. func replacingAttributes(raw newRaw: Data, date newDate: Date) -> NewPumpEvent {
  224. let newDose = dose?.replacingAttributes(startDate: newDate)
  225. return NewPumpEvent(date: newDate, dose: newDose, isMutable: isMutable, raw: newRaw, title: title, type: type)
  226. }
  227. }
  228. // MARK: - DoseEntry
  229. extension DoseEntry {
  230. init (_ dose: UnfinalizedDose) {
  231. switch dose.doseType {
  232. case .bolus:
  233. 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)
  234. case .tempBasal:
  235. self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.programmedTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType)
  236. case .suspend:
  237. self = DoseEntry(suspendDate: dose.startTime)
  238. case .resume:
  239. self = DoseEntry(resumeDate: dose.startTime, insulinType: dose.insulinType)
  240. }
  241. }
  242. func replacingAttributes(startDate newStartDate: Date) -> DoseEntry {
  243. let value: Double
  244. switch unit {
  245. case .units:
  246. value = programmedUnits
  247. case .unitsPerHour:
  248. value = unitsPerHour
  249. }
  250. let duration = endDate.timeIntervalSince(startDate)
  251. let newEndDate = newStartDate.addingTimeInterval(duration)
  252. return DoseEntry(type: type, startDate: newStartDate, endDate: newEndDate, value: value, unit: unit, deliveredUnits: deliveredUnits, description: description, syncIdentifier: syncIdentifier, insulinType: insulinType)
  253. }
  254. }
  255. extension Collection where Element == NewPumpEvent {
  256. /// find matching entry
  257. func firstMatchingIndex(for dose: UnfinalizedDose, within: TimeInterval) -> Self.Index? {
  258. return firstIndex(where: { (event) -> Bool in
  259. guard let type = event.type, let eventDose = event.dose, abs(eventDose.startDate.timeIntervalSince(dose.startTime)) < within else {
  260. return false
  261. }
  262. switch dose.doseType {
  263. case .bolus:
  264. return type == .bolus && eventDose.programmedUnits == dose.programmedUnits ?? dose.units
  265. case .tempBasal:
  266. return type == .tempBasal && eventDose.unitsPerHour == dose.programmedTempRate ?? dose.rate
  267. case .suspend:
  268. return type == .suspend
  269. case .resume:
  270. return type == .resume
  271. }
  272. })
  273. }
  274. }
  275. extension UUID {
  276. var asRaw: Data {
  277. return withUnsafePointer(to: self) {
  278. Data(bytes: $0, count: MemoryLayout.size(ofValue: self))
  279. }
  280. }
  281. }