Просмотр исходного кода

Handle reason parsing for mmol/L users; adjust NS status upload for mmol/L reason

Deniz Cengiz 1 год назад
Родитель
Сommit
ecd4960831

+ 10 - 10
FreeAPS/Sources/Models/Determination.swift

@@ -2,10 +2,10 @@ import Foundation
 
 struct Determination: JSON, Equatable {
     let id: UUID?
-    let reason: String
+    var reason: String
     let units: Decimal?
     let insulinReq: Decimal?
-    let eventualBG: Int?
+    var eventualBG: Int?
     let sensitivityRatio: Decimal?
     let rate: Decimal?
     let duration: Decimal?
@@ -15,20 +15,20 @@ struct Determination: JSON, Equatable {
     var deliverAt: Date?
     let carbsReq: Decimal?
     let temp: TempType?
-    let bg: Decimal?
+    var bg: Decimal?
     let reservoir: Decimal?
-    let isf: Decimal?
+    var isf: Decimal?
     var timestamp: Date?
     let tdd: Decimal?
     let insulin: Insulin?
-    let current_target: Decimal?
+    var current_target: Decimal?
     let insulinForManualBolus: Decimal?
     let manualBolusErrorString: Decimal?
-    let minDelta: Decimal?
-    let expectedDelta: Decimal?
-    let minGuardBG: Decimal?
-    let minPredBG: Decimal?
-    let threshold: Decimal?
+    var minDelta: Decimal?
+    var expectedDelta: Decimal?
+    var minGuardBG: Decimal?
+    var minPredBG: Decimal?
+    var threshold: Decimal?
     let carbRatio: Decimal?
     let received: Bool?
 }

+ 99 - 2
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -323,7 +323,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             return
         }
 
-        // Suggested/ Enacted
+        // Suggested / Enacted
         async let enactedDeterminationID = determinationStorage
             .fetchLastDeterminationObjectID(predicate: NSPredicate.enactedDeterminationsNotYetUploadedToNightscout)
         async let suggestedDeterminationID = determinationStorage
@@ -335,7 +335,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         async let fetchedIOBEntry = storage.retrieveAsync(OpenAPS.Monitor.iob, as: [IOBEntry].self)
         async let fetchedPumpStatus = storage.retrieveAsync(OpenAPS.Monitor.status, as: PumpStatus.self)
 
-        let (fetchedEnactedDetermination, fetchedSuggestedDetermination) = await (
+        var (fetchedEnactedDetermination, fetchedSuggestedDetermination) = await (
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(enactedDeterminationID),
             determinationStorage.getOrefDeterminationNotYetUploadedToNightscout(suggestedDeterminationID)
         )
@@ -353,6 +353,10 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         var modifiedSuggestedDetermination = fetchedSuggestedDetermination
         if var suggestion = fetchedSuggestedDetermination {
             suggestion.timestamp = suggestion.deliverAt
+
+            if settingsManager.settings.units == .mmolL {
+                suggestion.reason = parseReasonGlucoseValuesToMmolL(suggestion.reason)
+            }
             // Check whether the last suggestion that was uploaded is the same that is fetched again when we are attempting to upload the enacted determination
             // Apparently we are too fast; so the flag update is not fast enough to have the predicate filter last suggestion out
             // If this check is truthy, set suggestion to nil so it's not uploaded again
@@ -363,6 +367,20 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             }
         }
 
+        if var fetchedEnacted = fetchedEnactedDetermination, settingsManager.settings.units == .mmolL {
+            var modifiedFetchedEnactedDetermination = fetchedEnactedDetermination
+            modifiedFetchedEnactedDetermination?
+                .reason = parseReasonGlucoseValuesToMmolL(fetchedEnacted.reason)
+
+            modifiedFetchedEnactedDetermination?.bg = fetchedEnacted.bg?.asMmolL
+            modifiedFetchedEnactedDetermination?.current_target = fetchedEnacted.current_target?.asMmolL
+            modifiedFetchedEnactedDetermination?.minGuardBG = fetchedEnacted.minGuardBG?.asMmolL
+            modifiedFetchedEnactedDetermination?.minPredBG = fetchedEnacted.minPredBG?.asMmolL
+            modifiedFetchedEnactedDetermination?.threshold = fetchedEnacted.threshold?.asMmolL
+
+            fetchedEnactedDetermination = modifiedFetchedEnactedDetermination
+        }
+
         // Gather all relevant data for OpenAPS Status
         let iob = await fetchedIOBEntry
         let openapsStatus = OpenAPSStatus(
@@ -906,3 +924,82 @@ extension BaseNightscoutManager: TempTargetsObserver {
         }
     }
 }
+
+extension BaseNightscoutManager {
+    /**
+     Converts glucose-related values in the given `reason` string to mmol/L, including ranges (e.g., `ISF: 54→54`), comparisons (e.g., `maxDelta 37 > 20% of BG 95`), and both positive and negative values (e.g., `Dev: -36`).
+
+     - Parameters:
+       - reason: The string containing glucose-related values to be converted.
+
+     - Returns:
+       A string with glucose values converted to mmol/L.
+
+     - Glucose tags handled: `ISF:`, `Target:`, `minPredBG`, `minGuardBG`, `IOBpredBG`, `COBpredBG`, `UAMpredBG`, `Dev:`, `maxDelta`, `BG`.
+     */
+    func parseReasonGlucoseValuesToMmolL(_ reason: String) -> String {
+        // Updated pattern to handle cases like minGuardBG 34, minGuardBG 34<70, and "maxDelta 37 > 20% of BG 95", and ensure "Target:" is handled correctly
+        let pattern =
+            "(ISF:\\s*-?\\d+→-?\\d+|Dev:\\s*-?\\d+|Target:\\s*-?\\d+|(?:minPredBG|minGuardBG|IOBpredBG|COBpredBG|UAMpredBG|maxDelta|BG)\\s*-?\\d+(?:<\\d+)?(?:>\\s*\\d+%\\s*of\\s*BG\\s*\\d+)?)"
+
+        let regex = try! NSRegularExpression(pattern: pattern)
+
+        func convertToMmolL(_ value: String) -> String {
+            if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) {
+                return glucoseValue.asMmolL.description
+            }
+            return value
+        }
+
+        let matches = regex.matches(in: reason, range: NSRange(reason.startIndex..., in: reason))
+        var updatedReason = reason
+
+        for match in matches.reversed() {
+            if let range = Range(match.range, in: reason) {
+                let glucoseValueString = String(reason[range])
+
+                if glucoseValueString.contains("→") {
+                    // Handle ISF case with an arrow (e.g., ISF: 54→54)
+                    let values = glucoseValueString.components(separatedBy: "→")
+                    let firstValue = convertToMmolL(values[0])
+                    let secondValue = convertToMmolL(values[1])
+                    let formattedGlucoseValueString = "\(values[0].components(separatedBy: ":")[0]): \(firstValue)→\(secondValue)"
+                    updatedReason.replaceSubrange(range, with: formattedGlucoseValueString)
+                } else if glucoseValueString.contains("<") {
+                    // Handle range case for minGuardBG like "minGuardBG 34<70"
+                    let values = glucoseValueString.components(separatedBy: "<")
+                    let firstValue = convertToMmolL(values[0])
+                    let secondValue = convertToMmolL(values[1])
+                    let formattedGlucoseValueString = "\(values[0].components(separatedBy: ":")[0]) \(firstValue)<\(secondValue)"
+                    updatedReason.replaceSubrange(range, with: formattedGlucoseValueString)
+                } else if glucoseValueString.contains(">"), glucoseValueString.contains("BG") {
+                    // Handle cases like "maxDelta 37 > 20% of BG 95"
+                    let pattern = "(\\d+) > \\d+% of BG (\\d+)"
+                    let matches = try! NSRegularExpression(pattern: pattern)
+                        .matches(in: glucoseValueString, range: NSRange(glucoseValueString.startIndex..., in: glucoseValueString))
+
+                    if let match = matches.first, match.numberOfRanges == 3 {
+                        let firstValueRange = Range(match.range(at: 1), in: glucoseValueString)!
+                        let secondValueRange = Range(match.range(at: 2), in: glucoseValueString)!
+
+                        let firstValue = convertToMmolL(String(glucoseValueString[firstValueRange]))
+                        let secondValue = convertToMmolL(String(glucoseValueString[secondValueRange]))
+
+                        let formattedGlucoseValueString = glucoseValueString.replacingOccurrences(
+                            of: "\(glucoseValueString[firstValueRange]) > 20% of BG \(glucoseValueString[secondValueRange])",
+                            with: "\(firstValue) > 20% of BG \(secondValue)"
+                        )
+                        updatedReason.replaceSubrange(range, with: formattedGlucoseValueString)
+                    }
+                } else {
+                    // General case for single glucose values like "Target: 100" or "minGuardBG 34"
+                    let parts = glucoseValueString.components(separatedBy: CharacterSet(charactersIn: ": "))
+                    let formattedValue = convertToMmolL(parts.last!.trimmingCharacters(in: .whitespaces))
+                    updatedReason.replaceSubrange(range, with: "\(parts[0]): \(formattedValue)")
+                }
+            }
+        }
+
+        return updatedReason
+    }
+}

+ 82 - 95
FreeAPS/Sources/Views/TagCloudView.swift

@@ -7,16 +7,14 @@ struct TagCloudView: View {
     var tags: [String]
     var shouldParseToMmolL: Bool
 
-    @State private var totalHeight
-//          = CGFloat.zero       // << variant for ScrollView/List
-        = CGFloat.infinity // << variant for VStack
+    @State private var totalHeight = CGFloat.infinity // << variant for VStack
+
     var body: some View {
         VStack {
             GeometryReader { geometry in
                 self.generateContent(in: geometry)
             }
         }
-//        .frame(height: totalHeight)// << variant for ScrollView/List
         .frame(maxHeight: totalHeight) // << variant for VStack
     }
 
@@ -29,8 +27,7 @@ struct TagCloudView: View {
                 self.item(for: tag, isMmolL: shouldParseToMmolL)
                     .padding([.horizontal, .vertical], 2)
                     .alignmentGuide(.leading, computeValue: { d in
-                        if abs(width - d.width) > g.size.width
-                        {
+                        if abs(width - d.width) > g.size.width {
                             width = 0
                             height -= d.height
                         }
@@ -53,40 +50,6 @@ struct TagCloudView: View {
         }.background(viewHeightReader($totalHeight))
     }
 
-//    private func item(for textTag: String) -> some View {
-//        var colorOfTag: Color {
-//            switch textTag {
-//            case textTag where textTag.contains("SMB Delivery Ratio:"):
-//                return .uam
-//            case textTag where textTag.contains("Bolus"):
-//                return .green
-//            case textTag where textTag.contains("TDD:"),
-//                 textTag where textTag.contains("tdd_factor"),
-//                 textTag where textTag.contains("Sigmoid function"),
-//                 textTag where textTag.contains("Logarithmic formula"),
-//                 textTag where textTag.contains("AF:"),
-//                 textTag where textTag.contains("Autosens/Dynamic Limit:"),
-//                 textTag where textTag.contains("Dynamic ISF/CR"),
-//                 textTag where textTag.contains("Basal ratio"),
-//                 textTag where textTag.contains("SMB Ratio"):
-//                return .zt
-//            case textTag where textTag.contains("Middleware:"):
-//                return .red
-//            case textTag where textTag.contains("SMB Ratio"):
-//                return .orange
-//            default:
-//                return .insulin
-//            }
-//        }
-//
-//        return ZStack { Text(textTag)
-//            .padding(.vertical, 2)
-//            .padding(.horizontal, 4)
-//            .font(.subheadline)
-//            .background(colorOfTag.opacity(0.8))
-//            .foregroundColor(Color.white)
-//            .cornerRadius(2) }
-//    }
     private func item(for textTag: String, isMmolL: Bool) -> some View {
         var colorOfTag: Color {
             switch textTag {
@@ -113,61 +76,7 @@ struct TagCloudView: View {
             }
         }
 
-        func formattedTextTag(for tag: String) -> String {
-            // List of glucose-related tags
-            let glucoseTags = ["ISF:", "Target:", "minPredBG", "minGuardBG", "IOBpredBG", "COBpredBG", "UAMpredBG", "Dev:"]
-
-            var updatedTag = tag
-
-            // Apply conversion if necessary
-            for glucoseTag in glucoseTags {
-                if glucoseTag == "ISF:" {
-                    // Handle the special ISF case with the arrow
-                    if let range = updatedTag.range(of: "\(glucoseTag)\\s*\\d+→\\d+", options: .regularExpression) {
-                        let glucoseValueString = updatedTag[range]
-                        let values = glucoseValueString.components(separatedBy: "→")
-
-                        if let firstValue = Double(
-                            values[0]
-                                .components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
-                        ),
-                            let secondValue = Double(
-                                values[1]
-                                    .components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
-                            )
-                        {
-                            let formattedFirstValue = isMmolL ? Double(firstValue.asMmolL) : firstValue
-                            let formattedSecondValue = isMmolL ? Double(secondValue.asMmolL) : secondValue
-
-                            let formattedGlucoseValueString =
-                                "\(glucoseTag) \(formattedFirstValue)→\(formattedSecondValue)"
-                            updatedTag = updatedTag.replacingOccurrences(
-                                of: glucoseValueString,
-                                with: formattedGlucoseValueString
-                            )
-                        }
-                    }
-                } else {
-                    // General case for other glucose tags
-                    if let range = updatedTag.range(of: "\(glucoseTag)\\s*\\d+", options: .regularExpression) {
-                        let glucoseValueString = updatedTag[range]
-                        if let glucoseValue = Double(
-                            glucoseValueString
-                                .components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
-                        ) {
-                            let formattedValue = isMmolL ? Double(glucoseValue.asMmolL) : glucoseValue
-                            updatedTag = updatedTag.replacingOccurrences(
-                                of: glucoseValueString,
-                                with: "\(glucoseTag) \(formattedValue)"
-                            )
-                        }
-                    }
-                }
-            }
-            return updatedTag
-        }
-
-        let formattedTextTag = formattedTextTag(for: textTag)
+        let formattedTextTag = formatGlucoseTags(textTag, isMmolL: isMmolL)
 
         return ZStack {
             Text(formattedTextTag)
@@ -180,6 +89,84 @@ struct TagCloudView: View {
         }
     }
 
+    /**
+     Converts glucose-related values in the given `tag` string to mmol/L, including ranges (e.g., `ISF: 54→54`), comparisons (e.g., `maxDelta 37 > 20% of BG 95`), and both positive and negative values (e.g., `Dev: -36`).
+
+     - Parameters:
+       - tag: The string containing glucose-related values to be converted.
+       - isMmolL: A Boolean flag indicating whether to convert values to mmol/L.
+
+     - Returns:
+       A string with glucose values converted to mmol/L.
+
+     - Glucose tags handled: `ISF:`, `Target:`, `minPredBG`, `minGuardBG`, `IOBpredBG`, `COBpredBG`, `UAMpredBG`, `Dev:`, `maxDelta`, `BG`.
+     */
+    private func formatGlucoseTags(_ tag: String, isMmolL: Bool) -> String {
+        // Updated pattern to handle cases like minGuardBG 34, minGuardBG 34<70, "maxDelta 37 > 20% of BG 95", and ensure "Target:" is handled correctly
+        let pattern =
+            "(ISF:\\s*-?\\d+→-?\\d+|Dev:\\s*-?\\d+|Target:\\s*-?\\d+|(?:minPredBG|minGuardBG|IOBpredBG|COBpredBG|UAMpredBG|maxDelta|BG)\\s*-?\\d+(?:<\\d+)?(?:>\\s*\\d+%\\s*of\\s*BG\\s*\\d+)?)"
+
+        let regex = try! NSRegularExpression(pattern: pattern)
+
+        func convertToMmolL(_ value: String) -> String {
+            if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) {
+                return isMmolL ? glucoseValue.asMmolL.description : value
+            }
+            return value
+        }
+
+        let matches = regex.matches(in: tag, range: NSRange(tag.startIndex..., in: tag))
+        var updatedTag = tag
+
+        for match in matches.reversed() {
+            if let range = Range(match.range, in: tag) {
+                let glucoseValueString = String(tag[range])
+
+                if glucoseValueString.contains("→") {
+                    // Handle ISF case with an arrow (e.g., ISF: 54→54)
+                    let values = glucoseValueString.components(separatedBy: "→")
+                    let firstValue = convertToMmolL(values[0])
+                    let secondValue = convertToMmolL(values[1])
+                    let formattedGlucoseValueString = "\(values[0].components(separatedBy: ":")[0]): \(firstValue)→\(secondValue)"
+                    updatedTag.replaceSubrange(range, with: formattedGlucoseValueString)
+                } else if glucoseValueString.contains("<") {
+                    // Handle range case for minGuardBG like "minGuardBG 34<70"
+                    let values = glucoseValueString.components(separatedBy: "<")
+                    let firstValue = convertToMmolL(values[0])
+                    let secondValue = convertToMmolL(values[1])
+                    let formattedGlucoseValueString = "\(values[0].components(separatedBy: ":")[0]) \(firstValue)<\(secondValue)"
+                    updatedTag.replaceSubrange(range, with: formattedGlucoseValueString)
+                } else if glucoseValueString.contains(">"), glucoseValueString.contains("BG") {
+                    // Handle cases like "maxDelta 37 > 20% of BG 95"
+                    let pattern = "(\\d+) > \\d+% of BG (\\d+)"
+                    let matches = try! NSRegularExpression(pattern: pattern)
+                        .matches(in: glucoseValueString, range: NSRange(glucoseValueString.startIndex..., in: glucoseValueString))
+
+                    if let match = matches.first, match.numberOfRanges == 3 {
+                        let firstValueRange = Range(match.range(at: 1), in: glucoseValueString)!
+                        let secondValueRange = Range(match.range(at: 2), in: glucoseValueString)!
+
+                        let firstValue = convertToMmolL(String(glucoseValueString[firstValueRange]))
+                        let secondValue = convertToMmolL(String(glucoseValueString[secondValueRange]))
+
+                        let formattedGlucoseValueString = glucoseValueString.replacingOccurrences(
+                            of: "\(glucoseValueString[firstValueRange]) > 20% of BG \(glucoseValueString[secondValueRange])",
+                            with: "\(firstValue) > 20% of BG \(secondValue)"
+                        )
+                        updatedTag.replaceSubrange(range, with: formattedGlucoseValueString)
+                    }
+                } else {
+                    // General case for single glucose values like "Target: 100" or "minGuardBG 34"
+                    let parts = glucoseValueString.components(separatedBy: CharacterSet(charactersIn: ": "))
+                    let formattedValue = convertToMmolL(parts.last!.trimmingCharacters(in: .whitespaces))
+                    updatedTag.replaceSubrange(range, with: "\(parts[0]): \(formattedValue)")
+                }
+            }
+        }
+
+        return updatedTag
+    }
+
     private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
         GeometryReader { geometry -> Color in
             let rect = geometry.frame(in: .local)