Explorar el Código

Initial take on refactoring FPU handling
* Minimum carb equivalents size of 10g
* Maximum carb equivalents size of 33g
* Up to 3 entries, 99g total
* Start adding carb equivalent entries after delay-setting minutes after carb entry
* Space out >1 carb equivalent entries at +30m of initial carb equivalent entry
* Default handling: carb entry, 1st equiv +60, 2nd equiv +60+30, 3rd equiv +60+60 (each calc'd from initial timestamp)

Deniz Cengiz hace 3 meses
padre
commit
56635cb247
Se han modificado 1 ficheros con 140 adiciones y 36 borrados
  1. 140 36
      Trio/Sources/APS/Storage/CarbsStorage.swift

+ 140 - 36
Trio/Sources/APS/Storage/CarbsStorage.swift

@@ -140,6 +140,70 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
      - Returns: A tuple containing the array of future carb entries and the total carb equivalents.
      */
+//    private func processFPU(
+//        entries: [CarbsEntry],
+//        fat: Decimal,
+//        protein: Decimal,
+//        createdAt: Date,
+//        actualDate: Date?
+//    ) -> ([CarbsEntry], Decimal) {
+//        let trioSettings = settings.settings
+//        let providerSettings = settingsProvider.settings
+//
+//        let interval = trioSettings.minuteInterval.clamp(to: providerSettings.minuteInterval)
+//        let timeCap = trioSettings.timeCap.clamp(to: providerSettings.timeCap)
+//        let adjustment = trioSettings.individualAdjustmentFactor.clamp(to: providerSettings.individualAdjustmentFactor)
+//        let delay = trioSettings.delay.clamp(to: providerSettings.delay)
+//
+//        let kcal = protein * 4 + fat * 9
+//        let carbEquivalents = (kcal / 10) * adjustment
+//        let fpus = carbEquivalents / 10
+//        var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
+//
+//        var carbEquivalentSize: Decimal = carbEquivalents / computedDuration
+//        carbEquivalentSize /= Decimal(60) / interval
+//
+//        if carbEquivalentSize < 1.0 {
+//            carbEquivalentSize = 1.0
+//            computedDuration = min(carbEquivalents / carbEquivalentSize, timeCap)
+//        }
+//
+//        let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
+//        carbEquivalentSize = Decimal(roundedEquivalent)
+//        var numberOfEquivalents = carbEquivalents / carbEquivalentSize
+//
+//        var useDate = actualDate ?? createdAt
+//        let fpuID = entries.first?.fpuID ?? UUID().uuidString
+//        var futureCarbArray = [CarbsEntry]()
+//        var firstIndex = true
+//
+//        // convert Decimal minutes to TimeInterval in seconds
+//        let delayTimeInterval = TimeInterval(delay * 60)
+//        let intervalTimeInterval = TimeInterval(interval * 60)
+//        while carbEquivalents > 0, numberOfEquivalents > 0 {
+//            useDate = firstIndex ? useDate.addingTimeInterval(delayTimeInterval) : useDate
+//                .addingTimeInterval(intervalTimeInterval)
+//            firstIndex = false
+    // g
+//            let eachCarbEntry = CarbsEntry(
+//                id: UUID().uuidString,
+//                createdAt: createdAt,
+//                actualDate: useDate,
+//                carbs: carbEquivalentSize,
+//                fat: 0,
+//                protein: 0,
+//                note: nil,
+//                enteredBy: CarbsEntry.local,
+//                isFPU: true,
+//                fpuID: fpuID
+//            )
+//            futureCarbArray.append(eachCarbEntry)
+//            numberOfEquivalents -= 1
+//        }
+//
+//        return (futureCarbArray, carbEquivalents)
+//    }
+
     private func processFPU(
         entries: [CarbsEntry],
         fat: Decimal,
@@ -147,49 +211,48 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         createdAt: Date,
         actualDate: Date?
     ) -> ([CarbsEntry], Decimal) {
-        let trioSettings = settings.settings
-        let providerSettings = settingsProvider.settings
+        let trio = settings.settings
+        let provider = settingsProvider.settings
 
-        let interval = trioSettings.minuteInterval.clamp(to: providerSettings.minuteInterval)
-        let timeCap = trioSettings.timeCap.clamp(to: providerSettings.timeCap)
-        let adjustment = trioSettings.individualAdjustmentFactor.clamp(to: providerSettings.individualAdjustmentFactor)
-        let delay = trioSettings.delay.clamp(to: providerSettings.delay)
+        let adjustment = trio.individualAdjustmentFactor
+            .clamp(to: provider.individualAdjustmentFactor)
 
-        let kcal = protein * 4 + fat * 9
-        let carbEquivalents = (kcal / 10) * adjustment
-        let fpus = carbEquivalents / 10
-        var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap)
+        let delayMinutes = trio.delay
+            .clamp(to: provider.delay)
 
-        var carbEquivalentSize: Decimal = carbEquivalents / computedDuration
-        carbEquivalentSize /= Decimal(60) / interval
+        // Constraints
+        let maxTotalGrams = 99
+        let maxEntries = 3
+        let maxPerEntry = 33
+        let minPerEntry = 10
+        let spacing: TimeInterval = 30 * 60
+
+        // kcal -> carb equivalents (kcal/10 * adjustment), rounded down to whole grams
+        let kcal = protein * 4 + fat * 9
+        let rawEquivalents = Int((kcal / 10) * adjustment)
+        let totalGrams = min(maxTotalGrams, max(0, rawEquivalents))
 
-        if carbEquivalentSize < 1.0 {
-            carbEquivalentSize = 1.0
-            computedDuration = min(carbEquivalents / carbEquivalentSize, timeCap)
+        guard totalGrams >= minPerEntry else {
+            return ([], Decimal(totalGrams))
         }
 
-        let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10
-        carbEquivalentSize = Decimal(roundedEquivalent)
-        var numberOfEquivalents = carbEquivalents / carbEquivalentSize
+        let amounts = splitIntoCarbEquivalents(
+            total: totalGrams,
+            maxEntries: maxEntries,
+            maxPerEntry: maxPerEntry,
+            minPerEntry: minPerEntry
+        )
 
-        var useDate = actualDate ?? createdAt
+        let baseDate = actualDate ?? createdAt
+        let start = baseDate.addingTimeInterval(TimeInterval(delayMinutes * 60))
         let fpuID = entries.first?.fpuID ?? UUID().uuidString
-        var futureCarbArray = [CarbsEntry]()
-        var firstIndex = true
-
-        // convert Decimal minutes to TimeInterval in seconds
-        let delayTimeInterval = TimeInterval(delay * 60)
-        let intervalTimeInterval = TimeInterval(interval * 60)
-        while carbEquivalents > 0, numberOfEquivalents > 0 {
-            useDate = firstIndex ? useDate.addingTimeInterval(delayTimeInterval) : useDate
-                .addingTimeInterval(intervalTimeInterval)
-            firstIndex = false
-
-            let eachCarbEntry = CarbsEntry(
+
+        let futureEntries: [CarbsEntry] = amounts.enumerated().map { idx, grams in
+            CarbsEntry(
                 id: UUID().uuidString,
                 createdAt: createdAt,
-                actualDate: useDate,
-                carbs: carbEquivalentSize,
+                actualDate: start.addingTimeInterval(TimeInterval(idx) * spacing),
+                carbs: Decimal(grams),
                 fat: 0,
                 protein: 0,
                 note: nil,
@@ -197,11 +260,52 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 isFPU: true,
                 fpuID: fpuID
             )
-            futureCarbArray.append(eachCarbEntry)
-            numberOfEquivalents -= 1
         }
 
-        return (futureCarbArray, carbEquivalents)
+        let totalScheduled = futureEntries.reduce(into: Decimal(0)) { $0 += $1.carbs }
+        return (futureEntries, totalScheduled)
+    }
+
+    // MARK: - Helpers
+
+    private func splitIntoCarbEquivalents(
+        total: Int,
+        maxEntries: Int,
+        maxPerEntry: Int,
+        minPerEntry: Int
+    ) -> [Int] {
+        guard total >= minPerEntry else { return [] }
+
+        // Choose an entry count that *guarantees* each entry can be <= maxPerEntry
+        let needed = (total + maxPerEntry - 1) / maxPerEntry
+        let count = min(maxEntries, max(1, needed))
+
+        // Even split (difference between buckets is at most 1)
+        func evenSplit(_ total: Int, count: Int) -> [Int] {
+            let base = total / count
+            let rem = total % count
+            return (0 ..< count).map { base + ($0 < rem ? 1 : 0) }
+        }
+
+        var buckets = evenSplit(total, count: count)
+
+        // Enforce minPerEntry by merging any too-small tail bucket into the previous one
+        // This should be rare, but it keeps the invariant
+        if buckets.count > 1 {
+            for i in stride(from: buckets.count - 1, through: 1, by: -1) {
+                let v = buckets[i]
+                guard v > 0, v < minPerEntry else { continue }
+                buckets[i - 1] += v
+                buckets[i] = 0
+            }
+            buckets = buckets.filter { $0 > 0 }
+        }
+
+        // Guarantee not to exceed maxPerEntry if merging a reduced count
+        // Clamp as final guard here
+        buckets = buckets.map { min(maxPerEntry, $0) }.filter { $0 >= minPerEntry }
+
+        return buckets
     }
 
     private func saveCarbEquivalents(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {