BloodGlucose.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import Foundation
  2. import HealthKit
  3. import LoopKit
  4. struct BloodGlucose: JSON, Identifiable, Hashable, Codable {
  5. enum Direction: String, JSON {
  6. case tripleUp = "TripleUp"
  7. case doubleUp = "DoubleUp"
  8. case singleUp = "SingleUp"
  9. case fortyFiveUp = "FortyFiveUp"
  10. case flat = "Flat"
  11. case fortyFiveDown = "FortyFiveDown"
  12. case singleDown = "SingleDown"
  13. case doubleDown = "DoubleDown"
  14. case tripleDown = "TripleDown"
  15. case none = "NONE"
  16. case notComputable = "NOT COMPUTABLE"
  17. case rateOutOfRange = "RATE OUT OF RANGE"
  18. init?(from string: String) {
  19. switch string {
  20. case "\u{2191}\u{2191}\u{2191}",
  21. "TripleUp":
  22. self = .tripleUp
  23. case "\u{2191}\u{2191}",
  24. "DoubleUp":
  25. self = .doubleUp
  26. case "\u{2191}",
  27. "SingleUp":
  28. self = .singleUp
  29. case "\u{2197}",
  30. "FortyFiveUp":
  31. self = .fortyFiveUp
  32. case "\u{2192}",
  33. "Flat":
  34. self = .flat
  35. case "\u{2198}",
  36. "FortyFiveDown":
  37. self = .fortyFiveDown
  38. case "\u{2193}",
  39. "SingleDown":
  40. self = .singleDown
  41. case "\u{2193}\u{2193}",
  42. "DoubleDown":
  43. self = .doubleDown
  44. case "\u{2193}\u{2193}\u{2193}",
  45. "TripleDown":
  46. self = .tripleDown
  47. case "\u{2194}",
  48. "NONE":
  49. self = .none
  50. case "NOT COMPUTABLE":
  51. self = .notComputable
  52. case "RATE OUT OF RANGE":
  53. self = .rateOutOfRange
  54. default:
  55. return nil
  56. }
  57. }
  58. }
  59. enum CodingKeys: String, CodingKey {
  60. case _id
  61. case sgv
  62. case direction
  63. case date
  64. case dateString
  65. case unfiltered
  66. case filtered
  67. case noise
  68. case glucose
  69. case type
  70. case activationDate
  71. case sessionStartDate
  72. case transmitterID
  73. }
  74. init(from decoder: Decoder) throws {
  75. let container = try decoder.container(keyedBy: CodingKeys.self)
  76. _id = try container.decode(String.self, forKey: ._id)
  77. do {
  78. sgv = try container.decode(Int.self, forKey: .sgv)
  79. } catch {
  80. // The nightscout API returns a double instead of an int
  81. sgv = Int(try container.decode(Double.self, forKey: .sgv))
  82. }
  83. direction = try container.decodeIfPresent(Direction.self, forKey: .direction)
  84. date = try container.decode(Decimal.self, forKey: .date)
  85. dateString = try container.decode(Date.self, forKey: .dateString)
  86. unfiltered = try container.decodeIfPresent(Decimal.self, forKey: .unfiltered)
  87. filtered = try container.decodeIfPresent(Decimal.self, forKey: .filtered)
  88. noise = try container.decodeIfPresent(Int.self, forKey: .noise)
  89. glucose = try container.decodeIfPresent(Int.self, forKey: .glucose)
  90. type = try container.decodeIfPresent(String.self, forKey: .type)
  91. activationDate = try container.decodeIfPresent(Date.self, forKey: .activationDate)
  92. sessionStartDate = try container.decodeIfPresent(Date.self, forKey: .sessionStartDate)
  93. transmitterID = try container.decodeIfPresent(String.self, forKey: .transmitterID)
  94. }
  95. init(
  96. _id: String = UUID().uuidString,
  97. sgv: Int? = nil,
  98. direction: Direction? = nil,
  99. date: Decimal,
  100. dateString: Date,
  101. unfiltered: Decimal? = nil,
  102. filtered: Decimal? = nil,
  103. noise: Int? = nil,
  104. glucose: Int? = nil,
  105. type: String? = nil,
  106. activationDate: Date? = nil,
  107. sessionStartDate: Date? = nil,
  108. transmitterID: String? = nil
  109. ) {
  110. self._id = _id
  111. self.sgv = sgv
  112. self.direction = direction
  113. self.date = date
  114. self.dateString = dateString
  115. self.unfiltered = unfiltered
  116. self.filtered = filtered
  117. self.noise = noise
  118. self.glucose = glucose
  119. self.type = type
  120. self.activationDate = activationDate
  121. self.sessionStartDate = sessionStartDate
  122. self.transmitterID = transmitterID
  123. }
  124. var _id: String?
  125. var id: String {
  126. _id ?? UUID().uuidString
  127. }
  128. var sgv: Int?
  129. var direction: Direction?
  130. let date: Decimal
  131. let dateString: Date
  132. let unfiltered: Decimal?
  133. let filtered: Decimal?
  134. let noise: Int?
  135. var glucose: Int?
  136. var type: String? = nil
  137. var activationDate: Date? = nil
  138. var sessionStartDate: Date? = nil
  139. var transmitterID: String? = nil
  140. var isStateValid: Bool { sgv ?? 0 >= 39 && noise ?? 1 != 4 }
  141. static func == (lhs: BloodGlucose, rhs: BloodGlucose) -> Bool {
  142. lhs.dateString == rhs.dateString
  143. }
  144. func hash(into hasher: inout Hasher) {
  145. hasher.combine(dateString)
  146. }
  147. }
  148. enum GlucoseUnits: String, JSON, Equatable {
  149. case mgdL = "mg/dL"
  150. case mmolL = "mmol/L"
  151. static let exchangeRate: Decimal = 0.0555
  152. }
  153. extension Int {
  154. var asMmolL: Decimal {
  155. Trio.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
  156. }
  157. var formattedAsMmolL: String {
  158. NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
  159. }
  160. }
  161. extension Decimal {
  162. var asMmolL: Decimal {
  163. Trio.rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
  164. }
  165. var asMgdL: Decimal {
  166. Trio.rounded(self / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
  167. }
  168. var formattedAsMmolL: String {
  169. NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
  170. }
  171. }
  172. extension Double {
  173. var asMmolL: Decimal {
  174. Trio.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
  175. }
  176. var asMgdL: Decimal {
  177. Trio.rounded(Decimal(self) / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
  178. }
  179. var formattedAsMmolL: String {
  180. NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
  181. }
  182. }
  183. extension NumberFormatter {
  184. static let glucoseFormatter: NumberFormatter = {
  185. let formatter = NumberFormatter()
  186. formatter.locale = Locale.current
  187. formatter.numberStyle = .decimal
  188. formatter.minimumFractionDigits = 1
  189. formatter.maximumFractionDigits = 1
  190. return formatter
  191. }()
  192. }
  193. extension BloodGlucose: SavitzkyGolaySmoothable {
  194. var value: Double {
  195. get {
  196. Double(glucose ?? 0)
  197. }
  198. set {
  199. glucose = Int(newValue)
  200. sgv = Int(newValue)
  201. }
  202. }
  203. }
  204. extension BloodGlucose {
  205. func convertStoredGlucoseSample(isManualGlucose: Bool) -> StoredGlucoseSample {
  206. StoredGlucoseSample(
  207. syncIdentifier: id,
  208. startDate: dateString.date,
  209. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose!)),
  210. wasUserEntered: isManualGlucose,
  211. device: HKDevice.local()
  212. )
  213. }
  214. }