GlucoseMathTests.swift 13 KB

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