UnfinalizedDose.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. //
  2. // UnfinalizedDose.swift
  3. // OmniKit
  4. //
  5. // Created by Pete Schwamb on 9/5/18.
  6. // Copyright © 2018 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. enum ScheduledCertainty: Int {
  19. case certain = 0
  20. case uncertain
  21. public var localizedDescription: String {
  22. switch self {
  23. case .certain:
  24. return LocalizedString("Certain", comment: "String describing a dose that was certainly scheduled")
  25. case .uncertain:
  26. return LocalizedString("Uncertain", comment: "String describing a dose that was possibly scheduled")
  27. }
  28. }
  29. }
  30. private let insulinFormatter: NumberFormatter = {
  31. let formatter = NumberFormatter()
  32. formatter.numberStyle = .decimal
  33. formatter.maximumFractionDigits = 3
  34. return formatter
  35. }()
  36. private let shortDateFormatter: DateFormatter = {
  37. let timeFormatter = DateFormatter()
  38. timeFormatter.dateStyle = .short
  39. timeFormatter.timeStyle = .medium
  40. return timeFormatter
  41. }()
  42. private let dateFormatter = ISO8601DateFormatter()
  43. fileprivate var uniqueKey: Data {
  44. return "\(doseType) \(scheduledUnits ?? units) \(dateFormatter.string(from: startTime))".data(using: .utf8)!
  45. }
  46. let doseType: DoseType
  47. public var units: Double
  48. var scheduledUnits: Double? // Tracks the scheduled units, as boluses may be canceled before finishing, at which point units would reflect actual delivered volume.
  49. var scheduledTempRate: Double? // Tracks the original temp rate, as during finalization the units are discretized to pump pulses, changing the actual rate
  50. let startTime: Date
  51. var duration: TimeInterval?
  52. var scheduledCertainty: ScheduledCertainty
  53. var insulinType: InsulinType?
  54. var automatic: Bool?
  55. var finishTime: Date? {
  56. get {
  57. return duration != nil ? startTime.addingTimeInterval(duration!) : nil
  58. }
  59. set {
  60. duration = newValue?.timeIntervalSince(startTime)
  61. }
  62. }
  63. private var nominalProgress: Double {
  64. guard let duration = duration else {
  65. return 0
  66. }
  67. let elapsed = -startTime.timeIntervalSinceNow
  68. return elapsed / duration
  69. }
  70. // A value from 0 to 1 giving the nominal progress percentage for a bolus or a temp basal
  71. public var progress: Double {
  72. return min(nominalProgress, 1)
  73. }
  74. // Is a bolus or a temp basal nominally finished
  75. public var isFinished: Bool {
  76. return progress >= 1
  77. }
  78. // Has a bolus operation had enough time to positively finish
  79. public var isBolusPositivelyFinished: Bool {
  80. // Pod faults if any pulse takes 20% or more of nominal to deliver
  81. return nominalProgress >= 1.2
  82. }
  83. // Units per hour
  84. public var rate: Double {
  85. guard let duration = duration else {
  86. return 0
  87. }
  88. return units / duration.hours
  89. }
  90. public var finalizedUnits: Double? {
  91. guard isFinished else {
  92. return nil
  93. }
  94. return units
  95. }
  96. init(bolusAmount: Double, startTime: Date, scheduledCertainty: ScheduledCertainty, insulinType: InsulinType, automatic: Bool?) {
  97. self.doseType = .bolus
  98. self.units = bolusAmount
  99. self.startTime = startTime
  100. self.duration = TimeInterval(bolusAmount / Pod.bolusDeliveryRate)
  101. self.scheduledCertainty = scheduledCertainty
  102. self.scheduledUnits = nil
  103. self.insulinType = insulinType
  104. self.automatic = automatic
  105. }
  106. init(tempBasalRate: Double, startTime: Date, duration: TimeInterval, scheduledCertainty: ScheduledCertainty, insulinType: InsulinType) {
  107. self.doseType = .tempBasal
  108. self.units = tempBasalRate * duration.hours
  109. self.startTime = startTime
  110. self.duration = duration
  111. self.scheduledCertainty = scheduledCertainty
  112. self.scheduledUnits = nil
  113. self.insulinType = insulinType
  114. self.automatic = nil
  115. }
  116. init(suspendStartTime: Date, scheduledCertainty: ScheduledCertainty) {
  117. self.doseType = .suspend
  118. self.units = 0
  119. self.startTime = suspendStartTime
  120. self.scheduledCertainty = scheduledCertainty
  121. self.automatic = nil
  122. }
  123. init(resumeStartTime: Date, scheduledCertainty: ScheduledCertainty, insulinType: InsulinType) {
  124. self.doseType = .resume
  125. self.units = 0
  126. self.startTime = resumeStartTime
  127. self.scheduledCertainty = scheduledCertainty
  128. self.insulinType = insulinType
  129. self.automatic = nil
  130. }
  131. public mutating func cancel(at date: Date, withRemaining remaining: Double? = nil) {
  132. guard let finishTime = finishTime, date < finishTime else {
  133. return
  134. }
  135. scheduledUnits = units
  136. let newDuration = date.timeIntervalSince(startTime)
  137. switch doseType {
  138. case .bolus:
  139. let oldRate = rate
  140. if let remaining = remaining {
  141. units = units - remaining
  142. } else {
  143. units = oldRate * newDuration.hours
  144. }
  145. case .tempBasal:
  146. scheduledTempRate = rate
  147. units = floor(rate * newDuration.hours * Pod.pulsesPerUnit) / Pod.pulsesPerUnit
  148. print("Temp basal scheduled units: \(String(describing: scheduledUnits)), delivered units: \(units), duration: \(newDuration.minutes)")
  149. default:
  150. break
  151. }
  152. duration = newDuration
  153. }
  154. public var isMutable: Bool {
  155. switch doseType {
  156. case .bolus, .tempBasal:
  157. return !isFinished
  158. default:
  159. return false
  160. }
  161. }
  162. public var description: String {
  163. let unitsStr = insulinFormatter.string(from: units) ?? ""
  164. let startTimeStr = shortDateFormatter.string(from: startTime)
  165. let durationStr = duration?.format(using: [.minute, .second]) ?? ""
  166. switch doseType {
  167. case .bolus:
  168. if let scheduledUnits = scheduledUnits {
  169. let scheduledUnitsStr = insulinFormatter.string(from: scheduledUnits) ?? "?"
  170. return String(format: LocalizedString("InterruptedBolus: %1$@ U (%2$@ U scheduled) %3$@ %4$@ %5$@", comment: "The format string describing a bolus that was interrupted. (1: The amount delivered)(2: The amount scheduled)(3: Start time of the dose)(4: duration)(5: scheduled certainty)"), unitsStr, scheduledUnitsStr, startTimeStr, durationStr, scheduledCertainty.localizedDescription)
  171. } else {
  172. return String(format: LocalizedString("Bolus: %1$@U %2$@ %3$@ %4$@", comment: "The format string describing a bolus. (1: The amount delivered)(2: Start time of the dose)(3: duration)(4: scheduled certainty)"), unitsStr, startTimeStr, durationStr, scheduledCertainty.localizedDescription)
  173. }
  174. case .tempBasal:
  175. let volumeStr = insulinFormatter.string(from: units) ?? "?"
  176. let rateStr = NumberFormatter.localizedString(from: NSNumber(value: scheduledTempRate ?? rate), number: .decimal)
  177. return String(format: LocalizedString("TempBasal: %1$@ U/hour %2$@ %3$@ %4$@ U %5$@", comment: "The format string describing a temp basal. (1: The rate)(2: Start time)(3: duration)(4: volume)(5: scheduled certainty"), rateStr, startTimeStr, durationStr, volumeStr, scheduledCertainty.localizedDescription)
  178. case .suspend:
  179. return String(format: LocalizedString("Suspend: %1$@ %2$@", comment: "The format string describing a suspend. (1: Time)(2: Scheduled certainty"), startTimeStr, scheduledCertainty.localizedDescription)
  180. case .resume:
  181. return String(format: LocalizedString("Resume: %1$@ %2$@", comment: "The format string describing a resume. (1: Time)(2: Scheduled certainty"), startTimeStr, scheduledCertainty.localizedDescription)
  182. }
  183. }
  184. // RawRepresentable
  185. public init?(rawValue: RawValue) {
  186. guard
  187. let rawDoseType = rawValue["doseType"] as? Int,
  188. let doseType = DoseType(rawValue: rawDoseType),
  189. let units = rawValue["units"] as? Double,
  190. let startTime = rawValue["startTime"] as? Date,
  191. let rawScheduledCertainty = rawValue["scheduledCertainty"] as? Int,
  192. let scheduledCertainty = ScheduledCertainty(rawValue: rawScheduledCertainty)
  193. else {
  194. return nil
  195. }
  196. self.doseType = doseType
  197. self.units = units
  198. self.startTime = startTime
  199. self.scheduledCertainty = scheduledCertainty
  200. if let scheduledUnits = rawValue["scheduledUnits"] as? Double {
  201. self.scheduledUnits = scheduledUnits
  202. }
  203. if let scheduledTempRate = rawValue["scheduledTempRate"] as? Double {
  204. self.scheduledTempRate = scheduledTempRate
  205. }
  206. if let duration = rawValue["duration"] as? Double {
  207. self.duration = duration
  208. }
  209. if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue {
  210. self.insulinType = InsulinType(rawValue: rawInsulinType)
  211. }
  212. }
  213. public var rawValue: RawValue {
  214. var rawValue: RawValue = [
  215. "doseType": doseType.rawValue,
  216. "units": units,
  217. "startTime": startTime,
  218. "scheduledCertainty": scheduledCertainty.rawValue
  219. ]
  220. if let scheduledUnits = scheduledUnits {
  221. rawValue["scheduledUnits"] = scheduledUnits
  222. }
  223. if let scheduledTempRate = scheduledTempRate {
  224. rawValue["scheduledTempRate"] = scheduledTempRate
  225. }
  226. if let duration = duration {
  227. rawValue["duration"] = duration
  228. }
  229. if let insulinType = insulinType {
  230. rawValue["insulinType"] = insulinType.rawValue
  231. }
  232. return rawValue
  233. }
  234. }
  235. private extension TimeInterval {
  236. func format(using units: NSCalendar.Unit) -> String? {
  237. let formatter = DateComponentsFormatter()
  238. formatter.allowedUnits = units
  239. formatter.unitsStyle = .full
  240. formatter.zeroFormattingBehavior = .dropLeading
  241. formatter.maximumUnitCount = 2
  242. return formatter.string(from: self)
  243. }
  244. }
  245. extension NewPumpEvent {
  246. init(_ dose: UnfinalizedDose) {
  247. let title = String(describing: dose)
  248. let entry = DoseEntry(dose)
  249. self.init(date: dose.startTime, dose: entry, isMutable: dose.isMutable, raw: dose.uniqueKey, title: title)
  250. }
  251. }
  252. extension DoseEntry {
  253. init (_ dose: UnfinalizedDose) {
  254. switch dose.doseType {
  255. case .bolus:
  256. 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)
  257. case .tempBasal:
  258. self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.scheduledTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType)
  259. case .suspend:
  260. self = DoseEntry(suspendDate: dose.startTime)
  261. case .resume:
  262. self = DoseEntry(resumeDate: dose.startTime, insulinType: dose.insulinType)
  263. }
  264. }
  265. }