TemporaryScheduleOverrideTests.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. //
  2. // TemporaryScheduleOverrideTests.swift
  3. // LoopKitTests
  4. //
  5. // Created by Michael Pangburn on 1/2/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import XCTest
  9. @testable import LoopKit
  10. class TemporaryScheduleOverrideTests: XCTestCase {
  11. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  12. let epsilon = 1e-6
  13. let basalRateSchedule = BasalRateSchedule(dailyItems: [
  14. RepeatingScheduleValue(startTime: .hours(0), value: 1.2),
  15. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  16. RepeatingScheduleValue(startTime: .hours(20), value: 1.0)
  17. ])!
  18. private func date(at time: String) -> Date {
  19. return dateFormatter.date(from: "2019-01-01T\(time):00")!
  20. }
  21. private func basalUpOverride(start: String, end: String) -> TemporaryScheduleOverride {
  22. return TemporaryScheduleOverride(
  23. context: .custom,
  24. settings: TemporaryScheduleOverrideSettings(
  25. unit: .milligramsPerDeciliter,
  26. targetRange: nil,
  27. insulinNeedsScaleFactor: 1.5
  28. ),
  29. startDate: date(at: start),
  30. duration: .finite(date(at: end).timeIntervalSince(date(at: start))),
  31. enactTrigger: .local,
  32. syncIdentifier: UUID()
  33. )
  34. }
  35. private func applyingActiveBasalOverride(from start: String, to end: String, on schedule: BasalRateSchedule, referenceDate: Date? = nil) -> BasalRateSchedule {
  36. let override = basalUpOverride(start: start, end: end)
  37. let referenceDate = referenceDate ?? override.startDate
  38. return schedule.applyingBasalRateMultiplier(from: override, relativeTo: referenceDate)
  39. }
  40. // Override start aligns with schedule item start
  41. func testBasalRateScheduleOverrideStartTimeMatch() {
  42. let overrideBasalSchedule = applyingActiveBasalOverride(from: "00:00", to: "01:00", on: basalRateSchedule)
  43. let expected = BasalRateSchedule(dailyItems: [
  44. RepeatingScheduleValue(startTime: .hours(0), value: 1.8),
  45. RepeatingScheduleValue(startTime: .hours(1), value: 1.2),
  46. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  47. RepeatingScheduleValue(startTime: .hours(20), value: 1.0)
  48. ])!
  49. XCTAssert(overrideBasalSchedule.equals(expected, accuracy: epsilon))
  50. }
  51. // Override contained fully within a schedule item
  52. func testBasalRateScheduleOverrideContained() {
  53. let overridden = applyingActiveBasalOverride(from: "02:00", to: "04:00", on: basalRateSchedule)
  54. let expected = BasalRateSchedule(dailyItems: [
  55. RepeatingScheduleValue(startTime: .hours(0), value: 1.2),
  56. RepeatingScheduleValue(startTime: .hours(2), value: 1.8),
  57. RepeatingScheduleValue(startTime: .hours(4), value: 1.2),
  58. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  59. RepeatingScheduleValue(startTime: .hours(20), value: 1.0)
  60. ])!
  61. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  62. }
  63. // Override end aligns with schedule item start
  64. func testBasalRateScheduleOverrideEndTimeMatch() {
  65. let overridden = applyingActiveBasalOverride(from: "02:00", to: "06:00", on: basalRateSchedule)
  66. let expected = BasalRateSchedule(dailyItems: [
  67. RepeatingScheduleValue(startTime: .hours(0), value: 1.2),
  68. RepeatingScheduleValue(startTime: .hours(2), value: 1.8),
  69. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  70. RepeatingScheduleValue(startTime: .hours(20), value: 1.0)
  71. ])!
  72. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  73. }
  74. // Override completely encapsulates schedule item
  75. func testBasalRateScheduleOverrideEncapsulate() {
  76. let overridden = applyingActiveBasalOverride(from: "02:00", to: "22:00", on: basalRateSchedule)
  77. let expected = BasalRateSchedule(dailyItems: [
  78. RepeatingScheduleValue(startTime: .hours(0), value: 1.2),
  79. RepeatingScheduleValue(startTime: .hours(2), value: 1.8),
  80. RepeatingScheduleValue(startTime: .hours(6), value: 2.1),
  81. RepeatingScheduleValue(startTime: .hours(20), value: 1.5),
  82. RepeatingScheduleValue(startTime: .hours(22), value: 1.0),
  83. ])!
  84. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  85. }
  86. func testSingleBasalRateSchedule() {
  87. let basalRateSchedule = BasalRateSchedule(dailyItems: [
  88. RepeatingScheduleValue(startTime: .hours(0), value: 1.0)
  89. ])!
  90. let overridden = applyingActiveBasalOverride(from: "08:00", to: "12:00", on: basalRateSchedule)
  91. let expected = BasalRateSchedule(dailyItems: [
  92. RepeatingScheduleValue(startTime: .hours(0), value: 1.0),
  93. RepeatingScheduleValue(startTime: .hours(8), value: 1.5),
  94. RepeatingScheduleValue(startTime: .hours(12), value: 1.0)
  95. ])!
  96. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  97. }
  98. func testOverrideCrossingMidnight() {
  99. var override = basalUpOverride(start: "22:00", end: "23:00")
  100. override.duration += .hours(5) // override goes from 10pm to 4am of the next day
  101. let overridden = basalRateSchedule.applyingBasalRateMultiplier(from: override, relativeTo: date(at: "22:00"))
  102. let expected = BasalRateSchedule(dailyItems: [
  103. RepeatingScheduleValue(startTime: .hours(0), value: 1.8),
  104. RepeatingScheduleValue(startTime: .hours(4), value: 1.2),
  105. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  106. RepeatingScheduleValue(startTime: .hours(20), value: 1.0),
  107. RepeatingScheduleValue(startTime: .hours(22), value: 1.5)
  108. ])!
  109. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  110. }
  111. func testMultiDayOverride() {
  112. var override = basalUpOverride(start: "02:00", end: "22:00")
  113. override.duration += .hours(48) // override goes from 2am until 10pm two days later
  114. let overridden = basalRateSchedule.applyingBasalRateMultiplier(
  115. from: override,
  116. relativeTo: date(at: "02:00") + .hours(24)
  117. )
  118. // expect full schedule override; start/end dates are too distant to have an effect
  119. let expected = BasalRateSchedule(dailyItems: [
  120. RepeatingScheduleValue(startTime: .hours(0), value: 1.8),
  121. RepeatingScheduleValue(startTime: .hours(6), value: 2.1),
  122. RepeatingScheduleValue(startTime: .hours(20), value: 1.5)
  123. ])!
  124. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  125. }
  126. func testOutdatedOverride() {
  127. let overridden = applyingActiveBasalOverride(from: "02:00", to: "04:00", on: basalRateSchedule,
  128. referenceDate: date(at: "12:00").addingTimeInterval(.hours(24)))
  129. let expected = basalRateSchedule
  130. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  131. }
  132. func testFarFutureOverride() {
  133. let overridden = applyingActiveBasalOverride(from: "10:00", to: "12:00", on: basalRateSchedule,
  134. referenceDate: date(at: "02:00").addingTimeInterval(-.hours(24)))
  135. let expected = basalRateSchedule
  136. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  137. }
  138. func testIndefiniteOverride() {
  139. var override = basalUpOverride(start: "02:00", end: "22:00")
  140. override.duration = .indefinite
  141. let overridden = basalRateSchedule.applyingBasalRateMultiplier(from: override, relativeTo: date(at: "02:00"))
  142. // expect full schedule overridden
  143. let expected = BasalRateSchedule(dailyItems: [
  144. RepeatingScheduleValue(startTime: .hours(0), value: 1.8),
  145. RepeatingScheduleValue(startTime: .hours(6), value: 2.1),
  146. RepeatingScheduleValue(startTime: .hours(20), value: 1.5)
  147. ])!
  148. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  149. }
  150. func testOverrideScheduleAnnotatingReservoirSplitsDose() {
  151. let schedule = BasalRateSchedule(dailyItems: [
  152. RepeatingScheduleValue(startTime: 0, value: 0.225),
  153. RepeatingScheduleValue(startTime: 3600.0, value: 0.18000000000000002),
  154. RepeatingScheduleValue(startTime: 10800.0, value: 0.135),
  155. RepeatingScheduleValue(startTime: 12689.855275034904, value: 0.15),
  156. RepeatingScheduleValue(startTime: 21600.0, value: 0.2),
  157. RepeatingScheduleValue(startTime: 32400.0, value: 0.2),
  158. RepeatingScheduleValue(startTime: 50400.0, value: 0.2),
  159. RepeatingScheduleValue(startTime: 52403.79680299759, value: 0.16000000000000003),
  160. RepeatingScheduleValue(startTime: 63743.58014559746, value: 0.2),
  161. RepeatingScheduleValue(startTime: 63743.58014583588, value: 0.16000000000000003),
  162. RepeatingScheduleValue(startTime: 69968.05249071121, value: 0.2),
  163. RepeatingScheduleValue(startTime: 69968.05249094963, value: 0.18000000000000002),
  164. RepeatingScheduleValue(startTime: 79200.0, value: 0.225),
  165. ])!
  166. let dose = DoseEntry(
  167. type: .tempBasal,
  168. startDate: date(at: "19:25"),
  169. endDate: date(at: "19:30"),
  170. value: 0.8,
  171. unit: .units
  172. )
  173. let annotated = [dose].annotated(with: schedule)
  174. XCTAssertEqual(3, annotated.count)
  175. XCTAssertEqual(dose.programmedUnits, annotated.map { $0.unitsInDeliverableIncrements }.reduce(0, +))
  176. }
  177. // MARK: - Target range tests
  178. func testActiveTargetRangeOverride() {
  179. let overrideRange = DoubleRange(minValue: 120, maxValue: 140)
  180. let overrideStart = Date()
  181. let overrideDuration = TimeInterval(hours: 4)
  182. let settings = TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter, targetRange: overrideRange)
  183. let override = TemporaryScheduleOverride(context: .custom, settings: settings, startDate: overrideStart, duration: .finite(overrideDuration), enactTrigger: .local, syncIdentifier: UUID())
  184. let normalRange = DoubleRange(minValue: 95, maxValue: 105)
  185. let rangeSchedule = GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0, value: normalRange)])!.applyingOverride(override)
  186. XCTAssertEqual(rangeSchedule.value(at: overrideStart), overrideRange)
  187. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration / 2), overrideRange)
  188. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration), overrideRange)
  189. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration + .hours(2)), overrideRange)
  190. }
  191. func testFutureTargetRangeOverride() {
  192. let overrideRange = DoubleRange(minValue: 120, maxValue: 140)
  193. let overrideStart = Date() + .hours(2)
  194. let overrideDuration = TimeInterval(hours: 4)
  195. let settings = TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter, targetRange: overrideRange)
  196. let futureOverride = TemporaryScheduleOverride(context: .custom, settings: settings, startDate: overrideStart, duration: .finite(overrideDuration), enactTrigger: .local, syncIdentifier: UUID())
  197. let normalRange = DoubleRange(minValue: 95, maxValue: 105)
  198. let rangeSchedule = GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0, value: normalRange)])!.applyingOverride(futureOverride)
  199. XCTAssertEqual(rangeSchedule.value(at: overrideStart + .minutes(-5)), normalRange)
  200. XCTAssertEqual(rangeSchedule.value(at: overrideStart), overrideRange)
  201. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration), overrideRange)
  202. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration + .hours(2)), overrideRange)
  203. }
  204. }
  205. private extension TemporaryScheduleOverride.Duration {
  206. static func += (lhs: inout TemporaryScheduleOverride.Duration, rhs: TimeInterval) {
  207. switch lhs {
  208. case .finite(let interval):
  209. lhs = .finite(interval + rhs)
  210. case .indefinite:
  211. return
  212. }
  213. }
  214. }