UnfinalizedDose.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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. progress(at: Date())
  46. }
  47. public func progress(at date: Date) -> Double {
  48. let elapsed = -startTime.timeIntervalSince(date)
  49. return min(max(elapsed, 0) / duration, 1)
  50. }
  51. public var finished: Bool {
  52. return progress >= 1
  53. }
  54. // Units per hour
  55. public var rate: Double {
  56. guard duration.hours > 0 else {
  57. return 0
  58. }
  59. return units / duration.hours
  60. }
  61. public func isFinished(at date: Date) -> Bool {
  62. return progress(at: date) >= 1
  63. }
  64. public var finalizedUnits: Double? {
  65. guard finished else {
  66. return nil
  67. }
  68. return units
  69. }
  70. init(bolusAmount: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType? = nil, automatic: Bool = false) {
  71. self.doseType = .bolus
  72. self.units = bolusAmount
  73. self.startTime = startTime
  74. self.duration = duration
  75. self.scheduledUnits = nil
  76. self.insulinType = insulinType
  77. self.automatic = automatic
  78. }
  79. init(tempBasalRate: Double, startTime: Date, duration: TimeInterval, insulinType: InsulinType? = nil) {
  80. self.doseType = .tempBasal
  81. self.units = tempBasalRate * duration.hours
  82. self.startTime = startTime
  83. self.duration = duration
  84. self.scheduledUnits = nil
  85. self.insulinType = insulinType
  86. self.automatic = true
  87. }
  88. init(suspendStartTime: Date, automatic: Bool? = nil) {
  89. self.doseType = .suspend
  90. self.units = 0
  91. self.startTime = suspendStartTime
  92. self.duration = 0
  93. self.insulinType = nil
  94. self.automatic = automatic
  95. }
  96. init(resumeStartTime: Date, insulinType: InsulinType? = nil, automatic: Bool? = nil) {
  97. self.doseType = .resume
  98. self.units = 0
  99. self.startTime = resumeStartTime
  100. self.duration = 0
  101. self.insulinType = insulinType
  102. self.automatic = automatic
  103. }
  104. public mutating func cancel(at date: Date) {
  105. guard date < finishTime else {
  106. return
  107. }
  108. scheduledUnits = units
  109. // Guard against negative duration if clock has changed
  110. let newDuration = max(0, date.timeIntervalSince(startTime))
  111. switch doseType {
  112. case .bolus:
  113. units = rate * newDuration.hours
  114. case .tempBasal:
  115. scheduledTempRate = rate
  116. units = floor(rate * newDuration.hours * 20) / 20
  117. default:
  118. break
  119. }
  120. duration = newDuration
  121. }
  122. public var isMutable: Bool {
  123. switch doseType {
  124. case .bolus, .tempBasal:
  125. return !finished
  126. default:
  127. return false
  128. }
  129. }
  130. public var description: String {
  131. let unitsStr = insulinFormatter.string(from: NSNumber(value:units)) ?? "?"
  132. switch doseType {
  133. case .bolus:
  134. if let scheduledUnits = scheduledUnits,
  135. let scheduledUnitsStr = insulinFormatter.string(from: NSNumber(value:scheduledUnits))
  136. {
  137. return "Interrupted Bolus units:\(unitsStr) (\(scheduledUnitsStr) scheduled) startTime:\(startTime) duration:\(String(describing: duration))"
  138. } else {
  139. return "Bolus units:\(unitsStr) startTime:\(startTime) duration:\(String(describing: duration))"
  140. }
  141. case .tempBasal:
  142. return "Temp Basal rate:\(scheduledTempRate ?? rate) units:\(unitsStr) startTime:\(startTime) duration:\(String(describing: duration))"
  143. case .suspend, .resume:
  144. return "\(doseType) startTime:\(startTime)"
  145. }
  146. }
  147. public var eventTitle: String {
  148. switch doseType {
  149. case .bolus:
  150. return LocalizedString("Bolus", comment: "Pump Event title for UnfinalizedDose with doseType of .bolus")
  151. case .resume:
  152. return LocalizedString("Resume", comment: "Pump Event title for UnfinalizedDose with doseType of .resume")
  153. case .suspend:
  154. return LocalizedString("Suspend", comment: "Pump Event title for UnfinalizedDose with doseType of .suspend")
  155. case .tempBasal:
  156. return LocalizedString("Temp Basal", comment: "Pump Event title for UnfinalizedDose with doseType of .tempBasal")
  157. }
  158. }
  159. // RawRepresentable
  160. public init?(rawValue: RawValue) {
  161. guard
  162. let rawDoseType = rawValue["doseType"] as? Int,
  163. let doseType = DoseType(rawValue: rawDoseType),
  164. let units = rawValue["units"] as? Double,
  165. let startTime = rawValue["startTime"] as? Date,
  166. let duration = rawValue["duration"] as? Double
  167. else {
  168. return nil
  169. }
  170. self.doseType = doseType
  171. self.units = units
  172. self.startTime = startTime
  173. self.duration = duration
  174. if let scheduledUnits = rawValue["scheduledUnits"] as? Double {
  175. self.scheduledUnits = scheduledUnits
  176. }
  177. if let scheduledTempRate = rawValue["scheduledTempRate"] as? Double {
  178. self.scheduledTempRate = scheduledTempRate
  179. }
  180. if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) {
  181. self.insulinType = insulinType
  182. } else {
  183. self.insulinType = nil
  184. }
  185. self.automatic = rawValue["automatic"] as? Bool
  186. }
  187. public var rawValue: RawValue {
  188. var rawValue: RawValue = [
  189. "doseType": doseType.rawValue,
  190. "units": units,
  191. "startTime": startTime,
  192. "duration": duration,
  193. ]
  194. if let scheduledUnits = scheduledUnits {
  195. rawValue["scheduledUnits"] = scheduledUnits
  196. }
  197. if let scheduledTempRate = scheduledTempRate {
  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. extension NewPumpEvent {
  210. init(_ dose: UnfinalizedDose) {
  211. let entry = DoseEntry(dose)
  212. self.init(date: dose.startTime, dose: entry, raw: dose.uniqueKey, title: dose.eventTitle)
  213. }
  214. // Used for TestingScenarios, injecting doses into PumpManager
  215. public var unfinalizedDose: UnfinalizedDose? {
  216. let defaultInsulinType = InsulinType.novolog
  217. if let dose = dose {
  218. let duration = dose.endDate.timeIntervalSince(dose.startDate)
  219. switch dose.type {
  220. case .basal:
  221. return nil
  222. case .bolus:
  223. var newDose = UnfinalizedDose(bolusAmount: dose.programmedUnits, startTime: dose.startDate, duration: duration, insulinType: dose.insulinType ?? defaultInsulinType, automatic: dose.automatic ?? false)
  224. if let delivered = dose.deliveredUnits {
  225. newDose.scheduledUnits = dose.programmedUnits
  226. newDose.units = delivered
  227. }
  228. return newDose
  229. case .resume:
  230. return UnfinalizedDose(resumeStartTime: dose.startDate, insulinType: dose.insulinType ?? defaultInsulinType, automatic: dose.automatic)
  231. case .suspend:
  232. return UnfinalizedDose(suspendStartTime: dose.startDate, automatic: dose.automatic)
  233. case .tempBasal:
  234. return UnfinalizedDose(tempBasalRate: dose.unitsPerHour, startTime: dose.startDate, duration: duration, insulinType: dose.insulinType ?? defaultInsulinType)
  235. }
  236. }
  237. return nil
  238. }
  239. }
  240. extension DoseEntry {
  241. init (_ dose: UnfinalizedDose) {
  242. switch dose.doseType {
  243. case .bolus:
  244. 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, isMutable: dose.isMutable)
  245. case .tempBasal:
  246. self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.scheduledTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, isMutable: dose.isMutable)
  247. case .suspend:
  248. self = DoseEntry(suspendDate: dose.startTime, automatic: dose.automatic)
  249. case .resume:
  250. self = DoseEntry(resumeDate: dose.startTime, insulinType: dose.insulinType, automatic: dose.automatic)
  251. }
  252. }
  253. }