TemporaryScheduleOverrideHistory.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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: Int64
  49. init(override: TemporaryScheduleOverride, modificationCounter: Int64) {
  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 struct QueryAnchor: RawRepresentable {
  59. public typealias RawValue = [String: Any]
  60. internal var modificationCounter: Int64
  61. public init() {
  62. self.modificationCounter = 0
  63. }
  64. public init?(rawValue: RawValue) {
  65. guard let modificationCounter = rawValue["modificationCounter"] as? Int64 else {
  66. return nil
  67. }
  68. self.modificationCounter = modificationCounter
  69. }
  70. public var rawValue: RawValue {
  71. var rawValue: RawValue = [:]
  72. rawValue["modificationCounter"] = modificationCounter
  73. return rawValue
  74. }
  75. }
  76. private var recentEvents: [OverrideEvent] = [] {
  77. didSet {
  78. modificationCounter += 1
  79. if let lastTaintedEvent = taintedEventLog.last,
  80. Date().timeIntervalSince(lastTaintedEvent.override.startDate) > .hours(48)
  81. {
  82. taintedEventLog.removeAll()
  83. }
  84. }
  85. }
  86. /// Tracks a sequence of override events that failed validation checks.
  87. /// Stored to enable retrieval via issue report after a deliberate crash.
  88. private var taintedEventLog: [OverrideEvent] = []
  89. private var modificationCounter: Int64
  90. public var relevantTimeWindow: TimeInterval = TimeInterval.hours(10)
  91. public weak var delegate: TemporaryScheduleOverrideHistoryDelegate?
  92. public init() {
  93. modificationCounter = 0
  94. }
  95. public func recordOverride(_ override: TemporaryScheduleOverride?, at enableDate: Date = Date()) {
  96. guard override != lastUndeletedEvent?.override else {
  97. return
  98. }
  99. if let override = override {
  100. record(override, at: enableDate)
  101. } else {
  102. cancelActiveOverride(at: enableDate)
  103. }
  104. delegate?.temporaryScheduleOverrideHistoryDidUpdate(self)
  105. }
  106. private var lastUndeletedEvent: OverrideEvent? {
  107. return recentEvents.reversed().first { $0.override.actualEnd != .deleted }
  108. }
  109. private func deleteEventsStartingOnOrAfter(_ date: Date) {
  110. recentEvents.mutateEach { (event) in
  111. if event.override.startDate >= date {
  112. event.override.actualEnd = .deleted
  113. event.modificationCounter = modificationCounter
  114. }
  115. }
  116. }
  117. // Deletes overrides that start after the passed in override.
  118. private func record(_ override: TemporaryScheduleOverride, at enableDate: Date) {
  119. // Check for modification of existing entry
  120. var index = recentEvents.endIndex
  121. while index != recentEvents.startIndex {
  122. recentEvents.formIndex(before: &index)
  123. if recentEvents[index].override.syncIdentifier == override.syncIdentifier {
  124. recentEvents[index].override = override
  125. recentEvents[index].modificationCounter = modificationCounter
  126. return
  127. }
  128. }
  129. deleteEventsStartingOnOrAfter(override.startDate)
  130. let overrideEnd = min(override.startDate.nearestPrevious, enableDate)
  131. cancelActiveOverride(at: overrideEnd)
  132. let enabledEvent = OverrideEvent(override: override, modificationCounter: modificationCounter)
  133. recentEvents.append(enabledEvent)
  134. }
  135. private func cancelActiveOverride(at date: Date) {
  136. var index = recentEvents.endIndex
  137. while index != recentEvents.startIndex {
  138. recentEvents.formIndex(before: &index)
  139. if recentEvents[index].override.actualEnd != .deleted {
  140. if recentEvents[index].override.actualEndDate > date {
  141. if recentEvents[index].override.startDate > date {
  142. recentEvents[index].override.actualEnd = .deleted
  143. } else {
  144. recentEvents[index].override.actualEnd = .early(date)
  145. }
  146. recentEvents[index].modificationCounter = modificationCounter
  147. }
  148. break
  149. }
  150. }
  151. }
  152. public func resolvingRecentBasalSchedule(_ base: BasalRateSchedule, relativeTo referenceDate: Date = Date()) -> BasalRateSchedule {
  153. filterRecentEvents(relativeTo: referenceDate)
  154. return overridesReflectingEnabledDuration(relativeTo: referenceDate).reduce(base) { base, override in
  155. base.applyingBasalRateMultiplier(from: override, relativeTo: referenceDate)
  156. }
  157. }
  158. public func resolvingRecentInsulinSensitivitySchedule(_ base: InsulinSensitivitySchedule, relativeTo referenceDate: Date = Date()) -> InsulinSensitivitySchedule {
  159. filterRecentEvents(relativeTo: referenceDate)
  160. return overridesReflectingEnabledDuration(relativeTo: referenceDate).reduce(base) { base, override in
  161. base.applyingSensitivityMultiplier(from: override, relativeTo: referenceDate)
  162. }
  163. }
  164. public func resolvingRecentCarbRatioSchedule(_ base: CarbRatioSchedule, relativeTo referenceDate: Date = Date()) -> CarbRatioSchedule {
  165. filterRecentEvents(relativeTo: referenceDate)
  166. return overridesReflectingEnabledDuration(relativeTo: referenceDate).reduce(base) { base, override in
  167. base.applyingCarbRatioMultiplier(from: override, relativeTo: referenceDate)
  168. }
  169. }
  170. public func getEvents(relativeTo referenceDate: Date = Date()) -> [TemporaryScheduleOverride] {
  171. filterRecentEvents(relativeTo: referenceDate)
  172. return recentEvents.map { $0.override }
  173. }
  174. private func relevantPeriod(relativeTo referenceDate: Date) -> DateInterval {
  175. return DateInterval(
  176. start: referenceDate.addingTimeInterval(-relevantTimeWindow),
  177. end: referenceDate.addingTimeInterval(relevantTimeWindow)
  178. )
  179. }
  180. private func filterRecentEvents(relativeTo referenceDate: Date) {
  181. let period = relevantPeriod(relativeTo: referenceDate)
  182. var recentEvents = self.recentEvents
  183. recentEvents.removeAll(where: { event in
  184. event.override.actualEndDate < period.start || event.override.startDate > period.end
  185. })
  186. if recentEvents != self.recentEvents {
  187. self.recentEvents = recentEvents
  188. }
  189. }
  190. private func overridesReflectingEnabledDuration(relativeTo referenceDate: Date) -> [TemporaryScheduleOverride] {
  191. var overrides = recentEvents.filter({$0.override.actualEnd != .deleted}).map { event -> TemporaryScheduleOverride in
  192. var override = event.override
  193. if case .early(let endDate) = event.override.actualEnd {
  194. override.scheduledEndDate = endDate
  195. }
  196. return override
  197. }
  198. let period = relevantPeriod(relativeTo: referenceDate)
  199. overrides.mutateEach { override in
  200. // Save the actual (computed) end date prior to modifying the start date, which shifts the whole interval
  201. let end = override.scheduledEndDate
  202. override.startDate = max(override.startDate, period.start)
  203. override.scheduledEndDate = min(end, period.end)
  204. }
  205. validateOverridesReflectingEnabledDuration(overrides)
  206. return overrides
  207. }
  208. private func validateOverridesReflectingEnabledDuration(_ overrides: [TemporaryScheduleOverride]) {
  209. let overlappingOverridePairIndices: [(Int, Int)] =
  210. Array(overrides.enumerated())
  211. .allPairs()
  212. .compactMap {
  213. let ((index1, override1), (index2, override2)) = ($0, $1)
  214. if override1.activeInterval.intersects(override2.activeInterval) {
  215. return (index1, index2)
  216. } else {
  217. return nil
  218. }
  219. }
  220. let invalidOverrideIndices = overlappingOverridePairIndices.flatMap { [$0, $1] }
  221. guard invalidOverrideIndices.isEmpty else {
  222. // Save the invalid event history for debugging.
  223. taintedEventLog = recentEvents
  224. // Wipe only conflicting overrides to retain as much history as possible.
  225. recentEvents.removeAll(at: invalidOverrideIndices)
  226. // Store the history without the conflicting overrides
  227. delegate?.temporaryScheduleOverrideHistoryDidUpdate(self)
  228. // Crash deliberately to notify something has gone wrong.
  229. preconditionFailure("No overrides should overlap.")
  230. }
  231. }
  232. func wipeHistory() {
  233. recentEvents.removeAll()
  234. modificationCounter = 0
  235. }
  236. public func queryByAnchor(_ anchor: QueryAnchor?) -> (resultOverrides: [TemporaryScheduleOverride], deletedOverrides: [TemporaryScheduleOverride], newAnchor: QueryAnchor) {
  237. var resultOverrides = [TemporaryScheduleOverride]()
  238. var deletedOverrides = [TemporaryScheduleOverride]()
  239. for event in recentEvents {
  240. if anchor == nil || event.modificationCounter >= anchor!.modificationCounter {
  241. var override = event.override
  242. if case .early(let endDate) = event.override.actualEnd {
  243. override.scheduledEndDate = endDate
  244. }
  245. if event.override.actualEnd == .deleted {
  246. deletedOverrides.append(override)
  247. } else {
  248. resultOverrides.append(override)
  249. }
  250. }
  251. }
  252. var newAnchor = QueryAnchor()
  253. newAnchor.modificationCounter = modificationCounter
  254. return (resultOverrides: resultOverrides, deletedOverrides: deletedOverrides, newAnchor: newAnchor)
  255. }
  256. }
  257. extension OverrideEvent: RawRepresentable {
  258. typealias RawValue = [String: Any]
  259. init?(rawValue: RawValue) {
  260. guard
  261. let overrideRawValue = rawValue["override"] as? TemporaryScheduleOverride.RawValue,
  262. let override = TemporaryScheduleOverride(rawValue: overrideRawValue)
  263. else {
  264. return nil
  265. }
  266. self.override = override
  267. self.modificationCounter = rawValue["modificationCounter"] as? Int64 ?? 0
  268. if let isDeleted = rawValue["isDeleted"] as? Bool, isDeleted {
  269. self.override.actualEnd = .deleted
  270. } else if let endDate = rawValue["endDate"] as? Date {
  271. self.override.actualEnd = .early(endDate)
  272. }
  273. }
  274. var rawValue: RawValue {
  275. var raw: RawValue = [
  276. "override": override.rawValue,
  277. "modificationCounter": modificationCounter,
  278. "isDeleted": override.actualEnd == .deleted,
  279. ]
  280. if case .early(let endDate) = override.actualEnd {
  281. raw["endDate"] = endDate
  282. }
  283. return raw
  284. }
  285. }
  286. extension TemporaryScheduleOverrideHistory: RawRepresentable {
  287. public typealias RawValue = [String: Any]
  288. public convenience init?(rawValue: RawValue) {
  289. self.init()
  290. if let recentEventsRawValue = rawValue["recentEvents"] as? [[String: Any]] {
  291. let recentEvents = recentEventsRawValue.compactMap(OverrideEvent.init(rawValue:))
  292. guard recentEvents.count == recentEventsRawValue.count else {
  293. return nil
  294. }
  295. self.recentEvents = recentEvents
  296. }
  297. if let taintedEventsRawValue = rawValue["taintedEventLog"] as? [[String: Any]] {
  298. let taintedEventLog = taintedEventsRawValue.compactMap(OverrideEvent.init(rawValue:))
  299. guard taintedEventLog.count == taintedEventsRawValue.count else {
  300. return nil
  301. }
  302. self.taintedEventLog = taintedEventLog
  303. }
  304. self.modificationCounter = rawValue["modificationCounter"] as? Int64 ?? 0
  305. }
  306. public var rawValue: RawValue {
  307. return [
  308. "recentEvents": recentEvents.map { $0.rawValue },
  309. "taintedEventLog": taintedEventLog.map { $0.rawValue },
  310. "modificationCounter": modificationCounter
  311. ]
  312. }
  313. }
  314. extension TemporaryScheduleOverrideHistory: CustomDebugStringConvertible {
  315. public var debugDescription: String {
  316. return "TemporaryScheduleOverrideHistory(recentEvents: \(recentEvents), taintedEventLog: \(taintedEventLog))"
  317. }
  318. }
  319. private extension Date {
  320. var nearestPrevious: Date {
  321. return Date(timeIntervalSince1970: timeIntervalSince1970.nextDown)
  322. }
  323. }