CarbStatus.swift 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. //
  2. // CarbStatus.swift
  3. // LoopKit
  4. //
  5. // Copyright © 2017 LoopKit Authors. All rights reserved.
  6. //
  7. import Foundation
  8. import HealthKit
  9. public struct CarbStatus<T: CarbEntry> {
  10. /// Details entered by the user
  11. public let entry: T
  12. /// The last-computed absorption of the carbs
  13. public let absorption: AbsorbedCarbValue?
  14. /// The timeline of observed carb absorption. Nil if observed absorption is less than the modeled minimum
  15. public let observedTimeline: [CarbValue]?
  16. }
  17. // Masquerade as a carb entry, substituting AbsorbedCarbValue's interpretation of absorption time
  18. extension CarbStatus: SampleValue {
  19. public var quantity: HKQuantity {
  20. return entry.quantity
  21. }
  22. public var startDate: Date {
  23. return entry.startDate
  24. }
  25. }
  26. extension CarbStatus: CarbEntry {
  27. public var absorptionTime: TimeInterval? {
  28. return absorption?.estimatedDate.duration ?? entry.absorptionTime
  29. }
  30. }
  31. extension CarbStatus {
  32. func dynamicCarbsOnBoard(at date: Date, defaultAbsorptionTime: TimeInterval, delay: TimeInterval, delta: TimeInterval, absorptionModel: CarbAbsorptionComputable) -> Double {
  33. guard date >= startDate - delta,
  34. let absorption = absorption
  35. else {
  36. // We have to have absorption info for dynamic calculation
  37. return entry.carbsOnBoard(at: date, defaultAbsorptionTime: defaultAbsorptionTime, delay: delay, absorptionModel: absorptionModel)
  38. }
  39. let unit = HKUnit.gram()
  40. guard let observedTimeline = observedTimeline, let observationEnd = observedTimeline.last?.endDate else {
  41. // Less than minimum observed or observation not yet started; calc based on modeled absorption rate
  42. let total = absorption.total.doubleValue(for: unit)
  43. let time = date.timeIntervalSince(startDate) - delay
  44. let absorptionTime = absorption.estimatedDate.duration
  45. return absorptionModel.unabsorbedCarbs(of: total, atTime: time, absorptionTime: absorptionTime)
  46. }
  47. guard date <= observationEnd else {
  48. // Predicted absorption for remaining carbs, post-observation
  49. let effectiveTime = date.timeIntervalSince(observationEnd) + absorption.timeToAbsorbObservedCarbs
  50. let effectiveAbsorptionTime = absorption.timeToAbsorbObservedCarbs + absorption.estimatedTimeRemaining
  51. let total = absorption.total.doubleValue(for: unit)
  52. let unabsorbedAtEffectiveTime = absorptionModel.unabsorbedCarbs(of: total, atTime: effectiveTime, absorptionTime: effectiveAbsorptionTime)
  53. let unabsorbedCarbs = max(unabsorbedAtEffectiveTime, 0.0)
  54. return unabsorbedCarbs
  55. }
  56. // Observed absorption
  57. // TODO: This creates an O(n^2) situation for COB timelines
  58. let total = entry.quantity.doubleValue(for: unit)
  59. return max(observedTimeline.filter({ $0.endDate <= date }).reduce(total) { (total, value) -> Double in
  60. return total - value.quantity.doubleValue(for: unit)
  61. }, 0)
  62. }
  63. func dynamicAbsorbedCarbs(at date: Date, absorptionTime: TimeInterval, delay: TimeInterval, delta: TimeInterval, absorptionModel: CarbAbsorptionComputable) -> Double {
  64. guard date >= startDate,
  65. let absorption = absorption
  66. else {
  67. // We have to have absorption info for dynamic calculation
  68. return entry.absorbedCarbs(at: date, absorptionTime: absorptionTime, delay: delay, absorptionModel: absorptionModel)
  69. }
  70. let unit = HKUnit.gram()
  71. guard let observedTimeline = observedTimeline, let observationEnd = observedTimeline.last?.endDate else {
  72. // Less than minimum observed or observation not yet started; calc based on modeled absorption rate
  73. let total = absorption.total.doubleValue(for: unit)
  74. let time = date.timeIntervalSince(startDate) - delay
  75. let absorptionTime = absorption.estimatedDate.duration
  76. return absorptionModel.absorbedCarbs(of: total, atTime: time, absorptionTime: absorptionTime)
  77. }
  78. guard date <= observationEnd else {
  79. // Predicted absorption for remaining carbs, post-observation
  80. let effectiveTime = date.timeIntervalSince(observationEnd) + absorption.timeToAbsorbObservedCarbs
  81. let effectiveAbsorptionTime = absorption.timeToAbsorbObservedCarbs + absorption.estimatedTimeRemaining
  82. let total = absorption.total.doubleValue(for: unit)
  83. let absorbedAtEffectiveTime = absorptionModel.absorbedCarbs(of: total, atTime: effectiveTime, absorptionTime: effectiveAbsorptionTime)
  84. let absorbedCarbs = min(absorbedAtEffectiveTime, total)
  85. return absorbedCarbs
  86. }
  87. // Observed absorption
  88. // TODO: This creates an O(n^2) situation for carb effect timelines
  89. var sum: Double = 0
  90. var beforeDate = observedTimeline.filter { (value) -> Bool in
  91. value.startDate.addingTimeInterval(delta) <= date
  92. }
  93. // Apply only a portion of the value if it extends past the final value
  94. if let last = beforeDate.popLast() {
  95. let observationInterval = DateInterval(start: last.startDate, end: last.endDate)
  96. if observationInterval.duration > 0,
  97. let calculationInterval = DateInterval(start: last.startDate, end: date).intersection(with: observationInterval)
  98. {
  99. sum += calculationInterval.duration / observationInterval.duration * last.quantity.doubleValue(for: unit)
  100. }
  101. }
  102. return min(beforeDate.reduce(sum) { (sum, value) -> Double in
  103. return sum + value.quantity.doubleValue(for: unit)
  104. }, quantity.doubleValue(for: unit))
  105. }
  106. }