MockGlucoseProvider.swift 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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 CGMResult at that date.
  36. private let fetchDataAt: (_ date: Date, _ completion: @escaping (CGMResult) -> Void) -> Void
  37. func fetchData(at date: Date, completion: @escaping (CGMResult) -> Void) {
  38. fetchDataAt(date, completion)
  39. }
  40. func backfill(_ backfill: BackfillRequest, endingAt date: Date, completion: @escaping (CGMResult) -> 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: CGMResult = 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. syncIdentifier: UUID().uuidString,
  67. device: MockCGMDataSource.device
  68. )
  69. }
  70. }
  71. // MARK: - Models
  72. extension MockGlucoseProvider {
  73. fileprivate static func constant(_ quantity: HKQuantity) -> MockGlucoseProvider {
  74. return MockGlucoseProvider { date, completion in
  75. let sample = glucoseSample(at: date, quantity: quantity)
  76. completion(.newData([sample]))
  77. }
  78. }
  79. fileprivate static func sineCurve(parameters: MockCGMDataSource.Model.SineCurveParameters) -> MockGlucoseProvider {
  80. let (baseGlucose, amplitude, period, referenceDate) = parameters
  81. precondition(period > 0)
  82. let unit = HKUnit.milligramsPerDeciliter
  83. precondition(baseGlucose.is(compatibleWith: unit))
  84. precondition(amplitude.is(compatibleWith: unit))
  85. let baseGlucoseValue = baseGlucose.doubleValue(for: unit)
  86. let amplitudeValue = amplitude.doubleValue(for: unit)
  87. return MockGlucoseProvider { date, completion in
  88. let timeOffset = date.timeIntervalSince1970 - referenceDate.timeIntervalSince1970
  89. let glucoseValue = baseGlucoseValue + amplitudeValue * sin(2 * .pi / period * timeOffset)
  90. let sample = glucoseSample(at: date, quantity: HKQuantity(unit: unit, doubleValue: glucoseValue))
  91. completion(.newData([sample]))
  92. }
  93. }
  94. fileprivate static var noData: MockGlucoseProvider {
  95. return MockGlucoseProvider { _, completion in completion(.noData) }
  96. }
  97. fileprivate static func error(_ error: Error) -> MockGlucoseProvider {
  98. return MockGlucoseProvider { _, completion in completion(.error(error)) }
  99. }
  100. }
  101. // MARK: - Effects
  102. private struct MockGlucoseProviderError: Error { }
  103. extension MockGlucoseProvider {
  104. fileprivate func withRandomNoise(upTo magnitude: HKQuantity) -> MockGlucoseProvider {
  105. let unit = HKUnit.milligramsPerDeciliter
  106. precondition(magnitude.is(compatibleWith: unit))
  107. let magnitude = magnitude.doubleValue(for: unit)
  108. return mapGlucoseQuantities { glucose in
  109. let glucoseValue = glucose.doubleValue(for: unit) + .random(in: -magnitude...magnitude)
  110. return HKQuantity(unit: unit, doubleValue: glucoseValue)
  111. }
  112. }
  113. fileprivate func randomlyProducingLowOutlier(withChance chanceOfOutlier: Double, outlierDelta: HKQuantity) -> MockGlucoseProvider {
  114. return randomlyProducingOutlier(withChance: chanceOfOutlier, outlierDeltaMagnitude: outlierDelta, outlierDeltaSign: -)
  115. }
  116. fileprivate func randomlyProducingHighOutlier(withChance chanceOfOutlier: Double, outlierDelta: HKQuantity) -> MockGlucoseProvider {
  117. return randomlyProducingOutlier(withChance: chanceOfOutlier, outlierDeltaMagnitude: outlierDelta, outlierDeltaSign: +)
  118. }
  119. private func randomlyProducingOutlier(
  120. withChance chanceOfOutlier: Double,
  121. outlierDeltaMagnitude: HKQuantity,
  122. outlierDeltaSign: (Double) -> Double
  123. ) -> MockGlucoseProvider {
  124. let unit = HKUnit.milligramsPerDeciliter
  125. precondition(outlierDeltaMagnitude.is(compatibleWith: unit))
  126. let outlierDelta = outlierDeltaSign(outlierDeltaMagnitude.doubleValue(for: unit))
  127. return mapGlucoseQuantities { glucose in
  128. return coinFlip(
  129. withChanceOfHeads: chanceOfOutlier,
  130. ifHeads: HKQuantity(unit: unit, doubleValue: glucose.doubleValue(for: unit) + outlierDelta),
  131. ifTails: glucose
  132. )
  133. }
  134. }
  135. fileprivate func randomlyErroringOnNewData(withChance chance: Double) -> MockGlucoseProvider {
  136. return mapResult { result in
  137. return coinFlip(withChanceOfHeads: chance, ifHeads: .error(MockGlucoseProviderError()), ifTails: result)
  138. }
  139. }
  140. private func mapResult(_ transform: @escaping (CGMResult) -> CGMResult) -> MockGlucoseProvider {
  141. return MockGlucoseProvider { date, completion in
  142. self.fetchData(at: date) { result in
  143. completion(transform(result))
  144. }
  145. }
  146. }
  147. private func mapGlucoseQuantities(_ transform: @escaping (HKQuantity) -> HKQuantity) -> MockGlucoseProvider {
  148. return mapResult { result in
  149. return result.mapGlucoseQuantities(transform)
  150. }
  151. }
  152. }
  153. private extension CGMResult {
  154. func mapGlucoseQuantities(_ transform: (HKQuantity) -> HKQuantity) -> CGMResult {
  155. guard case .newData(let samples) = self else {
  156. return self
  157. }
  158. return .newData(
  159. samples.map { sample in
  160. return NewGlucoseSample(
  161. date: sample.date,
  162. quantity: transform(sample.quantity),
  163. isDisplayOnly: sample.isDisplayOnly,
  164. syncIdentifier: sample.syncIdentifier,
  165. syncVersion: sample.syncVersion,
  166. device: sample.device
  167. )
  168. }
  169. )
  170. }
  171. }
  172. private extension MockCGMDataSource.Model {
  173. var glucoseProvider: MockGlucoseProvider {
  174. switch self {
  175. case .constant(let quantity):
  176. return .constant(quantity)
  177. case .sineCurve(parameters: let parameters):
  178. return .sineCurve(parameters: parameters)
  179. case .noData:
  180. return .noData
  181. }
  182. }
  183. }
  184. private extension MockCGMDataSource.Effects {
  185. var transformations: [(MockGlucoseProvider) -> MockGlucoseProvider] {
  186. // Each effect maps to a transformation on a MockGlucoseProvider
  187. return [
  188. glucoseNoise.map { maximumDeltaMagnitude in { $0.withRandomNoise(upTo: maximumDeltaMagnitude) } },
  189. randomLowOutlier.map { chance, delta in { $0.randomlyProducingLowOutlier(withChance: chance, outlierDelta: delta) } },
  190. randomHighOutlier.map { chance, delta in { $0.randomlyProducingHighOutlier(withChance: chance, outlierDelta: delta) } },
  191. randomErrorChance.map { chance in { $0.randomlyErroringOnNewData(withChance: chance) } }
  192. ].compactMap { $0 }
  193. }
  194. }