InsulinMath.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. //
  2. // InsulinMath.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 1/30/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. extension DoseEntry {
  11. private func continuousDeliveryInsulinOnBoard(at date: Date, model: InsulinModel, delta: TimeInterval) -> Double {
  12. let doseDuration = endDate.timeIntervalSince(startDate) // t1
  13. let time = date.timeIntervalSince(startDate)
  14. var iob: Double = 0
  15. var doseDate = TimeInterval(0) // i
  16. repeat {
  17. let segment: Double
  18. if doseDuration > 0 {
  19. segment = max(0, min(doseDate + delta, doseDuration) - doseDate) / doseDuration
  20. } else {
  21. segment = 1
  22. }
  23. iob += segment * model.percentEffectRemaining(at: time - doseDate)
  24. doseDate += delta
  25. } while doseDate <= min(floor((time + model.delay) / delta) * delta, doseDuration)
  26. return iob
  27. }
  28. func insulinOnBoard(at date: Date, model: InsulinModel, delta: TimeInterval) -> Double {
  29. let time = date.timeIntervalSince(startDate)
  30. guard time >= 0 else {
  31. return 0
  32. }
  33. // Consider doses within the delta time window as momentary
  34. if endDate.timeIntervalSince(startDate) <= 1.05 * delta {
  35. return netBasalUnits * model.percentEffectRemaining(at: time)
  36. } else {
  37. return netBasalUnits * continuousDeliveryInsulinOnBoard(at: date, model: model, delta: delta)
  38. }
  39. }
  40. private func continuousDeliveryGlucoseEffect(at date: Date, model: InsulinModel, delta: TimeInterval) -> Double {
  41. let doseDuration = endDate.timeIntervalSince(startDate) // t1
  42. let time = date.timeIntervalSince(startDate)
  43. var value: Double = 0
  44. var doseDate = TimeInterval(0) // i
  45. repeat {
  46. let segment: Double
  47. if doseDuration > 0 {
  48. segment = max(0, min(doseDate + delta, doseDuration) - doseDate) / doseDuration
  49. } else {
  50. segment = 1
  51. }
  52. value += segment * (1.0 - model.percentEffectRemaining(at: time - doseDate))
  53. doseDate += delta
  54. } while doseDate <= min(floor((time + model.delay) / delta) * delta, doseDuration)
  55. return value
  56. }
  57. func glucoseEffect(at date: Date, model: InsulinModel, insulinSensitivity: Double, delta: TimeInterval) -> Double {
  58. let time = date.timeIntervalSince(startDate)
  59. guard time >= 0 else {
  60. return 0
  61. }
  62. // Consider doses within the delta time window as momentary
  63. if endDate.timeIntervalSince(startDate) <= 1.05 * delta {
  64. return netBasalUnits * -insulinSensitivity * (1.0 - model.percentEffectRemaining(at: time))
  65. } else {
  66. return netBasalUnits * -insulinSensitivity * continuousDeliveryGlucoseEffect(at: date, model: model, delta: delta)
  67. }
  68. }
  69. func trimmed(from start: Date? = nil, to end: Date? = nil, syncIdentifier: String? = nil) -> DoseEntry {
  70. let originalDuration = endDate.timeIntervalSince(startDate)
  71. let startDate = max(start ?? .distantPast, self.startDate)
  72. let endDate = max(startDate, min(end ?? .distantFuture, self.endDate))
  73. var trimmedDeliveredUnits: Double? = deliveredUnits
  74. var trimmedValue: Double = value
  75. if originalDuration > .ulpOfOne && (startDate > self.startDate || endDate < self.endDate) {
  76. let updatedActualDelivery = unitsInDeliverableIncrements * (endDate.timeIntervalSince(startDate) / originalDuration)
  77. if deliveredUnits != nil {
  78. trimmedDeliveredUnits = updatedActualDelivery
  79. }
  80. if case .units = unit {
  81. trimmedValue = updatedActualDelivery
  82. }
  83. }
  84. return DoseEntry(
  85. type: type,
  86. startDate: startDate,
  87. endDate: endDate,
  88. value: trimmedValue,
  89. unit: unit,
  90. deliveredUnits: trimmedDeliveredUnits,
  91. description: description,
  92. syncIdentifier: syncIdentifier,
  93. scheduledBasalRate: scheduledBasalRate,
  94. insulinType: insulinType,
  95. automatic: automatic,
  96. isMutable: isMutable,
  97. wasProgrammedByPumpUI: wasProgrammedByPumpUI
  98. )
  99. }
  100. }
  101. /**
  102. It takes a MM x22 pump about 40s to deliver 1 Unit while bolusing
  103. See: http://www.healthline.com/diabetesmine/ask-dmine-speed-insulin-pumps#3
  104. The x23 and newer pumps can deliver at 2x, 3x, and 4x that speed, targeting
  105. a maximum 5-minute delivery for all boluses (8U - 25U)
  106. A basal rate of 30 U/hour (near-max) would deliver an additional 0.5 U/min.
  107. */
  108. private let MaximumReservoirDropPerMinute = 6.5
  109. extension Collection where Element: ReservoirValue {
  110. /**
  111. Converts a continuous, chronological sequence of reservoir values to a sequence of doses
  112. This is an O(n) operation.
  113. - returns: An array of doses
  114. */
  115. var doseEntries: [DoseEntry] {
  116. var doses: [DoseEntry] = []
  117. var previousValue: Element?
  118. let numberFormatter = NumberFormatter()
  119. numberFormatter.numberStyle = .decimal
  120. numberFormatter.maximumFractionDigits = 3
  121. for value in self {
  122. if let previousValue = previousValue {
  123. let volumeDrop = previousValue.unitVolume - value.unitVolume
  124. let duration = value.startDate.timeIntervalSince(previousValue.startDate)
  125. if duration > 0 && 0 <= volumeDrop && volumeDrop <= MaximumReservoirDropPerMinute * duration.minutes {
  126. doses.append(DoseEntry(
  127. type: .tempBasal,
  128. startDate: previousValue.startDate,
  129. endDate: value.startDate,
  130. value: volumeDrop,
  131. unit: .units
  132. ))
  133. }
  134. }
  135. previousValue = value
  136. }
  137. return doses
  138. }
  139. /**
  140. Whether a span of chronological reservoir values is considered continuous and therefore reliable.
  141. Reservoir values of 0 are automatically considered unreliable due to the assumption that an unknown amount of insulin can be delivered after the 0 marker.
  142. - parameter startDate: The beginning of the interval in which to validate continuity
  143. - parameter endDate: The end of the interval in which to validate continuity
  144. - parameter maximumDuration: The maximum interval to consider reliable for a reservoir-derived dose
  145. - returns: Whether the reservoir values meet the critera for continuity
  146. */
  147. func isContinuous(from start: Date?, to end: Date, within maximumDuration: TimeInterval) -> Bool {
  148. guard let firstValue = self.first else {
  149. return false
  150. }
  151. // The first value has to be at least as old as the start date, as a reference point.
  152. let startDate = start ?? firstValue.endDate
  153. guard firstValue.endDate <= startDate else {
  154. return false
  155. }
  156. var lastValue = firstValue
  157. for value in self {
  158. defer {
  159. lastValue = value
  160. }
  161. // Volume and interval validation only applies for values in the specified range,
  162. guard value.endDate >= startDate && value.startDate <= end else {
  163. continue
  164. }
  165. // We can't trust 0. What else was delivered?
  166. guard value.unitVolume > 0 else {
  167. return false
  168. }
  169. // Rises in reservoir volume indicate a rewind + prime, and primes
  170. // can be easily confused with boluses.
  171. // Small rises (1 U) can be ignored as they're indicative of a mixed-precision sequence.
  172. guard value.unitVolume <= lastValue.unitVolume + 1 else {
  173. return false
  174. }
  175. // Ensure no more than the maximum interval has passed
  176. guard value.startDate.timeIntervalSince(lastValue.endDate) <= maximumDuration else {
  177. return false
  178. }
  179. }
  180. return true
  181. }
  182. }
  183. extension DoseEntry {
  184. /// Annotates a dose with the context of the scheduled basal rate
  185. ///
  186. /// If the dose crosses a schedule boundary, it will be split into multiple doses so each dose has a
  187. /// single scheduled basal rate.
  188. ///
  189. /// - Parameter basalSchedule: The basal rate schedule to apply
  190. /// - Returns: An array of annotated doses
  191. fileprivate func annotated(with basalSchedule: BasalRateSchedule) -> [DoseEntry] {
  192. switch type {
  193. case .tempBasal, .suspend, .resume:
  194. guard scheduledBasalRate == nil else {
  195. return [self]
  196. }
  197. break
  198. case .basal, .bolus:
  199. return [self]
  200. }
  201. var doses: [DoseEntry] = []
  202. let basalItems = basalSchedule.between(start: startDate, end: endDate)
  203. for (index, basalItem) in basalItems.enumerated() {
  204. let startDate: Date
  205. let endDate: Date
  206. // If we're splitting into multiple entries, keep the syncIdentifier unique
  207. var syncIdentifier = self.syncIdentifier
  208. if syncIdentifier != nil, basalItems.count > 1 {
  209. syncIdentifier! += " \(index + 1)/\(basalItems.count)"
  210. }
  211. if index == 0 {
  212. startDate = self.startDate
  213. } else {
  214. startDate = basalItem.startDate
  215. }
  216. if index == basalItems.count - 1 {
  217. endDate = self.endDate
  218. } else {
  219. endDate = basalItems[index + 1].startDate
  220. }
  221. var dose = trimmed(from: startDate, to: endDate, syncIdentifier: syncIdentifier)
  222. dose.scheduledBasalRate = HKQuantity(unit: DoseEntry.unitsPerHour, doubleValue: basalItem.value)
  223. doses.append(dose)
  224. }
  225. return doses
  226. }
  227. /// Annotates a dose with the specified insulin type.
  228. ///
  229. /// - Parameter insulinType: The insulin type to annotate the dose with.
  230. /// - Returns: A dose annotated with the insulin model
  231. public func annotated(with insulinType: InsulinType) -> DoseEntry {
  232. return DoseEntry(
  233. type: type,
  234. startDate: startDate,
  235. endDate: endDate,
  236. value: value,
  237. unit: unit,
  238. deliveredUnits: deliveredUnits,
  239. description: description,
  240. syncIdentifier: syncIdentifier,
  241. scheduledBasalRate: scheduledBasalRate,
  242. insulinType: insulinType,
  243. isMutable: isMutable,
  244. wasProgrammedByPumpUI: wasProgrammedByPumpUI
  245. )
  246. }
  247. }
  248. extension DoseEntry {
  249. /// Calculates the timeline of glucose effects for a temp basal dose
  250. /// Use case: predict glucose effects of zero temping
  251. ///
  252. /// - Parameters:
  253. /// - insulinModelProvider: A factory that can provide an insulin model given an insulin type
  254. /// - longestEffectDuration: The longest duration that a dose could be active.
  255. /// - defaultInsulinModelSetting: The model of insulin activity over time
  256. /// - insulinSensitivity: The schedule of glucose effect per unit of insulin
  257. /// - basalRateSchedule: The schedule of basal rates
  258. /// - Returns: An array of glucose effects for the duration of the temp basal dose plus the duration of insulin action
  259. public func tempBasalGlucoseEffects(
  260. insulinModelProvider: InsulinModelProvider,
  261. longestEffectDuration: TimeInterval,
  262. insulinSensitivity: InsulinSensitivitySchedule,
  263. basalRateSchedule: BasalRateSchedule
  264. ) -> [GlucoseEffect] {
  265. guard case .tempBasal = type else {
  266. return []
  267. }
  268. let netTempBasalDoses = self.annotated(with: basalRateSchedule)
  269. return netTempBasalDoses.glucoseEffects(insulinModelProvider: insulinModelProvider, longestEffectDuration: longestEffectDuration, insulinSensitivity: insulinSensitivity)
  270. }
  271. fileprivate var resolvingDelivery: DoseEntry {
  272. guard !isMutable, deliveredUnits == nil else {
  273. return self
  274. }
  275. let resolvedUnits: Double
  276. if case .units = unit {
  277. resolvedUnits = value
  278. } else {
  279. switch type {
  280. case .tempBasal:
  281. resolvedUnits = unitsInDeliverableIncrements
  282. case .basal:
  283. resolvedUnits = programmedUnits
  284. default:
  285. return self
  286. }
  287. }
  288. return DoseEntry(type: type, startDate: startDate, endDate: endDate, value: value, unit: unit, deliveredUnits: resolvedUnits, description: description, syncIdentifier: syncIdentifier, scheduledBasalRate: scheduledBasalRate, insulinType: insulinType, isMutable: isMutable, wasProgrammedByPumpUI: wasProgrammedByPumpUI)
  289. }
  290. }
  291. extension Collection where Element == DoseEntry {
  292. /**
  293. Maps a timeline of dose entries with overlapping start and end dates to a timeline of doses that represents actual insulin delivery.
  294. - returns: An array of reconciled insulin delivery history, as TempBasal and Bolus records
  295. */
  296. func reconciled() -> [DoseEntry] {
  297. var reconciled: [DoseEntry] = []
  298. var lastSuspend: DoseEntry?
  299. var lastBasal: DoseEntry?
  300. for dose in self {
  301. switch dose.type {
  302. case .bolus:
  303. reconciled.append(dose)
  304. case .basal, .tempBasal:
  305. if lastSuspend == nil, let last = lastBasal {
  306. let endDate = Swift.min(last.endDate, dose.startDate)
  307. // Ignore 0-duration doses
  308. if endDate > last.startDate {
  309. reconciled.append(last.trimmed(from: nil, to: endDate, syncIdentifier: last.syncIdentifier))
  310. }
  311. } else if let suspend = lastSuspend, dose.type == .tempBasal {
  312. // Handle missing resume. Basal following suspend, with no resume.
  313. reconciled.append(DoseEntry(
  314. type: suspend.type,
  315. startDate: suspend.startDate,
  316. endDate: dose.startDate,
  317. value: suspend.value,
  318. unit: suspend.unit,
  319. description: suspend.description ?? dose.description,
  320. syncIdentifier: suspend.syncIdentifier,
  321. insulinType: suspend.insulinType,
  322. isMutable: suspend.isMutable,
  323. wasProgrammedByPumpUI: suspend.wasProgrammedByPumpUI
  324. ))
  325. lastSuspend = nil
  326. }
  327. lastBasal = dose
  328. case .resume:
  329. if let suspend = lastSuspend {
  330. reconciled.append(DoseEntry(
  331. type: suspend.type,
  332. startDate: suspend.startDate,
  333. endDate: dose.startDate,
  334. value: suspend.value,
  335. unit: suspend.unit,
  336. description: suspend.description ?? dose.description,
  337. syncIdentifier: suspend.syncIdentifier,
  338. insulinType: suspend.insulinType,
  339. isMutable: suspend.isMutable,
  340. wasProgrammedByPumpUI: suspend.wasProgrammedByPumpUI
  341. ))
  342. lastSuspend = nil
  343. // Continue temp basals that may have started before suspending
  344. if let last = lastBasal {
  345. if last.endDate > dose.endDate {
  346. lastBasal = DoseEntry(
  347. type: last.type,
  348. startDate: dose.endDate,
  349. endDate: last.endDate,
  350. value: last.value,
  351. unit: last.unit,
  352. description: last.description,
  353. // We intentionally use the resume's identifier, as the basal entry has already been entered
  354. syncIdentifier: dose.syncIdentifier,
  355. insulinType: last.insulinType,
  356. automatic: last.automatic,
  357. isMutable: last.isMutable,
  358. wasProgrammedByPumpUI: last.wasProgrammedByPumpUI
  359. )
  360. } else {
  361. lastBasal = nil
  362. }
  363. }
  364. }
  365. case .suspend:
  366. if let last = lastBasal {
  367. reconciled.append(DoseEntry(
  368. type: last.type,
  369. startDate: last.startDate,
  370. endDate: Swift.min(last.endDate, dose.startDate),
  371. value: last.value,
  372. unit: last.unit,
  373. description: last.description,
  374. syncIdentifier: last.syncIdentifier,
  375. insulinType: last.insulinType,
  376. isMutable: last.isMutable,
  377. wasProgrammedByPumpUI: last.wasProgrammedByPumpUI
  378. ))
  379. if last.endDate <= dose.startDate {
  380. lastBasal = nil
  381. }
  382. }
  383. lastSuspend = dose
  384. }
  385. }
  386. if let suspend = lastSuspend {
  387. reconciled.append(suspend)
  388. } else if let last = lastBasal, last.endDate > last.startDate {
  389. reconciled.append(last)
  390. }
  391. return reconciled.map { $0.resolvingDelivery }
  392. }
  393. /// Annotates a sequence of dose entries with the configured basal rate schedule.
  394. ///
  395. /// Doses which cross time boundaries in the basal rate schedule are split into multiple entries.
  396. ///
  397. /// - Parameter basalSchedule: The basal rate schedule
  398. /// - Returns: An array of annotated dose entries
  399. func annotated(with basalSchedule: BasalRateSchedule) -> [DoseEntry] {
  400. var annotatedDoses: [DoseEntry] = []
  401. for dose in self {
  402. annotatedDoses += dose.annotated(with: basalSchedule)
  403. }
  404. return annotatedDoses
  405. }
  406. /**
  407. Calculates the total insulin delivery for a collection of doses
  408. - returns: The total insulin insulin, in Units
  409. */
  410. var totalDelivery: Double {
  411. return reduce(0) { (total, dose) -> Double in
  412. return total + dose.unitsInDeliverableIncrements
  413. }
  414. }
  415. /**
  416. Calculates the timeline of insulin remaining for a collection of doses
  417. - parameter insulinModelProvider: A factory that can provide an insulin model given an insulin type
  418. - parameter longestEffectDuration: The longest duration that a dose could be active.
  419. - parameter start: The date to start the timeline
  420. - parameter end: The date to end the timeline
  421. - parameter delta: The differential between timeline entries
  422. - returns: A sequence of insulin amount remaining
  423. */
  424. func insulinOnBoard(
  425. insulinModelProvider: InsulinModelProvider,
  426. longestEffectDuration: TimeInterval,
  427. from start: Date? = nil,
  428. to end: Date? = nil,
  429. delta: TimeInterval = TimeInterval(minutes: 5)
  430. ) -> [InsulinValue] {
  431. guard let (start, end) = LoopMath.simulationDateRangeForSamples(self, from: start, to: end, duration: longestEffectDuration, delta: delta) else {
  432. return []
  433. }
  434. var date = start
  435. var values = [InsulinValue]()
  436. repeat {
  437. let value = reduce(0) { (value, dose) -> Double in
  438. return value + dose.insulinOnBoard(at: date, model: insulinModelProvider.model(for: dose.insulinType), delta: delta)
  439. }
  440. values.append(InsulinValue(startDate: date, value: value))
  441. date = date.addingTimeInterval(delta)
  442. } while date <= end
  443. return values
  444. }
  445. /// Calculates the timeline of glucose effects for a collection of doses
  446. ///
  447. /// - Parameters:
  448. /// - insulinModelProvider: A factory that can provide an insulin model given an insulin type
  449. /// - longestEffectDuration: The longest duration that a dose could be active.
  450. /// - insulinSensitivity: The schedule of glucose effect per unit of insulin
  451. /// - start: The earliest date of effects to return
  452. /// - end: The latest date of effects to return
  453. /// - delta: The interval between returned effects
  454. /// - Returns: An array of glucose effects for the duration of the doses
  455. public func glucoseEffects(
  456. insulinModelProvider: InsulinModelProvider,
  457. longestEffectDuration: TimeInterval,
  458. insulinSensitivity: InsulinSensitivitySchedule,
  459. from start: Date? = nil,
  460. to end: Date? = nil,
  461. delta: TimeInterval = TimeInterval(/* minutes: */60 * 5)
  462. ) -> [GlucoseEffect] {
  463. guard let (start, end) = LoopMath.simulationDateRangeForSamples(self.filter({ entry in
  464. entry.netBasalUnits != 0
  465. }), from: start, to: end, duration: longestEffectDuration, delta: delta) else {
  466. return []
  467. }
  468. var date = start
  469. var values = [GlucoseEffect]()
  470. let unit = HKUnit.milligramsPerDeciliter
  471. repeat {
  472. let value = reduce(0) { (value, dose) -> Double in
  473. return value + dose.glucoseEffect(at: date, model: insulinModelProvider.model(for: dose.insulinType), insulinSensitivity: insulinSensitivity.quantity(at: dose.startDate).doubleValue(for: unit), delta: delta)
  474. }
  475. values.append(GlucoseEffect(startDate: date, quantity: HKQuantity(unit: unit, doubleValue: value)))
  476. date = date.addingTimeInterval(delta)
  477. } while date <= end
  478. return values
  479. }
  480. /// Applies the current basal schedule to a collection of reconciled doses in chronological order
  481. ///
  482. /// The scheduled basal rate is associated doses that override it, for later derivation of net delivery
  483. ///
  484. /// - Parameters:
  485. /// - basalSchedule: The active basal schedule during the dose delivery
  486. /// - start: The earliest date to apply the basal schedule
  487. /// - end: The latest date to include. Doses must end before this time to be included.
  488. /// - insertingBasalEntries: Whether basal doses should be created from the schedule. Pass true only for pump models that do not report their basal rates in event history.
  489. /// - Returns: An array of doses,
  490. func overlayBasalSchedule(_ basalSchedule: BasalRateSchedule, startingAt start: Date, endingAt end: Date = .distantFuture, insertingBasalEntries: Bool) -> [DoseEntry] {
  491. let dateFormatter = ISO8601DateFormatter() // GMT-based ISO formatting
  492. var newEntries = [DoseEntry]()
  493. var lastBasal: DoseEntry?
  494. if insertingBasalEntries {
  495. // Create a placeholder entry at our start date, so we know the correct duration of the
  496. // inserted basal entries
  497. lastBasal = DoseEntry(resumeDate: start)
  498. }
  499. for dose in self {
  500. switch dose.type {
  501. case .tempBasal, .basal, .suspend:
  502. // Only include entries if they have ended by the end date, since they may be cancelled
  503. guard dose.endDate <= end else {
  504. continue
  505. }
  506. if let lastBasal = lastBasal {
  507. if insertingBasalEntries {
  508. // For older pumps which don't report the start/end of scheduled basal delivery,
  509. // generate a basal entry from the specified schedule.
  510. for scheduled in basalSchedule.between(start: lastBasal.endDate, end: dose.startDate) {
  511. // Generate a unique identifier based on the start/end timestamps
  512. let start = Swift.max(lastBasal.endDate, scheduled.startDate)
  513. let end = Swift.min(dose.startDate, scheduled.endDate)
  514. guard end.timeIntervalSince(start) > .ulpOfOne else {
  515. continue
  516. }
  517. let syncIdentifier = "BasalRateSchedule \(dateFormatter.string(from: start)) \(dateFormatter.string(from: end))"
  518. newEntries.append(DoseEntry(
  519. type: .basal,
  520. startDate: start,
  521. endDate: end,
  522. value: scheduled.value,
  523. unit: .unitsPerHour,
  524. syncIdentifier: syncIdentifier,
  525. scheduledBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: scheduled.value),
  526. insulinType: lastBasal.insulinType,
  527. automatic: lastBasal.automatic
  528. ))
  529. }
  530. }
  531. }
  532. lastBasal = dose
  533. // Only include the last basal entry if has ended by our end date
  534. if let lastBasal = lastBasal {
  535. newEntries.append(lastBasal)
  536. }
  537. case .resume:
  538. assertionFailure("No resume events should be present in reconciled doses")
  539. case .bolus:
  540. newEntries.append(dose)
  541. }
  542. }
  543. return newEntries
  544. }
  545. /// Creates an array of DoseEntry values by unioning another array, de-duplicating by syncIdentifier
  546. ///
  547. /// - Parameter otherDoses: An array of doses to union
  548. /// - Returns: A new array of doses
  549. func appendedUnion(with otherDoses: [DoseEntry]) -> [DoseEntry] {
  550. var union: [DoseEntry] = []
  551. var syncIdentifiers: Set<String> = []
  552. for dose in (self + otherDoses) {
  553. if let syncIdentifier = dose.syncIdentifier {
  554. let (inserted, _) = syncIdentifiers.insert(syncIdentifier)
  555. if !inserted {
  556. continue
  557. }
  558. }
  559. union.append(dose)
  560. }
  561. return union
  562. }
  563. }
  564. extension BidirectionalCollection where Element == DoseEntry {
  565. /// The endDate of the last basal dose in the collection
  566. var lastBasalEndDate: Date? {
  567. for dose in self.reversed() {
  568. if dose.type == .basal || dose.type == .tempBasal || dose.type == .resume {
  569. return dose.endDate
  570. }
  571. }
  572. return nil
  573. }
  574. }