DoseEntry.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. //
  2. // DoseEntry.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 1/31/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. public struct DoseEntry: TimelineValue, Equatable {
  11. public let type: DoseType
  12. public let startDate: Date
  13. public let endDate: Date
  14. internal let value: Double
  15. public let unit: DoseUnit
  16. public let deliveredUnits: Double?
  17. public let description: String?
  18. public let insulinType: InsulinType?
  19. public let automatic: Bool?
  20. public let manuallyEntered: Bool
  21. public internal(set) var syncIdentifier: String?
  22. public let isMutable: Bool
  23. public let wasProgrammedByPumpUI: Bool
  24. /// The scheduled basal rate during this dose entry
  25. public internal(set) var scheduledBasalRate: HKQuantity?
  26. public init(suspendDate: Date, automatic: Bool? = nil, isMutable: Bool = false, wasProgrammedByPumpUI: Bool = false) {
  27. self.init(type: .suspend, startDate: suspendDate, value: 0, unit: .units, automatic: automatic, isMutable: isMutable, wasProgrammedByPumpUI: wasProgrammedByPumpUI)
  28. }
  29. public init(resumeDate: Date, insulinType: InsulinType? = nil, automatic: Bool? = nil, isMutable: Bool = false, wasProgrammedByPumpUI: Bool = false) {
  30. self.init(type: .resume, startDate: resumeDate, value: 0, unit: .units, insulinType: insulinType, automatic: automatic, isMutable: isMutable, wasProgrammedByPumpUI: wasProgrammedByPumpUI)
  31. }
  32. // If the insulin model field is nil, it's assumed that the model is the type of insulin the pump dispenses
  33. public init(type: DoseType, startDate: Date, endDate: Date? = nil, value: Double, unit: DoseUnit, deliveredUnits: Double? = nil, description: String? = nil, syncIdentifier: String? = nil, scheduledBasalRate: HKQuantity? = nil, insulinType: InsulinType? = nil, automatic: Bool? = nil, manuallyEntered: Bool = false, isMutable: Bool = false, wasProgrammedByPumpUI: Bool = false) {
  34. self.type = type
  35. self.startDate = startDate
  36. self.endDate = endDate ?? startDate
  37. self.value = value
  38. self.unit = unit
  39. self.deliveredUnits = deliveredUnits
  40. self.description = description
  41. self.syncIdentifier = syncIdentifier
  42. self.scheduledBasalRate = scheduledBasalRate
  43. self.insulinType = insulinType
  44. self.automatic = automatic
  45. self.manuallyEntered = manuallyEntered
  46. self.isMutable = isMutable
  47. self.wasProgrammedByPumpUI = wasProgrammedByPumpUI
  48. }
  49. }
  50. extension DoseEntry {
  51. public static var units = HKUnit.internationalUnit()
  52. public static let unitsPerHour = HKUnit.internationalUnit().unitDivided(by: .hour())
  53. private var hours: Double {
  54. return endDate.timeIntervalSince(startDate).hours
  55. }
  56. public var programmedUnits: Double {
  57. switch unit {
  58. case .units:
  59. return value
  60. case .unitsPerHour:
  61. return value * hours
  62. }
  63. }
  64. public var unitsPerHour: Double {
  65. switch unit {
  66. case .units:
  67. let hours = self.hours
  68. guard hours != 0 else {
  69. return 0
  70. }
  71. return value / hours
  72. case .unitsPerHour:
  73. return value
  74. }
  75. }
  76. /// The number of units delivered, net the basal rate scheduled during that time, which can be used to compute insulin on-board and glucose effects
  77. public var netBasalUnits: Double {
  78. switch type {
  79. case .bolus:
  80. return deliveredUnits ?? programmedUnits
  81. case .basal:
  82. return 0
  83. case .resume, .suspend, .tempBasal:
  84. break
  85. }
  86. guard hours > 0 else {
  87. return 0
  88. }
  89. let scheduledUnitsPerHour: Double
  90. if let basalRate = scheduledBasalRate {
  91. scheduledUnitsPerHour = basalRate.doubleValue(for: DoseEntry.unitsPerHour)
  92. } else {
  93. scheduledUnitsPerHour = 0
  94. }
  95. let scheduledUnits = scheduledUnitsPerHour * hours
  96. return unitsInDeliverableIncrements - scheduledUnits
  97. }
  98. /// The rate of delivery, net the basal rate scheduled during that time, which can be used to compute insulin on-board and glucose effects
  99. public var netBasalUnitsPerHour: Double {
  100. switch type {
  101. case .basal:
  102. return 0
  103. case .bolus:
  104. return self.unitsPerHour
  105. default:
  106. break
  107. }
  108. guard let basalRate = scheduledBasalRate else {
  109. return 0
  110. }
  111. let unitsPerHour = self.unitsPerHour - basalRate.doubleValue(for: DoseEntry.unitsPerHour)
  112. guard abs(unitsPerHour) > .ulpOfOne else {
  113. return 0
  114. }
  115. return unitsPerHour
  116. }
  117. /// The smallest increment per unit of hourly basal delivery
  118. /// TODO: Is this 40 for x23 models? (yes - PS 7/26/2019)
  119. /// MinimedPumpmanager will be updated to report deliveredUnits, so this will end up not being used.
  120. private static let minimumMinimedIncrementPerUnit: Double = 20
  121. /// Returns the delivered units, or rounds to nearest deliverable (mdt) increment
  122. public var unitsInDeliverableIncrements: Double {
  123. guard case .unitsPerHour = unit else {
  124. return deliveredUnits ?? programmedUnits
  125. }
  126. return deliveredUnits ?? round(programmedUnits * DoseEntry.minimumMinimedIncrementPerUnit) / DoseEntry.minimumMinimedIncrementPerUnit
  127. }
  128. }
  129. extension DoseEntry: Codable {
  130. public init(from decoder: Decoder) throws {
  131. let container = try decoder.container(keyedBy: CodingKeys.self)
  132. self.type = try container.decode(DoseType.self, forKey: .type)
  133. self.startDate = try container.decode(Date.self, forKey: .startDate)
  134. self.endDate = try container.decode(Date.self, forKey: .endDate)
  135. self.value = try container.decode(Double.self, forKey: .value)
  136. self.unit = try container.decode(DoseUnit.self, forKey: .unit)
  137. self.deliveredUnits = try container.decodeIfPresent(Double.self, forKey: .deliveredUnits)
  138. self.description = try container.decodeIfPresent(String.self, forKey: .description)
  139. self.syncIdentifier = try container.decodeIfPresent(String.self, forKey: .syncIdentifier)
  140. self.insulinType = try container.decodeIfPresent(InsulinType.self, forKey: .insulinType)
  141. if let scheduledBasalRate = try container.decodeIfPresent(Double.self, forKey: .scheduledBasalRate),
  142. let scheduledBasalRateUnit = try container.decodeIfPresent(String.self, forKey: .scheduledBasalRateUnit) {
  143. self.scheduledBasalRate = HKQuantity(unit: HKUnit(from: scheduledBasalRateUnit), doubleValue: scheduledBasalRate)
  144. }
  145. self.automatic = try container.decodeIfPresent(Bool.self, forKey: .automatic)
  146. self.manuallyEntered = try container.decodeIfPresent(Bool.self, forKey: .manuallyEntered) ?? false
  147. self.isMutable = try container.decodeIfPresent(Bool.self, forKey: .isMutable) ?? false
  148. self.wasProgrammedByPumpUI = try container.decodeIfPresent(Bool.self, forKey: .wasProgrammedByPumpUI) ?? false
  149. }
  150. public func encode(to encoder: Encoder) throws {
  151. var container = encoder.container(keyedBy: CodingKeys.self)
  152. try container.encode(type, forKey: .type)
  153. try container.encode(startDate, forKey: .startDate)
  154. try container.encode(endDate, forKey: .endDate)
  155. try container.encode(value, forKey: .value)
  156. try container.encode(unit, forKey: .unit)
  157. try container.encodeIfPresent(deliveredUnits, forKey: .deliveredUnits)
  158. try container.encodeIfPresent(description, forKey: .description)
  159. try container.encodeIfPresent(syncIdentifier, forKey: .syncIdentifier)
  160. try container.encodeIfPresent(insulinType, forKey: .insulinType)
  161. if let scheduledBasalRate = scheduledBasalRate {
  162. try container.encode(scheduledBasalRate.doubleValue(for: DoseEntry.unitsPerHour), forKey: .scheduledBasalRate)
  163. try container.encode(DoseEntry.unitsPerHour.unitString, forKey: .scheduledBasalRateUnit)
  164. }
  165. try container.encodeIfPresent(automatic, forKey: .automatic)
  166. try container.encode(manuallyEntered, forKey: .manuallyEntered)
  167. try container.encode(isMutable, forKey: .isMutable)
  168. try container.encode(wasProgrammedByPumpUI, forKey: .wasProgrammedByPumpUI)
  169. }
  170. private enum CodingKeys: String, CodingKey {
  171. case type
  172. case startDate
  173. case endDate
  174. case value
  175. case unit
  176. case deliveredUnits
  177. case description
  178. case syncIdentifier
  179. case scheduledBasalRate
  180. case scheduledBasalRateUnit
  181. case insulinType
  182. case automatic
  183. case manuallyEntered
  184. case isMutable
  185. case wasProgrammedByPumpUI
  186. }
  187. }
  188. extension DoseEntry: RawRepresentable {
  189. public typealias RawValue = [String: Any]
  190. public init?(rawValue: [String: Any]) {
  191. guard let rawType = rawValue["type"] as? DoseType.RawValue,
  192. let type = DoseType(rawValue: rawType),
  193. let startDate = rawValue["startDate"] as? Date,
  194. let endDate = rawValue["endDate"] as? Date,
  195. let value = rawValue["value"] as? Double,
  196. let rawUnit = rawValue["unit"] as? DoseUnit.RawValue,
  197. let unit = DoseUnit(rawValue: rawUnit),
  198. let manuallyEntered = rawValue["manuallyEntered"] as? Bool
  199. else {
  200. return nil
  201. }
  202. self.type = type
  203. self.startDate = startDate
  204. self.endDate = endDate
  205. self.value = value
  206. self.unit = unit
  207. self.manuallyEntered = manuallyEntered
  208. self.deliveredUnits = rawValue["deliveredUnits"] as? Double
  209. self.description = rawValue["description"] as? String
  210. self.insulinType = (rawValue["insulinType"] as? InsulinType.RawValue).flatMap { InsulinType(rawValue: $0) }
  211. self.automatic = rawValue["automatic"] as? Bool
  212. self.syncIdentifier = rawValue["syncIdentifier"] as? String
  213. self.scheduledBasalRate = (rawValue["scheduledBasalRate"] as? Double).flatMap { HKQuantity(unit: .internationalUnitsPerHour, doubleValue: $0) }
  214. self.isMutable = rawValue["isMutable"] as? Bool ?? false
  215. self.wasProgrammedByPumpUI = rawValue["wasProgrammedByPumpUI"] as? Bool ?? false
  216. }
  217. public var rawValue: [String: Any] {
  218. var rawValue: [String: Any] = [
  219. "type": type.rawValue,
  220. "startDate": startDate,
  221. "endDate": endDate,
  222. "value": value,
  223. "unit": unit.rawValue,
  224. "manuallyEntered": manuallyEntered,
  225. "isMutable": isMutable,
  226. "wasProgrammedByPumpUI": wasProgrammedByPumpUI
  227. ]
  228. rawValue["deliveredUnits"] = deliveredUnits
  229. rawValue["description"] = description
  230. rawValue["insulinType"] = insulinType?.rawValue
  231. rawValue["automatic"] = automatic
  232. rawValue["syncIdentifier"] = syncIdentifier
  233. rawValue["scheduledBasalRate"] = scheduledBasalRate?.doubleValue(for: .internationalUnitsPerHour)
  234. return rawValue
  235. }
  236. }