Jonas Björkert пре 2 година
родитељ
комит
57f0427e93

+ 4 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -11,6 +11,7 @@
 		DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; };
 		DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; };
 		DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */; };
 		DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */; };
 		DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C612C4175FD00DBADDF /* NSProfile.swift */; };
 		DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C612C4175FD00DBADDF /* NSProfile.swift */; };
+		DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */; };
 		DD13BC752C3FD6210062313B /* InfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC742C3FD6200062313B /* InfoType.swift */; };
 		DD13BC752C3FD6210062313B /* InfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC742C3FD6200062313B /* InfoType.swift */; };
 		DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC762C3FD64E0062313B /* InfoData.swift */; };
 		DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC762C3FD64E0062313B /* InfoData.swift */; };
 		DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC782C3FE63A0062313B /* InfoManager.swift */; };
 		DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC782C3FE63A0062313B /* InfoManager.swift */; };
@@ -210,6 +211,7 @@
 		DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = "<group>"; };
 		DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = "<group>"; };
 		DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = "<group>"; };
 		DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = "<group>"; };
 		DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = "<group>"; };
 		DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = "<group>"; };
+		DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKUnit+Extensions.swift"; sourceTree = "<group>"; };
 		DD13BC742C3FD6200062313B /* InfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoType.swift; sourceTree = "<group>"; };
 		DD13BC742C3FD6200062313B /* InfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoType.swift; sourceTree = "<group>"; };
 		DD13BC762C3FD64E0062313B /* InfoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoData.swift; sourceTree = "<group>"; };
 		DD13BC762C3FD64E0062313B /* InfoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoData.swift; sourceTree = "<group>"; };
 		DD13BC782C3FE63A0062313B /* InfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoManager.swift; sourceTree = "<group>"; };
 		DD13BC782C3FE63A0062313B /* InfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoManager.swift; sourceTree = "<group>"; };
@@ -493,6 +495,7 @@
 				DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */,
 				DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */,
 				DDCF979324C0D380002C9752 /* UIViewExtension.swift */,
 				DDCF979324C0D380002C9752 /* UIViewExtension.swift */,
 				DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */,
 				DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */,
+				DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */,
 			);
 			);
 			path = Extensions;
 			path = Extensions;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -1075,6 +1078,7 @@
 				FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */,
 				FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */,
 				FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */,
 				FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */,
 				FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */,
 				FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */,
+				DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */,
 				DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */,
 				DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */,
 				DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */,
 				DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */,
 				FCA2DDE62501095000254A8C /* Timers.swift in Sources */,
 				FCA2DDE62501095000254A8C /* Timers.swift in Sources */,

+ 1 - 1
LoopFollow/Controllers/Nightscout/DeviceStatus.swift

@@ -83,7 +83,7 @@ extension MainViewController {
         
         
     // NS Device Status Response Processor
     // NS Device Status Response Processor
     func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
     func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) {
-        infoManager.clearInfoData(types: [.iob, .cob, .override, .battery, .pump, .target, .isf, .carbRatio])
+        infoManager.clearInfoData(types: [.iob, .cob, .override, .battery, .pump, .target, .isf, .carbRatio, .updated])
 
 
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
         if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: device status") }
         if jsonDeviceStatus.count == 0 {
         if jsonDeviceStatus.count == 0 {

+ 17 - 0
LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift

@@ -46,6 +46,23 @@ extension MainViewController {
                     infoManager.updateInfoData(type: .carbRatio, value: profileCR)
                     infoManager.updateInfoData(type: .carbRatio, value: profileCR)
                 }
                 }
 
 
+                /*
+                 Target
+                 */
+                let profileTargetLow = profileManager.currentTargetLow()
+                let profileTargetHigh = profileManager.currentTargetHigh()
+                var profileTarget: String?
+
+                if let profileTargetLow = profileTargetLow, let profileTargetHigh = profileTargetHigh, profileTargetLow != profileTargetHigh {
+                    profileTarget = "\(profileTargetLow) - \(profileTargetHigh)"
+                } else if let profileTargetLow = profileTargetLow {
+                    profileTarget = profileTargetLow
+                }
+
+                if let profileTarget = profileTarget {
+                    infoManager.updateInfoData(type: .target, value: profileTarget)
+                }
+
                 if let iobdata = lastLoopRecord["iob"] as? [String: AnyObject],
                 if let iobdata = lastLoopRecord["iob"] as? [String: AnyObject],
                    let iobValue = iobdata["iob"] as? Double {
                    let iobValue = iobdata["iob"] as? Double {
                     let formattedIOB = String(format: "%.2f", iobValue)
                     let formattedIOB = String(format: "%.2f", iobValue)

+ 34 - 5
LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift

@@ -8,6 +8,7 @@
 
 
 import Foundation
 import Foundation
 import UIKit
 import UIKit
+import HealthKit
 
 
 extension MainViewController {
 extension MainViewController {
     func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) {
     func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) {
@@ -24,9 +25,28 @@ extension MainViewController {
                     wasEnacted = true
                     wasEnacted = true
                 }
                 }
 
 
+                var determinedUnit: HKUnit = .millimolesPerLiter
+
+                // Determine the unit based on the target value since no unit is provided
+                if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject],
+                   let enactedTargetValue = enacted["current_target"] as? Double {
+                    if enactedTargetValue > 40 {
+                        determinedUnit = .milligramsPerDeciliter
+                    }
+                }
+
                 /*
                 /*
-                ISF
-                */
+                 Updated
+                 */
+                if let enactedTimestamp = lastLoopRecord["enacted"]?["timestamp"] as? String,
+                   let enactedTime = formatter.date(from: enactedTimestamp)?.timeIntervalSince1970 {
+                    let formattedTime = Localizer.formatTimestampToLocalString(enactedTime)
+                    infoManager.updateInfoData(type: .updated, value: formattedTime)
+                }
+
+                /*
+                 ISF
+                 */
                 let profileISF = profileManager.currentISF()
                 let profileISF = profileManager.currentISF()
                 var enactedISF: String?
                 var enactedISF: String?
                 if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject],
                 if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject],
@@ -100,10 +120,19 @@ extension MainViewController {
                     }
                     }
                 }
                 }
 
 
+                /*
+                 Target
+                 */
+                let profileTargetHigh = profileManager.currentTargetHigh()
+                var enactedTarget: String?
                 if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject],
                 if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject],
-                   let currentTarget = enacted["current_target"] as? Double {
-                    let formattedTarget = Localizer.toDisplayUnits(String(currentTarget))
-                    infoManager.updateInfoData(type: .target, value: formattedTarget)
+                   let enactedTargetValue = enacted["current_target"] as? Double {
+                    enactedTarget = Localizer.toDisplayUnits(String(enactedTargetValue))
+                }
+                if let profileTargetHigh = profileTargetHigh, let enactedTarget = enactedTarget, profileTargetHigh != enactedTarget {
+                    infoManager.updateInfoData(type: .target, value: "\(profileTargetHigh) → \(enactedTarget)")
+                } else if let profileTargetHigh = profileTargetHigh {
+                    infoManager.updateInfoData(type: .target, value: profileTargetHigh)
                 }
                 }
 
 
                 var predictioncolor = UIColor.systemGray
                 var predictioncolor = UIColor.systemGray

+ 11 - 7
LoopFollow/Controllers/Nightscout/NSProfile.swift

@@ -1,10 +1,7 @@
-//
-//  NSProfile.swift
-//  LoopFollow
-//
-//  Created by Jonas Björkert on 2024-07-12.
-//  Copyright © 2024 Jon Fawcett. All rights reserved.
-//
+// NSProfile.swift
+// LoopFollow
+// Created by Jonas Björkert on 2024-07-12.
+// Copyright © 2024 Jon Fawcett. All rights reserved.
 
 
 import Foundation
 import Foundation
 
 
@@ -32,11 +29,18 @@ struct NSProfile: Decodable {
             let insulinNeedsScaleFactor: Double?
             let insulinNeedsScaleFactor: Double?
             let symbol: String?
             let symbol: String?
         }
         }
+        struct TargetEntry: Decodable {
+            let value: Double
+            let time: String
+            let timeAsSeconds: Double
+        }
 
 
         let basal: [BasalEntry]
         let basal: [BasalEntry]
         let sens: [SensEntry]
         let sens: [SensEntry]
         let carbratio: [CarbRatioEntry]
         let carbratio: [CarbRatioEntry]
         let overrides: [OverrideEntry]?
         let overrides: [OverrideEntry]?
+        let target_high: [TargetEntry]?
+        let target_low: [TargetEntry]?
         let timezone: String
         let timezone: String
     }
     }
 
 

+ 43 - 32
LoopFollow/Controllers/Nightscout/ProfileManager.swift

@@ -1,42 +1,43 @@
-//
-//  ProfileManager.swift
-//  LoopFollow
-//
-//  Created by Jonas Björkert on 2024-07-12.
-//  Copyright © 2024 Jon Fawcett. All rights reserved.
-//
+// ProfileManager.swift
+// LoopFollow
+// Created by Jonas Björkert on 2024-07-12.
+// Copyright © 2024 Jon Fawcett. All rights reserved.
 
 
 import Foundation
 import Foundation
+import HealthKit
 
 
 struct ProfileManager {
 struct ProfileManager {
-    var isfSchedule: [TimeValue]
-    var basalSchedule: [TimeValue]
-    var carbRatioSchedule: [TimeValue]
+    var isfSchedule: [TimeValue<HKQuantity>]
+    var basalSchedule: [TimeValue<Double>]
+    var carbRatioSchedule: [TimeValue<Double>]
+    var targetLowSchedule: [TimeValue<HKQuantity>]
+    var targetHighSchedule: [TimeValue<HKQuantity>]
     var overrides: [Override]
     var overrides: [Override]
-    var units: String
+    var units: HKUnit
     var timezone: String
     var timezone: String
     var defaultProfile: String
     var defaultProfile: String
 
 
-    struct TimeValue {
+    struct TimeValue<T> {
         let timeAsSeconds: Int
         let timeAsSeconds: Int
-        let value: Double
+        let value: T
     }
     }
 
 
     struct Override {
     struct Override {
         let name: String
         let name: String
-        let targetRange: [Double]
+        let targetRange: [HKQuantity]
         let duration: Int
         let duration: Int
         let insulinNeedsScaleFactor: Double
         let insulinNeedsScaleFactor: Double
         let symbol: String
         let symbol: String
     }
     }
 
 
     init() {
     init() {
-        // Initialize with default values
         self.isfSchedule = []
         self.isfSchedule = []
         self.basalSchedule = []
         self.basalSchedule = []
         self.carbRatioSchedule = []
         self.carbRatioSchedule = []
+        self.targetLowSchedule = []
+        self.targetHighSchedule = []
         self.overrides = []
         self.overrides = []
-        self.units = "mmol/L"
+        self.units = .millimolesPerLiter
         self.timezone = "UTC"
         self.timezone = "UTC"
         self.defaultProfile = ""
         self.defaultProfile = ""
     }
     }
@@ -46,15 +47,17 @@ struct ProfileManager {
             return
             return
         }
         }
 
 
-        self.units = profileData.units
+        self.units = profileData.units == "mg/dL" ? .milligramsPerDeciliter : .millimolesPerLiter
         self.timezone = store.timezone
         self.timezone = store.timezone
         self.defaultProfile = profileData.defaultProfile
         self.defaultProfile = profileData.defaultProfile
 
 
-        self.isfSchedule = store.sens.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) }
+        self.isfSchedule = store.sens.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) }
         self.basalSchedule = store.basal.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) }
         self.basalSchedule = store.basal.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) }
         self.carbRatioSchedule = store.carbratio.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) }
         self.carbRatioSchedule = store.carbratio.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) }
+        self.targetLowSchedule = store.target_low?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? []
+        self.targetHighSchedule = store.target_high?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? []
         if let overrides = store.overrides {
         if let overrides = store.overrides {
-            self.overrides = overrides.map { Override(name: $0.name ?? "", targetRange: $0.targetRange ?? [], duration: $0.duration ?? 0, insulinNeedsScaleFactor: $0.insulinNeedsScaleFactor ?? 1.0, symbol: $0.symbol ?? "") }
+            self.overrides = overrides.map { Override(name: $0.name ?? "", targetRange: $0.targetRange?.map { HKQuantity(unit: self.units, doubleValue: $0) } ?? [], duration: $0.duration ?? 0, insulinNeedsScaleFactor: $0.insulinNeedsScaleFactor ?? 1.0, symbol: $0.symbol ?? "") }
         } else {
         } else {
             self.overrides = []
             self.overrides = []
         }
         }
@@ -62,7 +65,7 @@ struct ProfileManager {
 
 
     func currentISF() -> String? {
     func currentISF() -> String? {
         if let isf = getCurrentValue(from: isfSchedule) {
         if let isf = getCurrentValue(from: isfSchedule) {
-            return Localizer.formatLocalDouble(isf, unit: self.units)
+            return Localizer.formatQuantity(isf)
         }
         }
         return nil
         return nil
     }
     }
@@ -76,12 +79,26 @@ struct ProfileManager {
 
 
     func currentCarbRatio() -> String? {
     func currentCarbRatio() -> String? {
         if let carbRatio = getCurrentValue(from: carbRatioSchedule) {
         if let carbRatio = getCurrentValue(from: carbRatioSchedule) {
-            return Localizer.formatToLocalizedString(carbRatio)
+            return Localizer.formatToLocalizedString(carbRatio, maxFractionDigits: 1, minFractionDigits: 0)
         }
         }
         return nil
         return nil
     }
     }
 
 
-    private func getCurrentValue(from schedule: [TimeValue]) -> Double? {
+    func currentTargetLow() -> String? {
+        if let targetLow = getCurrentValue(from: targetLowSchedule) {
+            return Localizer.formatQuantity(targetLow)
+        }
+        return nil
+    }
+
+    func currentTargetHigh() -> String? {
+        if let targetHigh = getCurrentValue(from: targetHighSchedule) {
+            return Localizer.formatQuantity(targetHigh)
+        }
+        return nil
+    }
+
+    private func getCurrentValue<T>(from schedule: [TimeValue<T>]) -> T? {
         guard !schedule.isEmpty else { return nil }
         guard !schedule.isEmpty else { return nil }
 
 
         let now = Date()
         let now = Date()
@@ -90,7 +107,7 @@ struct ProfileManager {
         calendar.component(.minute, from: now) * 60 +
         calendar.component(.minute, from: now) * 60 +
         calendar.component(.second, from: now)
         calendar.component(.second, from: now)
 
 
-        var lastValue: Double?
+        var lastValue: T?
         for timeValue in schedule {
         for timeValue in schedule {
             if currentTimeInSeconds >= timeValue.timeAsSeconds {
             if currentTimeInSeconds >= timeValue.timeAsSeconds {
                 lastValue = timeValue.value
                 lastValue = timeValue.value
@@ -101,20 +118,14 @@ struct ProfileManager {
         return lastValue
         return lastValue
     }
     }
 
 
-    private func formatValue(_ value: Double) -> String {
-        if units == "mg/dL" {
-            return String(format: "%.0f", value)
-        } else {
-            return String(format: "%.2f", value)
-        }
-    }
-
     mutating func clear() {
     mutating func clear() {
         self.isfSchedule = []
         self.isfSchedule = []
         self.basalSchedule = []
         self.basalSchedule = []
         self.carbRatioSchedule = []
         self.carbRatioSchedule = []
+        self.targetLowSchedule = []
+        self.targetHighSchedule = []
         self.overrides = []
         self.overrides = []
-        self.units = "mmol/L"
+        self.units = HKUnit.millimolesPerLiter
         self.timezone = "UTC"
         self.timezone = "UTC"
         self.defaultProfile = ""
         self.defaultProfile = ""
     }
     }

+ 20 - 0
LoopFollow/Extensions/HKUnit+Extensions.swift

@@ -0,0 +1,20 @@
+//
+//  HKUnit+Extensions.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2024-07-15.
+//  Copyright © 2024 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+import HealthKit
+
+extension HKUnit {
+    public static let milligramsPerDeciliter: HKUnit = {
+        return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci))
+    }()
+
+    public static let millimolesPerLiter: HKUnit = {
+        return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter())
+    }()
+}

+ 2 - 1
LoopFollow/InfoTable/InfoType.swift

@@ -9,7 +9,7 @@
 import Foundation
 import Foundation
 
 
 enum InfoType: Int, CaseIterable {
 enum InfoType: Int, CaseIterable {
-    case iob, cob, basal, override, battery, pump, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio
+    case iob, cob, basal, override, battery, pump, sage, cage, recBolus, minMax, carbsToday, autosens, profile, target, isf, carbRatio, updated
 
 
     var name: String {
     var name: String {
         switch self {
         switch self {
@@ -29,6 +29,7 @@ enum InfoType: Int, CaseIterable {
         case .target: return "Target"
         case .target: return "Target"
         case .isf: return "ISF"
         case .isf: return "ISF"
         case .carbRatio: return "CR"
         case .carbRatio: return "CR"
+        case .updated: return "Updated"
         }
         }
     }
     }
 
 

+ 22 - 1
LoopFollow/helpers/Localizer.swift

@@ -7,7 +7,7 @@
 //
 //
 
 
 import Foundation
 import Foundation
-
+import HealthKit
 
 
 class Localizer {
 class Localizer {
     static func formatToLocalizedString(_ value: Double, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) -> String {
     static func formatToLocalizedString(_ value: Double, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) -> String {
@@ -21,6 +21,27 @@ class Localizer {
         return numberFormatter.string(from: numberValue) ?? String(value)
         return numberFormatter.string(from: numberValue) ?? String(value)
     }
     }
 
 
+    static func formatQuantity(_ quantity: HKQuantity) -> String {
+        let unitPreference = UserDefaultsRepository.units.value
+
+        if unitPreference == "mg/dL" {
+            let valueInMgdL = quantity.doubleValue(for: .milligramsPerDeciliter)
+            return formatToLocalizedString(valueInMgdL, maxFractionDigits: 0, minFractionDigits: 0)
+        } else {
+            let valueInMmolL = quantity.doubleValue(for: .millimolesPerLiter)
+            return formatToLocalizedString(valueInMmolL, maxFractionDigits: 1, minFractionDigits: 1)
+        }
+    }
+
+    static func formatTimestampToLocalString(_ timestamp: TimeInterval) -> String {
+        let date = Date(timeIntervalSince1970: timestamp)
+        let dateFormatter = DateFormatter()
+        dateFormatter.setLocalizedDateFormatFromTemplate("jms")
+        dateFormatter.locale = Locale.current
+        dateFormatter.timeZone = TimeZone.current
+        return dateFormatter.string(from: date)
+    }
+
     static func formatLocalDouble(_ value: Double, unit: String? = nil) -> String {
     static func formatLocalDouble(_ value: Double, unit: String? = nil) -> String {
         let numberFormatter = NumberFormatter()
         let numberFormatter = NumberFormatter()
         numberFormatter.numberStyle = .decimal
         numberFormatter.numberStyle = .decimal