UnfinalizedDose.swift 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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? = nil, automatic: Bool = false) {
  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? = nil) {
  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, automatic: Bool? = nil) {
  83. self.doseType = .suspend
  84. self.units = 0
  85. self.startTime = suspendStartTime
  86. self.duration = 0
  87. self.insulinType = nil
  88. self.automatic = automatic
  89. }
  90. init(resumeStartTime: Date, insulinType: InsulinType? = nil, automatic: Bool? = nil) {
  91. self.doseType = .resume
  92. self.units = 0
  93. self.startTime = resumeStartTime
  94. self.duration = 0
  95. self.insulinType = insulinType
  96. self.automatic = automatic
  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. public var eventTitle: String {
  141. switch doseType {
  142. case .bolus:
  143. return LocalizedString("Bolus", comment: "Pump Event title for UnfinalizedDose with doseType of .bolus")
  144. case .resume:
  145. return LocalizedString("Resume", comment: "Pump Event title for UnfinalizedDose with doseType of .resume")
  146. case .suspend:
  147. return LocalizedString("Suspend", comment: "Pump Event title for UnfinalizedDose with doseType of .suspend")
  148. case .tempBasal:
  149. return LocalizedString("Temp Basal", comment: "Pump Event title for UnfinalizedDose with doseType of .tempBasal")
  150. }
  151. }
  152. // RawRepresentable
  153. public init?(rawValue: RawValue) {
  154. guard
  155. let rawDoseType = rawValue["doseType"] as? Int,
  156. let doseType = DoseType(rawValue: rawDoseType),
  157. let units = rawValue["units"] as? Double,
  158. let startTime = rawValue["startTime"] as? Date,
  159. let duration = rawValue["duration"] as? Double
  160. else {
  161. return nil
  162. }
  163. self.doseType = doseType
  164. self.units = units
  165. self.startTime = startTime
  166. self.duration = duration
  167. if let scheduledUnits = rawValue["scheduledUnits"] as? Double {
  168. self.scheduledUnits = scheduledUnits
  169. }
  170. if let scheduledTempRate = rawValue["scheduledTempRate"] as? Double {
  171. self.scheduledTempRate = scheduledTempRate
  172. }
  173. if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue, let insulinType = InsulinType(rawValue: rawInsulinType) {
  174. self.insulinType = insulinType
  175. } else {
  176. self.insulinType = nil
  177. }
  178. self.automatic = rawValue["automatic"] as? Bool
  179. }
  180. public var rawValue: RawValue {
  181. var rawValue: RawValue = [
  182. "doseType": doseType.rawValue,
  183. "units": units,
  184. "startTime": startTime,
  185. "duration": duration,
  186. ]
  187. if let scheduledUnits = scheduledUnits {
  188. rawValue["scheduledUnits"] = scheduledUnits
  189. }
  190. if let scheduledTempRate = scheduledTempRate {
  191. rawValue["scheduledTempRate"] = scheduledTempRate
  192. }
  193. if let insulinType = insulinType {
  194. rawValue["insulinType"] = insulinType.rawValue
  195. }
  196. if let automatic = automatic {
  197. rawValue["automatic"] = automatic
  198. }
  199. return rawValue
  200. }
  201. }
  202. extension NewPumpEvent {
  203. init(_ dose: UnfinalizedDose) {
  204. let entry = DoseEntry(dose)
  205. self.init(date: dose.startTime, dose: entry, raw: dose.uniqueKey, title: dose.eventTitle)
  206. }
  207. // Used for TestingScenarios, injecting doses into PumpManager
  208. public var unfinalizedDose: UnfinalizedDose? {
  209. let defaultInsulinType = InsulinType.novolog
  210. if let dose = dose {
  211. let duration = dose.endDate.timeIntervalSince(dose.startDate)
  212. switch dose.type {
  213. case .basal:
  214. return nil
  215. case .bolus:
  216. var newDose = UnfinalizedDose(bolusAmount: dose.programmedUnits, startTime: dose.startDate, duration: duration, insulinType: dose.insulinType ?? defaultInsulinType, automatic: dose.automatic ?? false)
  217. if let delivered = dose.deliveredUnits {
  218. newDose.scheduledUnits = dose.programmedUnits
  219. newDose.units = delivered
  220. }
  221. return newDose
  222. case .resume:
  223. return UnfinalizedDose(resumeStartTime: dose.startDate, insulinType: dose.insulinType ?? defaultInsulinType, automatic: dose.automatic)
  224. case .suspend:
  225. return UnfinalizedDose(suspendStartTime: dose.startDate, automatic: dose.automatic)
  226. case .tempBasal:
  227. return UnfinalizedDose(tempBasalRate: dose.unitsPerHour, startTime: dose.startDate, duration: duration, insulinType: dose.insulinType ?? defaultInsulinType)
  228. }
  229. }
  230. return nil
  231. }
  232. }
  233. extension DoseEntry {
  234. init (_ dose: UnfinalizedDose) {
  235. switch dose.doseType {
  236. case .bolus:
  237. 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)
  238. case .tempBasal:
  239. 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)
  240. case .suspend:
  241. self = DoseEntry(suspendDate: dose.startTime, automatic: dose.automatic)
  242. case .resume:
  243. self = DoseEntry(resumeDate: dose.startTime, insulinType: dose.insulinType, automatic: dose.automatic)
  244. }
  245. }
  246. }