UnfinalizedDose.swift 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. //
  2. // UnfinalizedDose.swift
  3. // MockKit
  4. //
  5. // Created by Pete Schwamb on 7/30/19.
  6. // Copyright © 2019 LoopKit Authors. 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. private let dateFormatter = ISO8601DateFormatter()
  19. private let insulinFormatter: NumberFormatter = {
  20. let formatter = NumberFormatter()
  21. formatter.numberStyle = .decimal
  22. formatter.maximumFractionDigits = 3
  23. return formatter
  24. }()
  25. fileprivate var uniqueKey: Data {
  26. return "\(doseType) \(scheduledUnits ?? units) \(dateFormatter.string(from: startTime))".data(using: .utf8)!
  27. }
  28. let doseType: DoseType
  29. public var units: Double
  30. var scheduledUnits: Double? // Tracks the scheduled units, as boluses may be canceled before finishing, at which point units would reflect actual delivered volume.
  31. var scheduledTempRate: Double? // Tracks the original temp rate, as during finalization the units are discretized to pump pulses, changing the actual rate
  32. let startTime: Date
  33. var duration: TimeInterval
  34. let insulinType: InsulinType?
  35. let automatic: Bool?
  36. var finishTime: Date {
  37. get {
  38. return startTime.addingTimeInterval(duration)
  39. }
  40. set {
  41. duration = newValue.timeIntervalSince(startTime)
  42. }
  43. }
  44. public var progress: Double {
  45. let elapsed = -startTime.timeIntervalSinceNow
  46. return min(elapsed / duration, 1)
  47. }
  48. public var finished: Bool {
  49. return progress >= 1
  50. }
  51. // Units per hour
  52. public var rate: Double {
  53. guard duration.hours > 0 else {
  54. return 0
  55. }
  56. return units / duration.hours
  57. }
  58. public var finalizedUnits: Double? {
  59. guard finished else {
  60. return nil
  61. }
  62. return units
  63. }
  64. init(bolusAmount: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType, automatic: Bool) {
  65. self.doseType = .bolus
  66. self.units = bolusAmount
  67. self.startTime = startTime
  68. self.duration = duration
  69. self.scheduledUnits = nil
  70. self.insulinType = insulinType
  71. self.automatic = automatic
  72. }
  73. init(tempBasalRate: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType) {
  74. self.doseType = .tempBasal
  75. self.units = tempBasalRate * duration.hours
  76. self.startTime = startTime
  77. self.duration = duration
  78. self.scheduledUnits = nil
  79. self.insulinType = insulinType
  80. self.automatic = true
  81. }
  82. init(suspendStartTime: Date) {
  83. self.doseType = .suspend
  84. self.units = 0
  85. self.startTime = suspendStartTime
  86. self.duration = 0
  87. self.insulinType = nil
  88. self.automatic = false
  89. }
  90. init(resumeStartTime: Date, insulinType: InsulinType) {
  91. self.doseType = .resume
  92. self.units = 0
  93. self.startTime = resumeStartTime
  94. self.duration = 0
  95. self.insulinType = insulinType
  96. self.automatic = false
  97. }
  98. public mutating func cancel(at date: Date) {
  99. guard date < finishTime else {
  100. return
  101. }
  102. scheduledUnits = units
  103. let newDuration = date.timeIntervalSince(startTime)
  104. switch doseType {
  105. case .bolus:
  106. units = rate * newDuration.hours
  107. case .tempBasal:
  108. scheduledTempRate = rate
  109. units = floor(rate * newDuration.hours * 20) / 20
  110. default:
  111. break
  112. }
  113. duration = newDuration
  114. }
  115. public var isMutable: Bool {
  116. switch doseType {
  117. case .bolus, .tempBasal:
  118. return !finished
  119. default:
  120. return false
  121. }
  122. }
  123. public var description: String {
  124. let unitsStr = insulinFormatter.string(from: NSNumber(value:units)) ?? "?"
  125. switch doseType {
  126. case .bolus:
  127. if let scheduledUnits = scheduledUnits,
  128. let scheduledUnitsStr = insulinFormatter.string(from: NSNumber(value:scheduledUnits))
  129. {
  130. return "Interrupted Bolus units:\(unitsStr) (\(scheduledUnitsStr) scheduled) startTime:\(startTime) duration:\(String(describing: duration))"
  131. } else {
  132. return "Bolus units:\(unitsStr) startTime:\(startTime) duration:\(String(describing: duration))"
  133. }
  134. case .tempBasal:
  135. return "Temp Basal rate:\(scheduledTempRate ?? rate) units:\(unitsStr) startTime:\(startTime) duration:\(String(describing: duration))"
  136. case .suspend, .resume:
  137. return "\(doseType) startTime:\(startTime)"
  138. }
  139. }
  140. // RawRepresentable
  141. public init?(rawValue: RawValue) {
  142. guard
  143. let rawDoseType = rawValue["doseType"] as? Int,
  144. let doseType = DoseType(rawValue: rawDoseType),
  145. let units = rawValue["units"] as? Double,
  146. let startTime = rawValue["startTime"] as? Date,
  147. let duration = rawValue["duration"] as? Double
  148. else {
  149. return nil
  150. }
  151. self.doseType = doseType
  152. self.units = units
  153. self.startTime = startTime
  154. self.duration = duration
  155. if let scheduledUnits = rawValue["scheduledUnits"] as? Double {
  156. self.scheduledUnits = scheduledUnits
  157. }
  158. if let scheduledTempRate = rawValue["scheduledTempRate"] as? Double {
  159. self.scheduledTempRate = scheduledTempRate
  160. }
  161. if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) {
  162. self.insulinType = insulinType
  163. } else {
  164. self.insulinType = nil
  165. }
  166. self.automatic = rawValue["automatic"] as? Bool
  167. }
  168. public var rawValue: RawValue {
  169. var rawValue: RawValue = [
  170. "doseType": doseType.rawValue,
  171. "units": units,
  172. "startTime": startTime,
  173. "duration": duration,
  174. ]
  175. if let scheduledUnits = scheduledUnits {
  176. rawValue["scheduledUnits"] = scheduledUnits
  177. }
  178. if let scheduledTempRate = scheduledTempRate {
  179. rawValue["scheduledTempRate"] = scheduledTempRate
  180. }
  181. if let insulinType = insulinType {
  182. rawValue["insulinType"] = insulinType.rawValue
  183. }
  184. if let automatic = automatic {
  185. rawValue["automatic"] = automatic
  186. }
  187. return rawValue
  188. }
  189. }
  190. extension NewPumpEvent {
  191. init(_ dose: UnfinalizedDose) {
  192. let title = String(describing: dose)
  193. let entry = DoseEntry(dose)
  194. self.init(date: dose.startTime, dose: entry, isMutable: dose.isMutable, raw: dose.uniqueKey, title: title)
  195. }
  196. // Used for TestingScenarios, injecting doses into PumpManager
  197. public var unfinalizedDose: UnfinalizedDose? {
  198. let defaultInsulinType = InsulinType.novolog
  199. if let dose = dose {
  200. let duration = dose.endDate.timeIntervalSince(dose.startDate)
  201. switch dose.type {
  202. case .basal:
  203. return nil
  204. case .bolus:
  205. var newDose = UnfinalizedDose(bolusAmount: dose.programmedUnits, startTime: dose.startDate, duration: duration, insulinType: dose.insulinType ?? defaultInsulinType, automatic: dose.automatic ?? false)
  206. if let delivered = dose.deliveredUnits {
  207. newDose.scheduledUnits = dose.programmedUnits
  208. newDose.units = delivered
  209. }
  210. return newDose
  211. case .resume:
  212. return UnfinalizedDose(resumeStartTime: dose.startDate, insulinType: dose.insulinType ?? defaultInsulinType)
  213. case .suspend:
  214. return UnfinalizedDose(suspendStartTime: dose.startDate)
  215. case .tempBasal:
  216. return UnfinalizedDose(tempBasalRate: dose.unitsPerHour, startTime: dose.startDate, duration: duration, insulinType: dose.insulinType ?? defaultInsulinType)
  217. }
  218. }
  219. return nil
  220. }
  221. }
  222. extension DoseEntry {
  223. init (_ dose: UnfinalizedDose) {
  224. switch dose.doseType {
  225. case .bolus:
  226. self = DoseEntry(type: .bolus, startDate: dose.startTime, endDate: dose.finishTime, value: dose.scheduledUnits ?? dose.units, unit: .units, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic)
  227. case .tempBasal:
  228. self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.scheduledTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType)
  229. case .suspend:
  230. self = DoseEntry(suspendDate: dose.startTime)
  231. case .resume:
  232. self = DoseEntry(resumeDate: dose.startTime, insulinType: dose.insulinType)
  233. }
  234. }
  235. }