Explorar el Código

Add new row for limits in recommended bolus popup

* new `factoredInsulin` property of `CalculationResult` is the full bolus with rec bolus percentage, fatty meal bolus percentage, and super bolus applied... but before applying limits like Max Bolus
* Limits row displays info for
  * rec bolus < 0 U,
  * current glucuse < 54
  * rec bolus > maxBolus
  * rec bolus > maxIOB - IOB

* Don't recommend any insulin when `currentBG` is less than 54 mg/dL (3.0 mmol/L)
* Don't round anything until the very end of `insulinCalculated`'s calculation

* Refactor "Max COB" label to prevent using ZStack
* Revert `÷`s back to `/`
* Thicken divider before Full Bolus row and add `≈` before Full Bolus amount to more clearly indicate it's the result of the previous lines
* Use `.plain` rounding for IOB in popup, otherwise the first and last column doesn't match when negated
* Automated l18n stuff
* Linting from `dev` merge
Mike Plante hace 1 año
padre
commit
71021b69bb

+ 2 - 2
Trio/Sources/APS/CGM/PluginSource.swift

@@ -159,9 +159,9 @@ extension PluginSource: CGMManagerDelegate {
         UUID().uuidString
         UUID().uuidString
     }
     }
 
 
-    func cgmManager(_ cgmManager: CGMManager, didUpdate status: CGMManagerStatus) {
+    func cgmManager(_: CGMManager, didUpdate status: CGMManagerStatus) {
         debug(.deviceManager, "CGM Manager did update state to \(status)")
         debug(.deviceManager, "CGM Manager did update state to \(status)")
-        
+
         processQueue.async {
         processQueue.async {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
             if self.cgmHasValidSensorSession != status.hasValidSensorSession {
                 self.cgmHasValidSensorSession = status.hasValidSensorSession
                 self.cgmHasValidSensorSession = status.hasValidSensorSession

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 83 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 2 - 0
Trio/Sources/Modules/Treatments/TreatmentsStateModel.swift

@@ -62,6 +62,7 @@ extension Treatments {
         var wholeCobInsulin: Decimal = 0
         var wholeCobInsulin: Decimal = 0
         var iobInsulinReduction: Decimal = 0
         var iobInsulinReduction: Decimal = 0
         var wholeCalc: Decimal = 0
         var wholeCalc: Decimal = 0
+        var factoredInsulin: Decimal = 0
         var insulinCalculated: Decimal = 0
         var insulinCalculated: Decimal = 0
         var fraction: Decimal = 0
         var fraction: Decimal = 0
         var basal: Decimal = 0
         var basal: Decimal = 0
@@ -403,6 +404,7 @@ extension Treatments {
             iobInsulinReduction = result.iobInsulinReduction
             iobInsulinReduction = result.iobInsulinReduction
             superBolusInsulin = result.superBolusInsulin
             superBolusInsulin = result.superBolusInsulin
             wholeCalc = result.wholeCalc
             wholeCalc = result.wholeCalc
+            factoredInsulin = result.factoredInsulin
             fifteenMinInsulin = result.fifteenMinutesInsulin
             fifteenMinInsulin = result.fifteenMinutesInsulin
 
 
             return apsManager.roundBolus(amount: result.insulinCalculated)
             return apsManager.roundBolus(amount: result.insulinCalculated)

+ 89 - 86
Trio/Sources/Modules/Treatments/View/PopupView.swift

@@ -83,7 +83,7 @@ struct PopupView: View {
                     calcDeltaRow
                     calcDeltaRow
                     calcDeltaFormulaRow
                     calcDeltaFormulaRow
 
 
-                    DividerCustom()
+                    DividerCustom(2)
 
 
                     calcFullBolusRow
                     calcFullBolusRow
 
 
@@ -97,6 +97,16 @@ struct PopupView: View {
 
 
                     calcResultRow
                     calcResultRow
                     calcResultFormulaRow
                     calcResultFormulaRow
+
+                    DividerCustom()
+
+                    GridRow {
+                        Text("Recommended Bolus")
+                            .gridCellColumns(3)
+                            .gridCellAnchor(.center)
+                            .padding(.bottom, 10)
+                    }
+                    limitsRow
                 }
                 }
 
 
                 Spacer()
                 Spacer()
@@ -213,14 +223,13 @@ struct PopupView: View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
             HStack {
             HStack {
                 Text("IOB:").foregroundColor(.secondary)
                 Text("IOB:").foregroundColor(.secondary)
-                Text(self.insulinFormatter(state.iob) + " U")
+                Text(self.insulinFormatter(state.iob, .plain) + " U")
             }
             }
 
 
             Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
             Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
 
 
-            let iobFormatted = self.insulinFormatter(state.iob)
             HStack {
             HStack {
-                Text((state.iob >= 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
+                Text(self.insulinFormatter(-1 * state.iob, .plain))
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
                 .gridColumnAlignment(.trailing)
                 .gridColumnAlignment(.trailing)
@@ -229,27 +238,9 @@ struct PopupView: View {
 
 
     var calcCOBRow: some View {
     var calcCOBRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            // Left column using ZStack to overlay Max COB
-            ZStack(alignment: .leading) {
-                // Main COB content
-                HStack {
-                    Text("COB:").foregroundColor(.secondary)
-                    Text(
-                        state.wholeCob
-                            .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                            String(localized: " g", comment: "grams")
-                    ).foregroundColor(state.wholeCob >= state.maxCOB ? Color.loopRed : .primary)
-                }
-
-                // Max COB overlay positioned below
-                if state.wholeCob >= state.maxCOB {
-                    Text("Max COB")
-                        .foregroundColor(Color.loopRed)
-                        .font(.caption)
-                        .offset(y: 16) // Adjust this value to position the text correctly
-                }
-            }
-            .frame(height: 20) // Fixed height for main content only
+            let maxCobReached: Bool = state.wholeCob >= state.maxCOB
+            Text(maxCobReached ? "Max COB:" : "COB:")
+                .foregroundColor(maxCobReached ? Color.loopRed : .secondary)
 
 
             // Middle column
             // Middle column
             Text(
             Text(
@@ -265,9 +256,7 @@ struct PopupView: View {
 
 
             // Right column
             // Right column
             HStack {
             HStack {
-                Text(
-                    self.insulinFormatter(state.wholeCobInsulin)
-                )
+                Text(self.insulinFormatter(state.wholeCobInsulin))
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }
             }
             .fontWeight(.bold)
             .fontWeight(.bold)
@@ -277,13 +266,15 @@ struct PopupView: View {
 
 
     var calcCOBFormulaRow: some View {
     var calcCOBFormulaRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
+            Text(
+                state.wholeCob
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                    String(localized: " g", comment: "grams")
+            )
 
 
-            Text("COB ÷ Carb Ratio").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
-                .gridColumnAlignment(.leading)
-                .gridCellColumns(2)
+            Text("COB / Carb Ratio").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
+                .gridColumnAlignment(.leading).font(.caption)
         }
         }
-        .font(.caption)
     }
     }
 
 
     var calcDeltaRow: some View {
     var calcDeltaRow: some View {
@@ -332,6 +323,7 @@ struct PopupView: View {
             Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
             Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
 
 
             HStack {
             HStack {
+                Text("≈").foregroundColor(.secondary)
                 Text(self.insulinFormatter(state.wholeCalc))
                 Text(self.insulinFormatter(state.wholeCalc))
                     .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                     .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
@@ -373,23 +365,21 @@ struct PopupView: View {
 
 
     var calcResultRow: some View {
     var calcResultRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            Text("Result").fontWeight(.bold)
+            Text("Factors").foregroundColor(.secondary)
 
 
             HStack {
             HStack {
                 Text(state.useSuperBolus ? "(" : "")
                 Text(state.useSuperBolus ? "(" : "")
                     .foregroundColor(.loopRed)
                     .foregroundColor(.loopRed)
 
 
                     + Text(self.insulinFormatter(state.wholeCalc))
                     + Text(self.insulinFormatter(state.wholeCalc))
-                    .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
+                    .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.secondary)
 
 
                     + Text(" × ")
                     + Text(" × ")
-                    .foregroundColor(.secondary)
 
 
                     + Text((100 * state.fraction).formatted() + "%")
                     + Text((100 * state.fraction).formatted() + "%")
 
 
                     // if fatty meal is chosen
                     // if fatty meal is chosen
                     + Text(state.useFattyMealCorrectionFactor ? " × " : "")
                     + Text(state.useFattyMealCorrectionFactor ? " × " : "")
-                    .foregroundColor(.secondary)
 
 
                     + Text(state.useFattyMealCorrectionFactor ? (100 * state.fattyMealFactor).formatted() + "%" : "")
                     + Text(state.useFattyMealCorrectionFactor ? (100 * state.fattyMealFactor).formatted() + "%" : "")
                     .foregroundColor(.orange)
                     .foregroundColor(.orange)
@@ -400,21 +390,19 @@ struct PopupView: View {
                     .foregroundColor(.loopRed)
                     .foregroundColor(.loopRed)
 
 
                     + Text(state.useSuperBolus ? " + " : "")
                     + Text(state.useSuperBolus ? " + " : "")
-                    .foregroundColor(.secondary)
 
 
                     + Text(state.useSuperBolus ? self.insulinFormatter(state.superBolusInsulin) : "")
                     + Text(state.useSuperBolus ? self.insulinFormatter(state.superBolusInsulin) : "")
                     .foregroundColor(.loopRed)
                     .foregroundColor(.loopRed)
                     // endif superbolus is chosen
                     // endif superbolus is chosen
 
 
                     + Text(" ≈ ")
                     + Text(" ≈ ")
-                    .foregroundColor(.secondary)
             }
             }
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
+            .foregroundColor(.secondary)
 
 
             HStack {
             HStack {
-                Text(self.insulinFormatter(state.insulinCalculated))
+                Text(self.insulinFormatter(state.factoredInsulin))
                     .fontWeight(.bold)
                     .fontWeight(.bold)
-                    .foregroundColor(state.insulinCalculated >= state.maxBolus ? Color.loopRed : Color.blue)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }
             }
             .gridColumnAlignment(.trailing)
             .gridColumnAlignment(.trailing)
@@ -426,26 +414,16 @@ struct PopupView: View {
         GridRow(alignment: .bottom) {
         GridRow(alignment: .bottom) {
             if state.useFattyMealCorrectionFactor {
             if state.useFattyMealCorrectionFactor {
                 Group {
                 Group {
-                    getFormulaText("Full Bolus x Fatty Meal % x Percentage", colorScheme: colorScheme) +
-                        getCappedText(
-                            insulinCalculated: state.insulinCalculated,
-                            maxBolus: state.maxBolus,
-                            maxIOB: state.maxIOB,
-                            iob: state.iob
-                        )
+                    Text("Full Bolus x Rec. Bolus % x Fatty Meal %")
+                        .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
                 }
                 }
                 .font(.caption)
                 .font(.caption)
                 .gridCellAnchor(.center)
                 .gridCellAnchor(.center)
                 .gridCellColumns(3)
                 .gridCellColumns(3)
             } else if state.useSuperBolus {
             } else if state.useSuperBolus {
                 Group {
                 Group {
-                    getFormulaText("(Full Bolus x Percentage) + Super Bolus", colorScheme: colorScheme) +
-                        getCappedText(
-                            insulinCalculated: state.insulinCalculated,
-                            maxBolus: state.maxBolus,
-                            maxIOB: state.maxIOB,
-                            iob: state.iob
-                        )
+                    Text("(Full Bolus x Rec. Bolus %) + Super Bolus")
+                        .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
                 }
                 }
                 .font(.caption)
                 .font(.caption)
                 .gridCellAnchor(.center)
                 .gridCellAnchor(.center)
@@ -453,13 +431,8 @@ struct PopupView: View {
             } else {
             } else {
                 Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
                 Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
                 Group {
                 Group {
-                    getFormulaText("Full Bolus x Percentage", colorScheme: colorScheme) +
-                        getCappedText(
-                            insulinCalculated: state.insulinCalculated,
-                            maxBolus: state.maxBolus,
-                            maxIOB: state.maxIOB,
-                            iob: state.iob
-                        )
+                    Text("Full Bolus x Rec. Bolus %")
+                        .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
                 }
                 }
                 .font(.caption)
                 .font(.caption)
                 .padding(.top, 5)
                 .padding(.top, 5)
@@ -469,17 +442,59 @@ struct PopupView: View {
         }
         }
     }
     }
 
 
-    private func insulinFormatter(_ value: Decimal) -> String {
-        let toRound = NSDecimalNumber(decimal: value).doubleValue
-        let roundedValue = Decimal(floor(100 * toRound) / 100)
+    var limitsRow: some View {
+        GridRow(alignment: .top) {
+            Text("Limits").foregroundColor(.secondary)
+
+            VStack {
+                let iobAvailable: Decimal = state.maxIOB - state.iob
+                if state.factoredInsulin < 0 {
+                    Text("No insulin recommended.")
+                } else if state.currentBG < 54 {
+                    Text("Glucose is very low.")
+                } else if state.maxBolus <= iobAvailable && state.factoredInsulin > state.maxBolus {
+                    Text("Max Bolus = \(insulinFormatter(state.maxBolus)) U")
+                } else if state.factoredInsulin > iobAvailable {
+                    let iobFormatted = state.iob < 0 ? "(" + insulinFormatter(state.iob) + ")" : insulinFormatter(state.iob)
+                    Text(
+                        "\(insulinFormatter(state.maxIOB)) - \(iobFormatted) = \(insulinFormatter(iobAvailable)) U"
+                    )
+                    Text("Max IOB - Current IOB")
+                        .font(.caption)
+                        .foregroundColor(.secondary)
+                }
+            }
+            .foregroundColor(Color.loopRed)
+
+            HStack {
+                Text(insulinFormatter(state.insulinCalculated))
+                    .foregroundColor(state.insulinCalculated > 0 ? Color.insulin : .primary)
+                Text("U").foregroundColor(.secondary)
+            }
+            .fontWeight(.bold)
+            .gridColumnAlignment(.trailing)
+        }
+    }
 
 
+    private func insulinFormatter(_ value: Decimal, _ roundingMode: NSDecimalNumber.RoundingMode = .down) -> String {
         let formatter = NumberFormatter()
         let formatter = NumberFormatter()
         formatter.numberStyle = .decimal
         formatter.numberStyle = .decimal
         formatter.minimumFractionDigits = 2
         formatter.minimumFractionDigits = 2
         formatter.maximumFractionDigits = 2
         formatter.maximumFractionDigits = 2
-        formatter.locale = Locale.current // Uses the user's locale
+        formatter.locale = Locale.current
+
+        let handler = NSDecimalNumberHandler(
+            roundingMode: roundingMode,
+            scale: 2,
+            raiseOnExactness: false,
+            raiseOnOverflow: false,
+            raiseOnUnderflow: false,
+            raiseOnDivideByZero: false
+        )   
 
 
-        return formatter.string(from: roundedValue as NSNumber) ?? String(format: "%.2f", toRound)
+        let roundedValue = NSDecimalNumber(decimal: value).rounding(accordingToBehavior: handler)
+
+        return formatter.string(from: roundedValue) ?? "\(value)"
     }
     }
 
 
     struct DividerDouble: View {
     struct DividerDouble: View {
@@ -498,29 +513,17 @@ struct PopupView: View {
     }
     }
 
 
     struct DividerCustom: View {
     struct DividerCustom: View {
+        var height: CGFloat
+
+        init(_ height: CGFloat = 1) {
+            self.height = height
+        }
+
         var body: some View {
         var body: some View {
             Rectangle()
             Rectangle()
-                .frame(height: 1)
+                .frame(height: height)
                 .foregroundColor(.gray.opacity(0.65))
                 .foregroundColor(.gray.opacity(0.65))
                 .padding(.vertical)
                 .padding(.vertical)
         }
         }
     }
     }
 }
 }
-
-extension View {
-    // Function to generate the warning text for max bolus/IOB
-    func getCappedText(insulinCalculated: Decimal, maxBolus: Decimal, maxIOB: Decimal, iob: Decimal) -> Text {
-        let limitedByMaxBolus = insulinCalculated >= maxBolus && maxBolus < maxIOB - iob
-        let limitedByMaxIOB = insulinCalculated >= maxIOB - iob
-        return Text(
-            limitedByMaxBolus ? " ≈ Max Bolus" :
-                limitedByMaxIOB ? " ≈ Max IOB" : ""
-        ).foregroundColor(Color.loopRed)
-    }
-
-    // Function to generate the formula text with opacity
-    func getFormulaText(_ text: String, colorScheme: ColorScheme) -> Text {
-        Text(text)
-            .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
-    }
-}

+ 25 - 18
Trio/Sources/Services/BolusCalculator/BolusCalculationManager.swift

@@ -348,14 +348,15 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
     func calculateInsulin(input: CalculationInput) async -> CalculationResult {
     func calculateInsulin(input: CalculationInput) async -> CalculationResult {
         // insulin needed for the current blood glucose
         // insulin needed for the current blood glucose
         let targetDifference = input.currentBG - input.target
         let targetDifference = input.currentBG - input.target
-        let targetDifferenceInsulin = apsManager.roundBolusNoCap(amount: targetDifference / input.isf)
+
+        let targetDifferenceInsulin = targetDifference / input.isf
 
 
         // more or less insulin because of bg trend in the last 15 minutes
         // more or less insulin because of bg trend in the last 15 minutes
-        let fifteenMinutesInsulin = apsManager.roundBolusNoCap(amount: input.deltaBG / input.isf)
+        let fifteenMinutesInsulin = input.deltaBG / input.isf
 
 
         // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
         // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
         let wholeCob = min(Decimal(input.cob) + input.carbs, input.maxCOB)
         let wholeCob = min(Decimal(input.cob) + input.carbs, input.maxCOB)
-        let wholeCobInsulin = apsManager.roundBolusNoCap(amount: wholeCob / input.carbRatio)
+        let wholeCobInsulin = wholeCob / input.carbRatio
 
 
         // determine how much the calculator reduces/ increases the bolus because of IOB
         // determine how much the calculator reduces/ increases the bolus because of IOB
         let iobInsulinReduction = (-1) * input.iob
         let iobInsulinReduction = (-1) * input.iob
@@ -376,30 +377,35 @@ final class BaseBolusCalculationManager: BolusCalculationManager, Injectable {
         }
         }
 
 
         // apply custom factor at the end of the calculations
         // apply custom factor at the end of the calculations
-        let result = wholeCalc * input.fraction
-
         // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
         // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
-        var insulinCalculated: Decimal
+        var factoredInsulin = wholeCalc * input.fraction
         var superBolusInsulin: Decimal = 0
         var superBolusInsulin: Decimal = 0
         if input.useFattyMealCorrectionFactor {
         if input.useFattyMealCorrectionFactor {
-            insulinCalculated = result * input.fattyMealFactor
+            factoredInsulin *= input.fattyMealFactor
         } else if input.useSuperBolus {
         } else if input.useSuperBolus {
             superBolusInsulin = input.sweetMealFactor * input.basal
             superBolusInsulin = input.sweetMealFactor * input.basal
-            insulinCalculated = result + superBolusInsulin
-        } else {
-            insulinCalculated = result
+            factoredInsulin += superBolusInsulin
         }
         }
 
 
-        // display no negative insulinCalculated
-        insulinCalculated = max(insulinCalculated, 0)
-        insulinCalculated = min(insulinCalculated, input.maxBolus)
-        insulinCalculated = min(insulinCalculated, input.maxIOB - input.iob)
-
-        // round calculated recommendation to allowed bolus increment
-        insulinCalculated = apsManager.roundBolus(amount: insulinCalculated)
+        // the final result for recommended insulin amount
+        var insulinCalculated: Decimal
+        // don't recommend insulin when glucose is < 54
+        if input.currentBG < 54 {
+            insulinCalculated = 0
+        } else {
+            // no negative insulinCalculated
+            insulinCalculated = max(factoredInsulin, 0)
+            // don't exceed maxBolus
+            insulinCalculated = min(insulinCalculated, input.maxBolus)
+            // dont exceed maxIOB
+            insulinCalculated = min(insulinCalculated, input.maxIOB - input.iob)
+            // round calculated recommendation to allowed bolus increment
+            insulinCalculated = apsManager.roundBolus(amount: insulinCalculated)
+        }
 
 
         return CalculationResult(
         return CalculationResult(
             insulinCalculated: insulinCalculated,
             insulinCalculated: insulinCalculated,
+            factoredInsulin: factoredInsulin,
             wholeCalc: wholeCalc,
             wholeCalc: wholeCalc,
             correctionInsulin: targetDifferenceInsulin,
             correctionInsulin: targetDifferenceInsulin,
             iobInsulinReduction: iobInsulinReduction,
             iobInsulinReduction: iobInsulinReduction,
@@ -452,7 +458,8 @@ struct CalculationInput: Sendable {
 
 
 /// Results of the bolus calculation
 /// Results of the bolus calculation
 struct CalculationResult: Sendable {
 struct CalculationResult: Sendable {
-    let insulinCalculated: Decimal // Final calculated insulin amount
+    let insulinCalculated: Decimal // Final calculated insulin amount which respects limits
+    let factoredInsulin: Decimal // Total calculation after adjustments
     let wholeCalc: Decimal // Total calculation before adjustments
     let wholeCalc: Decimal // Total calculation before adjustments
     let correctionInsulin: Decimal // Insulin for BG correction
     let correctionInsulin: Decimal // Insulin for BG correction
     let iobInsulinReduction: Decimal // IOB reduction amount
     let iobInsulinReduction: Decimal // IOB reduction amount