DailyQuantitySchedule+Override.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. //
  2. // DailyQuantitySchedule+Override.swift
  3. // LoopKit
  4. //
  5. // Created by Michael Pangburn on 3/26/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import HealthKit
  9. extension GlucoseRangeSchedule {
  10. public func applyingOverride(_ override: TemporaryScheduleOverride) -> GlucoseRangeSchedule {
  11. guard let targetRange = override.settings.targetRange else {
  12. return self
  13. }
  14. let doubleRange = targetRange.doubleRange(for: unit)
  15. let rangeOverride = GlucoseRangeSchedule.Override(value: doubleRange, start: override.startDate, end: override.scheduledEndDate)
  16. return GlucoseRangeSchedule(rangeSchedule: rangeSchedule, override: rangeOverride)
  17. }
  18. }
  19. extension /* BasalRateSchedule */ DailyValueSchedule where T == Double {
  20. func applyingBasalRateMultiplier(
  21. from override: TemporaryScheduleOverride,
  22. relativeTo date: Date = Date()
  23. ) -> BasalRateSchedule {
  24. return applyingOverride(override, relativeTo: date, multiplier: \.basalRateMultiplier)
  25. }
  26. }
  27. extension /* InsulinSensitivitySchedule */ DailyQuantitySchedule where T == Double {
  28. func applyingSensitivityMultiplier(
  29. from override: TemporaryScheduleOverride,
  30. relativeTo date: Date = Date()
  31. ) -> InsulinSensitivitySchedule {
  32. return DailyQuantitySchedule(
  33. unit: unit,
  34. valueSchedule: valueSchedule.applyingOverride(
  35. override,
  36. relativeTo: date,
  37. multiplier: \.insulinSensitivityMultiplier
  38. )
  39. )
  40. }
  41. }
  42. extension /* CarbRatioSchedule */ DailyQuantitySchedule where T == Double {
  43. func applyingCarbRatioMultiplier(
  44. from override: TemporaryScheduleOverride,
  45. relativeTo date: Date = Date()
  46. ) -> CarbRatioSchedule {
  47. return DailyQuantitySchedule(
  48. unit: unit,
  49. valueSchedule: valueSchedule.applyingOverride(
  50. override,
  51. relativeTo: date,
  52. multiplier: \.carbRatioMultiplier
  53. )
  54. )
  55. }
  56. }
  57. extension DailyValueSchedule where T == Double {
  58. fileprivate func applyingOverride(
  59. _ override: TemporaryScheduleOverride,
  60. relativeTo date: Date,
  61. multiplier multiplierKeyPath: KeyPath<TemporaryScheduleOverrideSettings, Double?>
  62. ) -> DailyValueSchedule {
  63. guard let multiplier = override.settings[keyPath: multiplierKeyPath] else {
  64. return self
  65. }
  66. return applyingOverride(
  67. during: override.activeInterval,
  68. relativeTo: date,
  69. updatingOverridenValuesWith: { $0 * multiplier }
  70. )
  71. }
  72. }
  73. extension DailyValueSchedule {
  74. fileprivate func applyingOverride(
  75. during activeInterval: DateInterval,
  76. relativeTo referenceDate: Date,
  77. updatingOverridenValuesWith update: (T) -> T
  78. ) -> DailyValueSchedule {
  79. guard let activeInterval = clampingToAffectedInterval(activeInterval, relativeTo: referenceDate) else {
  80. // Override has no effect relative to the reference date
  81. return self
  82. }
  83. let overrideStartOffset = scheduleOffset(for: activeInterval.start)
  84. let overrideEndOffset = scheduleOffset(for: activeInterval.end)
  85. guard overrideStartOffset != overrideEndOffset else {
  86. // Full schedule overridden
  87. return DailyValueSchedule(
  88. dailyItems: items.map { item in RepeatingScheduleValue(startTime: item.startTime, value: update(item.value)) },
  89. timeZone: timeZone
  90. )!
  91. }
  92. let overrideCrossesMidnight = overrideStartOffset > overrideEndOffset
  93. let scheduleItemsIncludingOverride = scheduleItemsPaddedToClosedInterval
  94. .adjacentPairs()
  95. .flatMap { item, nextItem -> [RepeatingScheduleValue<T>] in
  96. let overriddenItemValue = update(item.value)
  97. let overriddenItem = RepeatingScheduleValue(startTime: item.startTime, value: overriddenItemValue)
  98. let overrideStart = RepeatingScheduleValue(startTime: overrideStartOffset, value: overriddenItemValue)
  99. let overrideEnd = RepeatingScheduleValue(startTime: overrideEndOffset, value: item.value)
  100. let scheduleItemInterval = item.startTime..<nextItem.startTime
  101. let overrideStartsInThisSegment = scheduleItemInterval.contains(overrideStartOffset)
  102. let overrideEndsInThisSegment = scheduleItemInterval.contains(overrideEndOffset)
  103. switch (overrideStartsInThisSegment, overrideEndsInThisSegment) {
  104. case (true, true):
  105. if overrideCrossesMidnight {
  106. return item.startTime == overrideEndOffset
  107. ? [overrideEnd, overrideStart]
  108. : [overriddenItem, overrideEnd, overrideStart]
  109. } else {
  110. return item.startTime == overrideStartOffset
  111. ? [overrideStart, overrideEnd]
  112. : [item, overrideStart, overrideEnd]
  113. }
  114. case (true, false):
  115. return item.startTime == overrideStartOffset
  116. ? [overrideStart]
  117. : [item, overrideStart]
  118. case (false, true):
  119. return item.startTime == overrideEndOffset
  120. ? [overrideEnd]
  121. : [overriddenItem, overrideEnd]
  122. case (false, false):
  123. let segmentIsDisjointWithOverride = overrideCrossesMidnight
  124. ? overrideEndOffset...overrideStartOffset ~= item.startTime
  125. : !(overrideStartOffset...overrideEndOffset ~= item.startTime)
  126. return segmentIsDisjointWithOverride
  127. ? [item]
  128. : [overriddenItem]
  129. }
  130. }
  131. return DailyValueSchedule(
  132. dailyItems: scheduleItemsIncludingOverride,
  133. timeZone: timeZone
  134. )!
  135. }
  136. /// Clamps the override date interval to the relevant period of effect given a reference date.
  137. /// Returns `nil` if an override during the given interval has no effect relative to the reference date.
  138. private func clampingToAffectedInterval(_ interval: DateInterval, relativeTo referenceDate: Date) -> DateInterval? {
  139. let relevantPeriodStart = referenceDate.addingTimeInterval(-repeatInterval)
  140. let relevantPeriodEnd = referenceDate.addingTimeInterval(repeatInterval)
  141. guard
  142. interval.end > relevantPeriodStart,
  143. interval.start < relevantPeriodEnd
  144. else {
  145. return nil
  146. }
  147. let startDate = max(interval.start, relevantPeriodStart)
  148. let endDate = min(interval.end, relevantPeriodEnd)
  149. let affectedInterval = DateInterval(start: startDate, end: endDate)
  150. return affectedInterval
  151. }
  152. /// Pads the schedule with an extra item to form a closed interval.
  153. private var scheduleItemsPaddedToClosedInterval: [RepeatingScheduleValue<T>] {
  154. guard let lastItem = items.last else {
  155. assertionFailure("Schedule can never be empty")
  156. return []
  157. }
  158. let lastItemStartingAtDayEnd = RepeatingScheduleValue(startTime: maxTimeInterval, value: lastItem.value)
  159. return items + [lastItemStartingAtDayEnd]
  160. }
  161. }