import Foundation import HealthKit import LoopKit struct BloodGlucose: JSON, Identifiable, Hashable, Codable { enum Direction: String, JSON { case tripleUp = "TripleUp" case doubleUp = "DoubleUp" case singleUp = "SingleUp" case fortyFiveUp = "FortyFiveUp" case flat = "Flat" case fortyFiveDown = "FortyFiveDown" case singleDown = "SingleDown" case doubleDown = "DoubleDown" case tripleDown = "TripleDown" case none = "NONE" case notComputable = "NOT COMPUTABLE" case rateOutOfRange = "RATE OUT OF RANGE" init?(from string: String) { switch string { case "\u{2191}\u{2191}\u{2191}", "TripleUp": self = .tripleUp case "\u{2191}\u{2191}", "DoubleUp": self = .doubleUp case "\u{2191}", "SingleUp": self = .singleUp case "\u{2197}", "FortyFiveUp": self = .fortyFiveUp case "\u{2192}", "Flat": self = .flat case "\u{2198}", "FortyFiveDown": self = .fortyFiveDown case "\u{2193}", "SingleDown": self = .singleDown case "\u{2193}\u{2193}", "DoubleDown": self = .doubleDown case "\u{2193}\u{2193}\u{2193}", "TripleDown": self = .tripleDown case "\u{2194}", "NONE": self = .none case "NOT COMPUTABLE": self = .notComputable case "RATE OUT OF RANGE": self = .rateOutOfRange default: return nil } } } enum CodingKeys: String, CodingKey { case _id case sgv case direction case date case dateString case unfiltered case filtered case noise case glucose case type case activationDate case sessionStartDate case transmitterID } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) _id = try container.decode(String.self, forKey: ._id) sgv = try? container.decodeIfPresent(Int.self, forKey: .sgv) if sgv == nil { // The nightscout API might return a double instead of an int, or the key might be missing if let doubleValue = try? container.decodeIfPresent(Double.self, forKey: .sgv) { sgv = Int(doubleValue) } // If both attempts fail, sgv remains nil } direction = try container.decodeIfPresent(Direction.self, forKey: .direction) date = try container.decode(Decimal.self, forKey: .date) dateString = try container.decode(Date.self, forKey: .dateString) unfiltered = try container.decodeIfPresent(Decimal.self, forKey: .unfiltered) filtered = try container.decodeIfPresent(Decimal.self, forKey: .filtered) noise = try container.decodeIfPresent(Int.self, forKey: .noise) glucose = try container.decodeIfPresent(Int.self, forKey: .glucose) type = try container.decodeIfPresent(String.self, forKey: .type) activationDate = try container.decodeIfPresent(Date.self, forKey: .activationDate) sessionStartDate = try container.decodeIfPresent(Date.self, forKey: .sessionStartDate) transmitterID = try container.decodeIfPresent(String.self, forKey: .transmitterID) } init( _id: String = UUID().uuidString, sgv: Int? = nil, direction: Direction? = nil, date: Decimal, dateString: Date, unfiltered: Decimal? = nil, filtered: Decimal? = nil, noise: Int? = nil, glucose: Int? = nil, type: String? = nil, activationDate: Date? = nil, sessionStartDate: Date? = nil, transmitterID: String? = nil ) { self._id = _id self.sgv = sgv self.direction = direction self.date = date self.dateString = dateString self.unfiltered = unfiltered self.filtered = filtered self.noise = noise self.glucose = glucose self.type = type self.activationDate = activationDate self.sessionStartDate = sessionStartDate self.transmitterID = transmitterID } var _id: String? var id: String { _id ?? UUID().uuidString } var sgv: Int? var direction: Direction? let date: Decimal let dateString: Date let unfiltered: Decimal? let filtered: Decimal? let noise: Int? var glucose: Int? var type: String? = nil var activationDate: Date? = nil var sessionStartDate: Date? = nil var transmitterID: String? = nil var isStateValid: Bool { sgv ?? 0 >= 39 && noise ?? 1 != 4 } static func == (lhs: BloodGlucose, rhs: BloodGlucose) -> Bool { lhs.dateString == rhs.dateString } func hash(into hasher: inout Hasher) { hasher.combine(dateString) } } enum GlucoseUnits: String, JSON, Equatable, CaseIterable, Identifiable { case mgdL = "mg/dL" case mmolL = "mmol/L" static let exchangeRate: Decimal = 0.0555 var id: String { rawValue } } extension Int { var asMmolL: Decimal { Trio.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain) } var formattedAsMmolL: String { NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)" } func formatted(for units: GlucoseUnits) -> String { units == .mgdL ? description : formattedAsMmolL } func formatted(withUnits units: GlucoseUnits) -> String { formatted(for: units) + " \(units.rawValue)" } } extension Decimal { func asUnit(_ unit: GlucoseUnits) -> Decimal { unit == .mgdL ? self : asMmolL } var asMmolL: Decimal { Trio.rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain) } var asMgdL: Decimal { Trio.rounded(self / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain) } var formattedAsMmolL: String { NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)" } func formatted(for units: GlucoseUnits) -> String { units == .mgdL ? description : formattedAsMmolL } func formatted(withUnits units: GlucoseUnits) -> String { formatted(for: units) + " \(units.rawValue)" } } extension Double { func asUnit(_ units: GlucoseUnits) -> Double { units == .mgdL ? self : Double(truncating: asMmolL as NSNumber) } var asMmolL: Decimal { Trio.rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain) } var asMgdL: Decimal { Trio.rounded(Decimal(self) / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain) } var formattedAsMmolL: String { NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)" } func formatted(for units: GlucoseUnits) -> String { units == .mgdL ? description : formattedAsMmolL } func formatted(withUnits units: GlucoseUnits) -> String { formatted(for: units) + " \(units.rawValue)" } } extension NumberFormatter { static let glucoseFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.locale = Locale.current formatter.numberStyle = .decimal formatter.minimumFractionDigits = 1 formatter.maximumFractionDigits = 1 return formatter }() } extension BloodGlucose { func convertStoredGlucoseSample(isManualGlucose: Bool) -> StoredGlucoseSample { StoredGlucoseSample( syncIdentifier: id, startDate: dateString.date, quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucose!)), wasUserEntered: isManualGlucose, device: HKDevice.local() ) } }