TemporaryScheduleOverrideHistory.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. //
  2. // TemporaryScheduleOverrideHistory.swift
  3. // LoopKit
  4. //
  5. // Created by Michael Pangburn on 3/25/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import Foundation
  9. public enum End: Equatable, Hashable, Codable {
  10. case natural
  11. case early(Date)
  12. case deleted // Ended before started
  13. private enum EndType: String, Decodable {
  14. case natural, early, deleted
  15. }
  16. private enum CodingKeys: String, CodingKey {
  17. case type
  18. case date
  19. }
  20. public init(from decoder: Decoder) throws {
  21. let container = try decoder.container(keyedBy: CodingKeys.self)
  22. let endType = try container.decode(EndType.self, forKey: .type)
  23. switch endType {
  24. case .natural:
  25. self = .natural
  26. case .early:
  27. let date = try container.decode(Date.self, forKey: .date)
  28. self = .early(date)
  29. case .deleted:
  30. self = .deleted
  31. }
  32. }
  33. public func encode(to encoder: Encoder) throws {
  34. var container = encoder.container(keyedBy: CodingKeys.self)
  35. switch self {
  36. case .natural:
  37. try container.encode(EndType.natural.rawValue, forKey: .type)
  38. case .early(let date):
  39. try container.encode(EndType.early.rawValue, forKey: .type)
  40. try container.encode(date, forKey: .date)
  41. case .deleted:
  42. try container.encode(EndType.deleted.rawValue, forKey: .type)
  43. }
  44. }
  45. }
  46. private struct OverrideEvent: Equatable {
  47. var override: TemporaryScheduleOverride
  48. var modificationCounter: Int
  49. init(override: TemporaryScheduleOverride, modificationCounter: Int) {
  50. self.override = override
  51. self.modificationCounter = modificationCounter
  52. }
  53. }
  54. public protocol TemporaryScheduleOverrideHistoryDelegate: AnyObject {
  55. func temporaryScheduleOverrideHistoryDidUpdate(_ history: TemporaryScheduleOverrideHistory)
  56. }
  57. public final class TemporaryScheduleOverrideHistory {
  58. public typealias QueryAnchor = Int
  59. private var recentEvents: [OverrideEvent] = [] {
  60. didSet {
  61. modificationCounter += 1
  62. delegate?.temporaryScheduleOverrideHistoryDidUpdate(self)
  63. if let lastTaintedEvent = taintedEventLog.last,
  64. Date().timeIntervalSince(lastTaintedEvent.override.startDate) > .hours(48)
  65. {
  66. taintedEventLog.removeAll()
  67. }
  68. }
  69. }
  70. /// Tracks a sequence of override events that failed validation checks.
  71. /// Stored to enable retrieval via issue report after a deliberate crash.
  72. private var taintedEventLog: [OverrideEvent] = [] {
  73. didSet {
  74. delegate?.temporaryScheduleOverrideHistoryDidUpdate(self)
  75. }
  76. }
  77. private var modificationCounter: Int
  78. public var relevantTimeWindow: TimeInterval = TimeInterval.hours(10)
  79. public weak var delegate: TemporaryScheduleOverrideHistoryDelegate?
  80. public init() {
  81. modificationCounter = 0
  82. }
  83. public func recordOverride(_ override: TemporaryScheduleOverride?, at enableDate: Date = Date()) {
  84. guard override != lastUndeletedEvent?.override else {
  85. return
  86. }
  87. if let override = override {
  88. record(override, at: enableDate)
  89. } else {
  90. cancelActiveOverride(at: enableDate)
  91. }
  92. }
  93. private var lastUndeletedEvent: OverrideEvent? {
  94. return recentEvents.reversed().first { $0.override.actualEnd != .deleted }
  95. }
  96. private func deleteEventsStartingOnOrAfter(_ date: Date) {
  97. recentEvents.mutateEach { (event) in
  98. if event.override.startDate >= date {
  99. event.override.actualEnd = .deleted
  100. event.modificationCounter = modificationCounter
  101. }
  102. }
  103. }
  104. // Deletes overrides that start after the passed in override.
  105. private func record(_ override: TemporaryScheduleOverride, at enableDate: Date) {
  106. // Check for modification of existing entry
  107. var index = recentEvents.endIndex
  108. while index != recentEvents.startIndex {
  109. recentEvents.formIndex(before: &index)
  110. if recentEvents[index].override.syncIdentifier == override.syncIdentifier {
  111. recentEvents[index].override = override
  112. recentEvents[index].modificationCounter = modificationCounter
  113. return
  114. }
  115. }
  116. deleteEventsStartingOnOrAfter(override.startDate)
  117. let overrideEnd = min(override.startDate.nearestPrevious, enableDate)
  118. cancelActiveOverride(at: overrideEnd)
  119. let enabledEvent = OverrideEvent(override: override, modificationCounter: modificationCounter)
  120. recentEvents.append(enabledEvent)
  121. }
  122. private func cancelActiveOverride(at date: Date) {
  123. var index = recentEvents.endIndex
  124. while index != recentEvents.startIndex {
  125. recentEvents.formIndex(before: &index)
  126. if recentEvents[index].override.actualEnd != .deleted {
  127. if recentEvents[index].override.actualEndDate > date {
  128. if recentEvents[index].override.startDate > date {
  129. recentEvents[index].override.actualEnd = .deleted
  130. } else {
  131. recentEvents[index].override.actualEnd = .early(date)
  132. }
  133. recentEvents[index].modificationCounter = modificationCounter
  134. }
  135. break
  136. }
  137. }
  138. }
  139. public func resolvingRecentBasalSchedule(_ base: BasalRateSchedule, relativeTo referenceDate: Date = Date()) -> BasalRateSchedule {
  140. filterRecentEvents(relativeTo: referenceDate)
  141. return overridesReflectingEnabledDuration(relativeTo: referenceDate).reduce(base) { base, override in
  142. base.applyingBasalRateMultiplier(from: override, relativeTo: referenceDate)
  143. }
  144. }
  145. public func resolvingRecentInsulinSensitivitySchedule(_ base: InsulinSensitivitySchedule, relativeTo referenceDate: Date = Date()) -> InsulinSensitivitySchedule {
  146. filterRecentEvents(relativeTo: referenceDate)
  147. return overridesReflectingEnabledDuration(relativeTo: referenceDate).reduce(base) { base, override in
  148. base.applyingSensitivityMultiplier(from: override, relativeTo: referenceDate)
  149. }
  150. }
  151. public func resolvingRecentCarbRatioSchedule(_ base: CarbRatioSchedule, relativeTo referenceDate: Date = Date()) -> CarbRatioSchedule {
  152. filterRecentEvents(relativeTo: referenceDate)
  153. return overridesReflectingEnabledDuration(relativeTo: referenceDate).reduce(base) { base, override in
  154. base.applyingCarbRatioMultiplier(from: override, relativeTo: referenceDate)
  155. }
  156. }
  157. public func getEvents(relativeTo referenceDate: Date = Date()) -> [TemporaryScheduleOverride] {
  158. filterRecentEvents(relativeTo: referenceDate)
  159. return recentEvents.map { $0.override }
  160. }
  161. private func relevantPeriod(relativeTo referenceDate: Date) -> DateInterval {
  162. return DateInterval(
  163. start: referenceDate.addingTimeInterval(-relevantTimeWindow),
  164. end: referenceDate.addingTimeInterval(relevantTimeWindow)
  165. )
  166. }
  167. private func filterRecentEvents(relativeTo referenceDate: Date) {
  168. let period = relevantPeriod(relativeTo: referenceDate)
  169. var recentEvents = self.recentEvents
  170. recentEvents.removeAll(where: { event in
  171. event.override.actualEndDate < period.start || event.override.startDate > period.end
  172. })
  173. if recentEvents != self.recentEvents {
  174. self.recentEvents = recentEvents
  175. }
  176. }
  177. private func overridesReflectingEnabledDuration(relativeTo referenceDate: Date) -> [TemporaryScheduleOverride] {
  178. var overrides = recentEvents.filter({$0.override.actualEnd != .deleted}).map { event -> TemporaryScheduleOverride in
  179. var override = event.override
  180. if case .early(let endDate) = event.override.actualEnd {
  181. override.scheduledEndDate = endDate
  182. }
  183. return override
  184. }
  185. let period = relevantPeriod(relativeTo: referenceDate)
  186. overrides.mutateEach { override in
  187. // Save the actual (computed) end date prior to modifying the start date, which shifts the whole interval
  188. let end = override.scheduledEndDate
  189. override.startDate = max(override.startDate, period.start)
  190. override.scheduledEndDate = min(end, period.end)
  191. }
  192. validateOverridesReflectingEnabledDuration(overrides)
  193. return overrides
  194. }
  195. private func validateOverridesReflectingEnabledDuration(_ overrides: [TemporaryScheduleOverride]) {
  196. let overlappingOverridePairIndices: [(Int, Int)] =
  197. Array(overrides.enumerated())
  198. .allPairs()
  199. .compactMap {
  200. let ((index1, override1), (index2, override2)) = ($0, $1)
  201. if override1.activeInterval.intersects(override2.activeInterval) {
  202. return (index1, index2)
  203. } else {
  204. return nil
  205. }
  206. }
  207. let invalidOverrideIndices = overlappingOverridePairIndices.flatMap { [$0, $1] }
  208. guard invalidOverrideIndices.isEmpty else {
  209. // Save the invalid event history for debugging.
  210. taintedEventLog = recentEvents
  211. // Wipe only conflicting overrides to retain as much history as possible.
  212. recentEvents.removeAll(at: invalidOverrideIndices)
  213. // Crash deliberately to notify something has gone wrong.
  214. preconditionFailure("No overrides should overlap.")
  215. }
  216. }
  217. func wipeHistory() {
  218. recentEvents.removeAll()
  219. modificationCounter = 0
  220. }
  221. public func queryByAnchor(_ anchor: QueryAnchor?) -> (resultOverrides: [TemporaryScheduleOverride], deletedOverrides: [TemporaryScheduleOverride], newAnchor: QueryAnchor?) {
  222. var resultOverrides = [TemporaryScheduleOverride]()
  223. var deletedOverrides = [TemporaryScheduleOverride]()
  224. for event in recentEvents {
  225. if anchor == nil || event.modificationCounter >= anchor! {
  226. var override = event.override
  227. if case .early(let endDate) = event.override.actualEnd {
  228. override.scheduledEndDate = endDate
  229. }
  230. if event.override.actualEnd == .deleted {
  231. deletedOverrides.append(override)
  232. } else {
  233. resultOverrides.append(override)
  234. }
  235. }
  236. }
  237. return (resultOverrides: resultOverrides, deletedOverrides: deletedOverrides, newAnchor: modificationCounter)
  238. }
  239. }
  240. extension OverrideEvent: RawRepresentable {
  241. typealias RawValue = [String: Any]
  242. init?(rawValue: RawValue) {
  243. guard
  244. let overrideRawValue = rawValue["override"] as? TemporaryScheduleOverride.RawValue,
  245. let override = TemporaryScheduleOverride(rawValue: overrideRawValue)
  246. else {
  247. return nil
  248. }
  249. self.override = override
  250. self.modificationCounter = rawValue["modificationCounter"] as? Int ?? 0
  251. if let isDeleted = rawValue["isDeleted"] as? Bool, isDeleted {
  252. self.override.actualEnd = .deleted
  253. } else if let endDate = rawValue["endDate"] as? Date {
  254. self.override.actualEnd = .early(endDate)
  255. }
  256. }
  257. var rawValue: RawValue {
  258. var raw: RawValue = [
  259. "override": override.rawValue,
  260. "modificationCounter": modificationCounter,
  261. "isDeleted": override.actualEnd == .deleted,
  262. ]
  263. if case .early(let endDate) = override.actualEnd {
  264. raw["endDate"] = endDate
  265. }
  266. return raw
  267. }
  268. }
  269. extension TemporaryScheduleOverrideHistory: RawRepresentable {
  270. public typealias RawValue = [String: Any]
  271. public convenience init?(rawValue: RawValue) {
  272. self.init()
  273. if let recentEventsRawValue = rawValue["recentEvents"] as? [[String: Any]] {
  274. let recentEvents = recentEventsRawValue.compactMap(OverrideEvent.init(rawValue:))
  275. guard recentEvents.count == recentEventsRawValue.count else {
  276. return nil
  277. }
  278. self.recentEvents = recentEvents
  279. }
  280. if let taintedEventsRawValue = rawValue["taintedEventLog"] as? [[String: Any]] {
  281. let taintedEventLog = taintedEventsRawValue.compactMap(OverrideEvent.init(rawValue:))
  282. guard taintedEventLog.count == taintedEventsRawValue.count else {
  283. return nil
  284. }
  285. self.taintedEventLog = taintedEventLog
  286. }
  287. self.modificationCounter = rawValue["modificationCounter"] as? Int ?? 0
  288. }
  289. public var rawValue: RawValue {
  290. return [
  291. "recentEvents": recentEvents.map { $0.rawValue },
  292. "taintedEventLog": taintedEventLog.map { $0.rawValue },
  293. "modificationCounter": modificationCounter
  294. ]
  295. }
  296. }
  297. extension TemporaryScheduleOverrideHistory: CustomDebugStringConvertible {
  298. public var debugDescription: String {
  299. return "TemporaryScheduleOverrideHistory(recentEvents: \(recentEvents), taintedEventLog: \(taintedEventLog))"
  300. }
  301. }
  302. private extension Date {
  303. var nearestPrevious: Date {
  304. return Date(timeIntervalSince1970: timeIntervalSince1970.nextDown)
  305. }
  306. }