GlucoseMathTests.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. //
  2. // GlucoseMathTests.swift
  3. // GlucoseKitTests
  4. //
  5. // Created by Nathan Racklyeft on 1/24/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import XCTest
  9. @testable import LoopKit
  10. import HealthKit
  11. public struct GlucoseFixtureValue: GlucoseSampleValue {
  12. public let startDate: Date
  13. public let quantity: HKQuantity
  14. public let isDisplayOnly: Bool
  15. public let wasUserEntered: Bool
  16. public let provenanceIdentifier: String
  17. public let condition: GlucoseCondition?
  18. public let trendRate: HKQuantity?
  19. public var trend: LoopKit.GlucoseTrend?
  20. public var syncIdentifier: String?
  21. public init(startDate: Date, quantity: HKQuantity, isDisplayOnly: Bool, wasUserEntered: Bool, provenanceIdentifier: String?, condition: GlucoseCondition?, trendRate: HKQuantity?) {
  22. self.startDate = startDate
  23. self.quantity = quantity
  24. self.isDisplayOnly = isDisplayOnly
  25. self.wasUserEntered = wasUserEntered
  26. self.provenanceIdentifier = provenanceIdentifier ?? "com.loopkit.LoopKitTests"
  27. self.condition = condition
  28. self.trendRate = trendRate
  29. }
  30. }
  31. extension GlucoseFixtureValue: Comparable {
  32. public static func <(lhs: GlucoseFixtureValue, rhs: GlucoseFixtureValue) -> Bool {
  33. return lhs.startDate < rhs.startDate
  34. }
  35. public static func ==(lhs: GlucoseFixtureValue, rhs: GlucoseFixtureValue) -> Bool {
  36. return lhs.startDate == rhs.startDate &&
  37. lhs.quantity == rhs.quantity &&
  38. lhs.isDisplayOnly == rhs.isDisplayOnly &&
  39. lhs.wasUserEntered == rhs.wasUserEntered &&
  40. lhs.provenanceIdentifier == rhs.provenanceIdentifier &&
  41. lhs.condition == rhs.condition &&
  42. lhs.trendRate == rhs.trendRate
  43. }
  44. }
  45. class GlucoseMathTests: XCTestCase {
  46. private func printFixture(_ effectVelocity: [GlucoseEffectVelocity]) {
  47. let formatter = ISO8601DateFormatter.localTimeDate()
  48. let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())
  49. print("\n\n")
  50. print(String(data: try! JSONSerialization.data(
  51. withJSONObject: effectVelocity.map({ (value) -> [String: Any] in
  52. return [
  53. "startDate": formatter.string(from: value.startDate),
  54. "endDate": formatter.string(from: value.endDate),
  55. "value": value.quantity.doubleValue(for: unit),
  56. "unit": unit.unitString
  57. ]
  58. }),
  59. options: .prettyPrinted), encoding: .utf8)!)
  60. print("\n\n")
  61. }
  62. func loadInputFixture(_ resourceName: String) -> [GlucoseFixtureValue] {
  63. let fixture: [JSONDictionary] = loadFixture(resourceName)
  64. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  65. return fixture.map {
  66. return GlucoseFixtureValue(
  67. startDate: dateFormatter.date(from: $0["date"] as! String)!,
  68. quantity: HKQuantity(unit: HKUnit.milligramsPerDeciliter, doubleValue: $0["amount"] as! Double),
  69. isDisplayOnly: ($0["display_only"] as? Bool) ?? false,
  70. wasUserEntered: ($0["user_entered"] as? Bool) ?? false,
  71. provenanceIdentifier: $0["provenance_identifier"] as? String,
  72. condition: ($0["condition"] as? String).flatMap { GlucoseCondition(rawValue: $0) },
  73. trendRate: ($0["trend_rate"] as? Double).flatMap { HKQuantity(unit: .milligramsPerDeciliter, doubleValue: $0) }
  74. )
  75. }
  76. }
  77. func loadOutputFixture(_ resourceName: String) -> [GlucoseEffect] {
  78. let fixture: [JSONDictionary] = loadFixture(resourceName)
  79. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  80. return fixture.map {
  81. return GlucoseEffect(startDate: dateFormatter.date(from: $0["date"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue: $0["amount"] as! Double))
  82. }
  83. }
  84. func loadEffectVelocityFixture(_ resourceName: String) -> [GlucoseEffectVelocity] {
  85. let fixture: [JSONDictionary] = loadFixture(resourceName)
  86. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  87. return fixture.map {
  88. return GlucoseEffectVelocity(startDate: dateFormatter.date(from: $0["startDate"] as! String)!, endDate: dateFormatter.date(from: $0["endDate"] as! String)!, quantity: HKQuantity(unit: HKUnit(from: $0["unit"] as! String), doubleValue:$0["value"] as! Double))
  89. }
  90. }
  91. func testMomentumEffectForBouncingGlucose() {
  92. let input = loadInputFixture("momentum_effect_bouncing_glucose_input")
  93. let output = loadOutputFixture("momentum_effect_bouncing_glucose_output")
  94. let effects = input.linearMomentumEffect(duration: .minutes(30))
  95. let unit = HKUnit.milligramsPerDeciliter
  96. XCTAssertEqual(output.count, effects.count)
  97. for (expected, calculated) in zip(output, effects) {
  98. XCTAssertEqual(expected.startDate, calculated.startDate)
  99. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  100. }
  101. }
  102. func testMomentumEffectForRisingGlucose() {
  103. let input = loadInputFixture("momentum_effect_rising_glucose_input")
  104. let output = loadOutputFixture("momentum_effect_rising_glucose_output")
  105. let effects = input.linearMomentumEffect(duration: .minutes(30))
  106. let unit = HKUnit.milligramsPerDeciliter
  107. XCTAssertEqual(output.count, effects.count)
  108. for (expected, calculated) in zip(output, effects) {
  109. XCTAssertEqual(expected.startDate, calculated.startDate)
  110. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  111. }
  112. }
  113. func testMomentumEffectForRisingGlucoseDoubles() {
  114. let input = loadInputFixture("momentum_effect_rising_glucose_double_entries_input")
  115. let output = loadOutputFixture("momentum_effect_rising_glucose_output")
  116. let effects = input.linearMomentumEffect(duration: .minutes(30))
  117. let unit = HKUnit.milligramsPerDeciliter
  118. XCTAssertEqual(output.count, effects.count)
  119. for (expected, calculated) in zip(output, effects) {
  120. XCTAssertEqual(expected.startDate, calculated.startDate)
  121. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  122. }
  123. }
  124. func testMomentumEffectForFallingGlucose() {
  125. let input = loadInputFixture("momentum_effect_falling_glucose_input")
  126. let output = loadOutputFixture("momentum_effect_falling_glucose_output")
  127. let effects = input.linearMomentumEffect(duration: .minutes(30))
  128. let unit = HKUnit.milligramsPerDeciliter
  129. XCTAssertEqual(output.count, effects.count)
  130. for (expected, calculated) in zip(output, effects) {
  131. XCTAssertEqual(expected.startDate, calculated.startDate)
  132. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  133. }
  134. }
  135. func testMomentumEffectForFallingGlucoseDuplicates() {
  136. var input = loadInputFixture("momentum_effect_falling_glucose_input")
  137. let output = loadOutputFixture("momentum_effect_falling_glucose_output")
  138. input.append(contentsOf: input)
  139. input.sort(by: <)
  140. let effects = input.linearMomentumEffect(duration: .minutes(30))
  141. let unit = HKUnit.milligramsPerDeciliter
  142. XCTAssertEqual(output.count, effects.count)
  143. for (expected, calculated) in zip(output, effects) {
  144. XCTAssertEqual(expected.startDate, calculated.startDate)
  145. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  146. }
  147. }
  148. func testMomentumEffectForStableGlucose() {
  149. let input = loadInputFixture("momentum_effect_stable_glucose_input")
  150. let output = loadOutputFixture("momentum_effect_stable_glucose_output")
  151. let effects = input.linearMomentumEffect(duration: .minutes(30))
  152. let unit = HKUnit.milligramsPerDeciliter
  153. XCTAssertEqual(output.count, effects.count)
  154. for (expected, calculated) in zip(output, effects) {
  155. XCTAssertEqual(expected.startDate, calculated.startDate)
  156. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  157. }
  158. }
  159. func testMomentumEffectForDuplicateGlucose() {
  160. let input = loadInputFixture("momentum_effect_duplicate_glucose_input")
  161. let effects = input.linearMomentumEffect()
  162. XCTAssertEqual(0, effects.count)
  163. }
  164. func testMomentumEffectForEmptyGlucose() {
  165. let input = [GlucoseFixtureValue]()
  166. let effects = input.linearMomentumEffect()
  167. XCTAssertEqual(0, effects.count)
  168. }
  169. func testMomentumEffectForSpacedOutGlucose() {
  170. let input = loadInputFixture("momentum_effect_incomplete_glucose_input")
  171. let effects = input.linearMomentumEffect()
  172. XCTAssertEqual(0, effects.count)
  173. }
  174. func testMomentumEffectForTooFewGlucose() {
  175. let input = loadInputFixture("momentum_effect_bouncing_glucose_input")[0...1]
  176. let effects = input.linearMomentumEffect()
  177. XCTAssertEqual(0, effects.count)
  178. }
  179. func testMomentumEffectForDisplayOnlyGlucose() {
  180. let input = loadInputFixture("momentum_effect_display_only_glucose_input")
  181. let effects = input.linearMomentumEffect()
  182. XCTAssertEqual(0, effects.count)
  183. }
  184. func testMomentumEffectForMixedProvenanceGlucose() {
  185. let input = loadInputFixture("momentum_effect_mixed_provenance_glucose_input")
  186. let effects = input.linearMomentumEffect()
  187. XCTAssertEqual(0, effects.count)
  188. }
  189. func testCounteractionEffectsForFallingGlucose() {
  190. let input = loadInputFixture("counteraction_effect_falling_glucose_input")
  191. let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin")
  192. let output = loadEffectVelocityFixture("counteraction_effect_falling_glucose_output")
  193. let effects = input.counteractionEffects(to: insulinEffect)
  194. let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())
  195. XCTAssertEqual(output.count, effects.count)
  196. for (expected, calculated) in zip(output, effects) {
  197. XCTAssertEqual(expected.startDate, calculated.startDate)
  198. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  199. }
  200. }
  201. func testCounteractionEffectsForFallingGlucoseDuplicates() {
  202. var input = loadInputFixture("counteraction_effect_falling_glucose_input")
  203. input.append(contentsOf: input)
  204. input.sort(by: <)
  205. let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin")
  206. let output = loadEffectVelocityFixture("counteraction_effect_falling_glucose_output")
  207. let effects = input.counteractionEffects(to: insulinEffect)
  208. let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())
  209. XCTAssertEqual(output.count, effects.count)
  210. for (expected, calculated) in zip(output, effects) {
  211. XCTAssertEqual(expected.startDate, calculated.startDate)
  212. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  213. }
  214. }
  215. func testCounteractionEffectsForFallingGlucoseAlmostDuplicates() {
  216. let input = loadInputFixture("counteraction_effect_falling_glucose_almost_duplicates_input")
  217. let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin")
  218. let output = loadEffectVelocityFixture("counteraction_effect_falling_glucose_almost_duplicates_output")
  219. let effects = input.counteractionEffects(to: insulinEffect)
  220. let unit = HKUnit.milligramsPerDeciliter.unitDivided(by: .minute())
  221. XCTAssertEqual(output.count, effects.count)
  222. for (expected, calculated) in zip(output, effects) {
  223. XCTAssertEqual(expected.startDate, calculated.startDate)
  224. XCTAssertEqual(expected.endDate, calculated.endDate)
  225. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  226. }
  227. }
  228. func testCounteractionEffectsForNoGlucose() {
  229. let input = [GlucoseFixtureValue]()
  230. let insulinEffect = loadOutputFixture("counteraction_effect_falling_glucose_insulin")
  231. let output = [GlucoseEffectVelocity]()
  232. let effects = input.counteractionEffects(to: insulinEffect)
  233. XCTAssertEqual(output.count, effects.count)
  234. }
  235. func testMomentumEffectWithVelocityLimit() {
  236. let input = loadInputFixture("momentum_effect_impossible_rising_glucose_input")
  237. let output = loadOutputFixture("momentum_effect_impossible_rising_glucose_output")
  238. let effects = input.linearMomentumEffect(duration: .minutes(30))
  239. let unit = HKUnit.milligramsPerDeciliter
  240. XCTAssertEqual(output.count, effects.count)
  241. for (expected, calculated) in zip(output, effects) {
  242. XCTAssertEqual(expected.startDate, calculated.startDate)
  243. XCTAssertEqual(expected.quantity.doubleValue(for: unit), calculated.quantity.doubleValue(for: unit), accuracy: Double(Float.ulpOfOne))
  244. }
  245. }
  246. }