Guardrail.swift 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. //
  2. // Guardrail.swift
  3. // LoopKit
  4. //
  5. // Created by Michael Pangburn on 4/10/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import HealthKit
  9. public enum SafetyClassification: Equatable {
  10. public enum Threshold: Equatable {
  11. case minimum
  12. case belowRecommended
  13. case aboveRecommended
  14. case maximum
  15. }
  16. case withinRecommendedRange
  17. case outsideRecommendedRange(Threshold)
  18. }
  19. public struct Guardrail<Value: Comparable> {
  20. public let absoluteBounds: ClosedRange<Value>
  21. public let recommendedBounds: ClosedRange<Value>
  22. public let startingSuggestion: Value?
  23. public init(absoluteBounds: ClosedRange<Value>, recommendedBounds: ClosedRange<Value>, startingSuggestion: Value? = nil) {
  24. precondition(absoluteBounds.lowerBound <= recommendedBounds.lowerBound, "The minimum value must be less than or equal to the smallest recommended value")
  25. precondition(absoluteBounds.upperBound >= recommendedBounds.upperBound, "The maximum value must be greater than or equal to the greatest recommended value")
  26. if let startingSuggestion = startingSuggestion {
  27. precondition(recommendedBounds.contains(startingSuggestion))
  28. }
  29. self.absoluteBounds = absoluteBounds
  30. self.recommendedBounds = recommendedBounds
  31. self.startingSuggestion = startingSuggestion
  32. }
  33. public func classification(for value: Value) -> SafetyClassification {
  34. switch value {
  35. case ...absoluteBounds.lowerBound where absoluteBounds.lowerBound != recommendedBounds.lowerBound:
  36. return .outsideRecommendedRange(.minimum)
  37. case ..<recommendedBounds.lowerBound:
  38. return .outsideRecommendedRange(.belowRecommended)
  39. case ...recommendedBounds.upperBound:
  40. return .withinRecommendedRange
  41. case ..<absoluteBounds.upperBound:
  42. return .outsideRecommendedRange(.aboveRecommended)
  43. case absoluteBounds.upperBound...:
  44. return .outsideRecommendedRange(.maximum)
  45. default:
  46. preconditionFailure("Unreachable")
  47. }
  48. }
  49. }
  50. extension Guardrail where Value: Strideable {
  51. public func allValues(stridingBy increment: Value.Stride) -> StrideThrough<Value> {
  52. stride(from: absoluteBounds.lowerBound, through: absoluteBounds.upperBound, by: increment)
  53. }
  54. }
  55. extension Guardrail where Value == HKQuantity {
  56. public init(absoluteBounds: ClosedRange<Double>, recommendedBounds: ClosedRange<Double>, unit: HKUnit, startingSuggestion: Double? = nil) {
  57. let absoluteBoundsWithUnit = HKQuantity(unit: unit, doubleValue: absoluteBounds.lowerBound)...HKQuantity(unit: unit, doubleValue: absoluteBounds.upperBound)
  58. let recommendedBoundsWithUnit = HKQuantity(unit: unit, doubleValue: recommendedBounds.lowerBound)...HKQuantity(unit: unit, doubleValue: recommendedBounds.upperBound)
  59. let startingSuggestionQuantity: HKQuantity?
  60. if let startingSuggestion = startingSuggestion {
  61. startingSuggestionQuantity = HKQuantity(unit: unit, doubleValue: startingSuggestion)
  62. } else {
  63. startingSuggestionQuantity = nil
  64. }
  65. self.init(absoluteBounds: absoluteBoundsWithUnit, recommendedBounds: recommendedBoundsWithUnit, startingSuggestion: startingSuggestionQuantity)
  66. }
  67. public func allQuantities(stridingBy increment: HKQuantity, unit: HKUnit) -> [HKQuantity] {
  68. allValues(stridingBy: increment, unit: unit)
  69. .map { HKQuantity(unit: unit, doubleValue: $0) }
  70. }
  71. public func allValues(stridingBy increment: HKQuantity, unit: HKUnit) -> [Double] {
  72. Array(stride(
  73. from: absoluteBounds.lowerBound.doubleValue(for: unit, withRounding: true),
  74. through: absoluteBounds.upperBound.doubleValue(for: unit, withRounding: true),
  75. by: increment.doubleValue(for: unit, withRounding: true)
  76. ))
  77. }
  78. }