BloodGlucose.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  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. sgv = try? container.decodeIfPresent(Int.self, forKey: .sgv)
  78. if sgv == nil {
  79. // The nightscout API might return a double instead of an int, or the key might be missing
  80. if let doubleValue = try? container.decodeIfPresent(Double.self, forKey: .sgv) {
  81. sgv = Int(doubleValue)
  82. }
  83. // If both attempts fail, sgv remains nil
  84. }
  85. direction = try container.decodeIfPresent(Direction.self, forKey: .direction)
  86. date = try container.decode(Decimal.self, forKey: .date)
  87. dateString = try container.decode(Date.self, forKey: .dateString)
  88. unfiltered = try container.decodeIfPresent(Decimal.self, forKey: .unfiltered)
  89. filtered = try container.decodeIfPresent(Decimal.self, forKey: .filtered)
  90. noise = try container.decodeIfPresent(Int.self, forKey: .noise)
  91. glucose = try container.decodeIfPresent(Int.self, forKey: .glucose)
  92. type = try container.decodeIfPresent(String.self, forKey: .type)
  93. activationDate = try container.decodeIfPresent(Date.self, forKey: .activationDate)
  94. sessionStartDate = try container.decodeIfPresent(Date.self, forKey: .sessionStartDate)
  95. transmitterID = try container.decodeIfPresent(String.self, forKey: .transmitterID)
  96. }
  97. init(
  98. _id: String = UUID().uuidString,
  99. sgv: Int? = nil,
  100. direction: Direction? = nil,
  101. date: Decimal,
  102. dateString: Date,
  103. unfiltered: Decimal? = nil,
  104. filtered: Decimal? = nil,
  105. noise: Int? = nil,
  106. glucose: Int? = nil,
  107. type: String? = nil,
  108. activationDate: Date? = nil,
  109. sessionStartDate: Date? = nil,
  110. transmitterID: String? = nil
  111. ) {
  112. self._id = _id
  113. self.sgv = sgv
  114. self.direction = direction
  115. self.date = date
  116. self.dateString = dateString
  117. self.unfiltered = unfiltered
  118. self.filtered = filtered
  119. self.noise = noise
  120. self.glucose = glucose
  121. self.type = type
  122. self.activationDate = activationDate
  123. self.sessionStartDate = sessionStartDate
  124. self.transmitterID = transmitterID
  125. }
  126. var _id: String?
  127. var id: String {
  128. _id ?? UUID().uuidString
  129. }
  130. var sgv: Int?
  131. var direction: Direction?
  132. let date: Decimal
  133. let dateString: Date
  134. let unfiltered: Decimal?
  135. let filtered: Decimal?
  136. let noise: Int?
  137. var glucose: Int?
  138. var type: String? = nil
  139. var activationDate: Date? = nil
  140. var sessionStartDate: Date? = nil
  141. var transmitterID: String? = nil
  142. var isStateValid: Bool { sgv ?? 0 >= 39 && noise ?? 1 != 4 }
  143. static func == (lhs: BloodGlucose, rhs: BloodGlucose) -> Bool {
  144. lhs.dateString == rhs.dateString
  145. }
  146. func hash(into hasher: inout Hasher) {
  147. hasher.combine(dateString)
  148. }
  149. }
  150. enum GlucoseUnits: String, JSON, Equatable, CaseIterable, Identifiable {
  151. case mgdL = "mg/dL"
  152. case mmolL = "mmol/L"
  153. static let exchangeRate: Decimal = 0.0555
  154. var id: String { rawValue }
  155. }
  156. extension Int {
  157. var asMmolL: Decimal {
  158. Trio.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
  159. }
  160. var formattedAsMmolL: String {
  161. NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
  162. }
  163. func formatted(for units: GlucoseUnits) -> String {
  164. units == .mgdL ? description : formattedAsMmolL
  165. }
  166. func formatted(withUnits units: GlucoseUnits) -> String {
  167. formatted(for: units) + " \(units.rawValue)"
  168. }
  169. }
  170. extension Decimal {
  171. func asUnit(_ unit: GlucoseUnits) -> Decimal {
  172. unit == .mgdL ? self : asMmolL
  173. }
  174. var asMmolL: Decimal {
  175. Trio.rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
  176. }
  177. var asMgdL: Decimal {
  178. Trio.rounded(self / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
  179. }
  180. var formattedAsMmolL: String {
  181. NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
  182. }
  183. func formatted(for units: GlucoseUnits) -> String {
  184. units == .mgdL ? description : formattedAsMmolL
  185. }
  186. func formatted(withUnits units: GlucoseUnits) -> String {
  187. formatted(for: units) + " \(units.rawValue)"
  188. }
  189. }
  190. extension Double {
  191. func asUnit(_ units: GlucoseUnits) -> Double {
  192. units == .mgdL ? self : Double(truncating: asMmolL as NSNumber)
  193. }
  194. var asMmolL: Decimal {
  195. Trio.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
  196. }
  197. var asMgdL: Decimal {
  198. Trio.rounded(Decimal(self) / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
  199. }
  200. var formattedAsMmolL: String {
  201. NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
  202. }
  203. func formatted(for units: GlucoseUnits) -> String {
  204. units == .mgdL ? description : formattedAsMmolL
  205. }
  206. func formatted(withUnits units: GlucoseUnits) -> String {
  207. formatted(for: units) + " \(units.rawValue)"
  208. }
  209. }
  210. extension NumberFormatter {
  211. static let glucoseFormatter: NumberFormatter = {
  212. let formatter = NumberFormatter()
  213. formatter.locale = Locale.current
  214. formatter.numberStyle = .decimal
  215. formatter.minimumFractionDigits = 1
  216. formatter.maximumFractionDigits = 1
  217. return formatter
  218. }()
  219. }
  220. extension BloodGlucose {
  221. func convertStoredGlucoseSample(isManualGlucose: Bool) -> StoredGlucoseSample {
  222. StoredGlucoseSample(
  223. syncIdentifier: id,
  224. startDate: dateString.date,
  225. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose!)),
  226. wasUserEntered: isManualGlucose,
  227. device: HKDevice.local()
  228. )
  229. }
  230. }