MockGlucoseProvider.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. //
  2. // MockGlucoseProvider.swift
  3. // LoopKit
  4. //
  5. // Created by Michael Pangburn on 11/23/18.
  6. // Copyright © 2018 LoopKit Authors. All rights reserved.
  7. //
  8. import HealthKit
  9. import LoopKit
  10. /// Returns a value based on the result of a random coin flip.
  11. /// - Parameter chanceOfHeads: The chance of flipping heads. Must be a value in the range `0...1`. Defaults to `0.5`.
  12. /// - Parameter valueIfHeads: An autoclosure producing the value to return if the coin flips heads.
  13. /// - Parameter valueIfTails: An autoclosure producing the value to return if the coin flips tails.
  14. private func coinFlip<Output>(
  15. withChanceOfHeads chanceOfHeads: Double = 0.5,
  16. ifHeads valueIfHeads: @autoclosure () -> Output,
  17. ifTails valueIfTails: @autoclosure () -> Output
  18. ) -> Output {
  19. precondition((0...1).contains(chanceOfHeads))
  20. let isHeads = .random(in: 0..<100) < chanceOfHeads * 100
  21. return isHeads ? valueIfHeads() : valueIfTails()
  22. }
  23. struct MockGlucoseProvider {
  24. struct BackfillRequest {
  25. let duration: TimeInterval
  26. let dataPointFrequency: TimeInterval
  27. var dataPointCount: Int {
  28. return Int(duration / dataPointFrequency)
  29. }
  30. init(datingBack duration: TimeInterval, dataPointFrequency: TimeInterval) {
  31. self.duration = duration
  32. self.dataPointFrequency = dataPointFrequency
  33. }
  34. }
  35. /// Given a date, asynchronously produce the CGMReadingResult at that date.
  36. private let fetchDataAt: (_ date: Date, _ completion: @escaping (CGMReadingResult) -> Void) -> Void
  37. func fetchData(at date: Date, completion: @escaping (CGMReadingResult) -> Void) {
  38. fetchDataAt(date, completion)
  39. }
  40. func backfill(_ backfill: BackfillRequest, endingAt date: Date, completion: @escaping (CGMReadingResult) -> Void) {
  41. let dataPointDates = (0...backfill.dataPointCount).map { offset in
  42. return date.addingTimeInterval(-backfill.dataPointFrequency * Double(offset))
  43. }
  44. dataPointDates.asyncMap(fetchDataAt) { allResults in
  45. let allSamples = allResults.flatMap { result -> [NewGlucoseSample] in
  46. if case .newData(let samples) = result {
  47. return samples
  48. } else {
  49. return []
  50. }
  51. }
  52. let result: CGMReadingResult = allSamples.isEmpty ? .noData : .newData(allSamples)
  53. completion(result)
  54. }
  55. }
  56. }
  57. extension MockGlucoseProvider {
  58. init(model: MockCGMDataSource.Model, effects: MockCGMDataSource.Effects) {
  59. self = effects.transformations.reduce(model.glucoseProvider) { model, transform in transform(model) }
  60. }
  61. private static func glucoseSample(at date: Date, quantity: HKQuantity) -> NewGlucoseSample {
  62. return NewGlucoseSample(
  63. date: date,
  64. quantity: quantity,
  65. isDisplayOnly: false,
  66. wasUserEntered: false,
  67. syncIdentifier: UUID().uuidString,
  68. device: MockCGMDataSource.device
  69. )
  70. }
  71. }
  72. // MARK: - Models
  73. extension MockGlucoseProvider {
  74. fileprivate static func constant(_ quantity: HKQuantity) -> MockGlucoseProvider {
  75. return MockGlucoseProvider { date, completion in
  76. let sample = glucoseSample(at: date, quantity: quantity)
  77. completion(.newData([sample]))
  78. }
  79. }
  80. fileprivate static func sineCurve(parameters: MockCGMDataSource.Model.SineCurveParameters) -> MockGlucoseProvider {
  81. let (baseGlucose, amplitude, period, referenceDate) = parameters
  82. precondition(period > 0)
  83. let unit = HKUnit.milligramsPerDeciliter
  84. precondition(baseGlucose.is(compatibleWith: unit))
  85. precondition(amplitude.is(compatibleWith: unit))
  86. let baseGlucoseValue = baseGlucose.doubleValue(for: unit)
  87. let amplitudeValue = amplitude.doubleValue(for: unit)
  88. return MockGlucoseProvider { date, completion in
  89. let timeOffset = date.timeIntervalSince1970 - referenceDate.timeIntervalSince1970
  90. let glucoseValue = baseGlucoseValue + amplitudeValue * sin(2 * .pi / period * timeOffset)
  91. let sample = glucoseSample(at: date, quantity: HKQuantity(unit: unit, doubleValue: glucoseValue))
  92. completion(.newData([sample]))
  93. }
  94. }
  95. fileprivate static var noData: MockGlucoseProvider {
  96. return MockGlucoseProvider { _, completion in completion(.noData) }
  97. }
  98. fileprivate static func error(_ error: Error) -> MockGlucoseProvider {
  99. return MockGlucoseProvider { _, completion in completion(.error(error)) }
  100. }
  101. }
  102. // MARK: - Effects
  103. private struct MockGlucoseProviderError: Error { }
  104. extension MockGlucoseProvider {
  105. fileprivate func withRandomNoise(upTo magnitude: HKQuantity) -> MockGlucoseProvider {
  106. let unit = HKUnit.milligramsPerDeciliter
  107. precondition(magnitude.is(compatibleWith: unit))
  108. let magnitude = magnitude.doubleValue(for: unit)
  109. return mapGlucoseQuantities { glucose in
  110. let glucoseValue = glucose.doubleValue(for: unit) + .random(in: -magnitude...magnitude)
  111. return HKQuantity(unit: unit, doubleValue: glucoseValue)
  112. }
  113. }
  114. fileprivate func randomlyProducingLowOutlier(withChance chanceOfOutlier: Double, outlierDelta: HKQuantity) -> MockGlucoseProvider {
  115. return randomlyProducingOutlier(withChance: chanceOfOutlier, outlierDeltaMagnitude: outlierDelta, outlierDeltaSign: -)
  116. }
  117. fileprivate func randomlyProducingHighOutlier(withChance chanceOfOutlier: Double, outlierDelta: HKQuantity) -> MockGlucoseProvider {
  118. return randomlyProducingOutlier(withChance: chanceOfOutlier, outlierDeltaMagnitude: outlierDelta, outlierDeltaSign: +)
  119. }
  120. private func randomlyProducingOutlier(
  121. withChance chanceOfOutlier: Double,
  122. outlierDeltaMagnitude: HKQuantity,
  123. outlierDeltaSign: (Double) -> Double
  124. ) -> MockGlucoseProvider {
  125. let unit = HKUnit.milligramsPerDeciliter
  126. precondition(outlierDeltaMagnitude.is(compatibleWith: unit))
  127. let outlierDelta = outlierDeltaSign(outlierDeltaMagnitude.doubleValue(for: unit))
  128. return mapGlucoseQuantities { glucose in
  129. return coinFlip(
  130. withChanceOfHeads: chanceOfOutlier,
  131. ifHeads: HKQuantity(unit: unit, doubleValue: glucose.doubleValue(for: unit) + outlierDelta),
  132. ifTails: glucose
  133. )
  134. }
  135. }
  136. fileprivate func randomlyErroringOnNewData(withChance chance: Double) -> MockGlucoseProvider {
  137. return mapResult { result in
  138. return coinFlip(withChanceOfHeads: chance, ifHeads: .error(MockGlucoseProviderError()), ifTails: result)
  139. }
  140. }
  141. private func mapResult(_ transform: @escaping (CGMReadingResult) -> CGMReadingResult) -> MockGlucoseProvider {
  142. return MockGlucoseProvider { date, completion in
  143. self.fetchData(at: date) { result in
  144. completion(transform(result))
  145. }
  146. }
  147. }
  148. private func mapGlucoseQuantities(_ transform: @escaping (HKQuantity) -> HKQuantity) -> MockGlucoseProvider {
  149. return mapResult { result in
  150. return result.mapGlucoseQuantities(transform)
  151. }
  152. }
  153. }
  154. private extension CGMReadingResult {
  155. func mapGlucoseQuantities(_ transform: (HKQuantity) -> HKQuantity) -> CGMReadingResult {
  156. guard case .newData(let samples) = self else {
  157. return self
  158. }
  159. return .newData(
  160. samples.map { sample in
  161. return NewGlucoseSample(
  162. date: sample.date,
  163. quantity: transform(sample.quantity),
  164. isDisplayOnly: sample.isDisplayOnly,
  165. wasUserEntered: sample.wasUserEntered,
  166. syncIdentifier: sample.syncIdentifier,
  167. syncVersion: sample.syncVersion,
  168. device: sample.device
  169. )
  170. }
  171. )
  172. }
  173. }
  174. private extension MockCGMDataSource.Model {
  175. var glucoseProvider: MockGlucoseProvider {
  176. switch self {
  177. case .constant(let quantity):
  178. return .constant(quantity)
  179. case .sineCurve(parameters: let parameters):
  180. return .sineCurve(parameters: parameters)
  181. case .noData:
  182. return .noData
  183. case .signalLoss:
  184. return .noData
  185. }
  186. }
  187. }
  188. private extension MockCGMDataSource.Effects {
  189. var transformations: [(MockGlucoseProvider) -> MockGlucoseProvider] {
  190. // Each effect maps to a transformation on a MockGlucoseProvider
  191. return [
  192. glucoseNoise.map { maximumDeltaMagnitude in { $0.withRandomNoise(upTo: maximumDeltaMagnitude) } },
  193. randomLowOutlier.map { chance, delta in { $0.randomlyProducingLowOutlier(withChance: chance, outlierDelta: delta) } },
  194. randomHighOutlier.map { chance, delta in { $0.randomlyProducingHighOutlier(withChance: chance, outlierDelta: delta) } },
  195. randomErrorChance.map { chance in { $0.randomlyErroringOnNewData(withChance: chance) } }
  196. ].compactMap { $0 }
  197. }
  198. }