Explorar o código

Refactor MealHistory.findMealInputs()
* Use compactMap and Set-based deduping
* Replace quadratic lookup (O(n^2) with O(n) merge of carb & bolus entries
* Confirm same results via unit tests ✅

Deniz Cengiz hai 11 meses
pai
achega
c475dc498b
Modificáronse 1 ficheiros con 48 adicións e 92 borrados
  1. 48 92
      Trio/Sources/APS/OpenAPSSwift/Meal/MealHistory.swift

+ 48 - 92
Trio/Sources/APS/OpenAPSSwift/Meal/MealHistory.swift

@@ -6,113 +6,69 @@ struct MealInput {
     var carbs: Decimal? /// `current.carbs`
     var bolus: Decimal? /// from `current.amount` in Bolus events
     /// omitting nsCarbs, bwCarbs, journalCarbs
+
+    enum InputType: String {
+        case carbs
+        case bolus
+    }
 }
 
 enum MealHistory {
-    /// Checks if `array` contains a MealInput with an entry that is ± 2 seconds around `t`.
-    /// and a non-nil property given by propName ("carbs", "bolus", etc.).
-    static func arrayHasElementWithSameTimestampAndProperty(
-        mealInputs: [MealInput],
-        dateTime: Date,
-        propName: String
-    ) -> Bool {
-        // Create upper and lower bound, i.e. ± 2 seconds around t
-        let tMin = dateTime.addingTimeInterval(-2)
-        let tMax = dateTime.addingTimeInterval(2)
-
-        return mealInputs.contains { input in
-            // Timestamp close enough?
-            guard input.timestamp >= tMin, input.timestamp <= tMax else {
-                return false
-            }
-
-            // Check the property name
-            switch propName {
-            case "carbs":
-                return input.carbs != nil
-            case "bolus":
-                return input.bolus != nil
-            default:
-                return false
-            }
-        }
+    private struct MealInputKey: Hashable {
+        let timestamp: Date
+        let type: MealInput.InputType
     }
 
-    // the overall function signature (from oref) should be this one:
-    //    static func findMealInputs(
-    //        pumpHistory: [PumpHistoryEvent],
-    //        profile _: Profile,
-    //        basalProfile _: [BasalProfileEntry],
-    //        clock _: Date,
-    //        carbHistory: [CarbsEntry],
-    //        glucoseHistory _: [BloodGlucose]
-    //    ) -> [MealInput] {
-    // however, we only require pumpHistory and carbHistory, so omiting the unused parameters
     static func findMealInputs(
         pumpHistory: [PumpHistoryEvent],
         carbHistory: [CarbsEntry]
     ) -> [MealInput] {
-        var mealInputs: [MealInput] = []
-        var duplicates = 0
-
-        // Process carbHistory
-        for current in carbHistory {
-            // The JS code checks `if (current.carbs && current.created_at)`
-            // In Swift, that's basically "non-nil carbs" and we rely on the type's Date.
-            if current.carbs > 0 {
-                let temp = MealInput(
-                    timestamp: current.createdAt,
-                    carbs: current.carbs,
-                    bolus: nil
-                )
+        let carbInputs = carbHistory.compactMap { entry -> MealInput? in
+            guard entry.carbs > 0 else { return nil }
+            return MealInput(
+                timestamp: entry.createdAt,
+                carbs: entry.carbs,
+                bolus: nil
+            )
+        }
 
-                if !arrayHasElementWithSameTimestampAndProperty(
-                    mealInputs: mealInputs,
-                    dateTime: current.createdAt,
-                    propName: "carbs"
-                ) {
-                    mealInputs.append(temp)
-                } else {
-                    duplicates += 1
-                }
-            }
+        let bolusInputs = pumpHistory.compactMap { ev -> MealInput? in
+            guard ev.type == .bolus, let amt = ev.amount else { return nil }
+            return MealInput(
+                timestamp: ev.timestamp,
+                carbs: nil,
+                bolus: amt
+            )
         }
 
-        // Process pumpHistory
-        for current in pumpHistory {
-            // bolus event handling
-            if current.type == .bolus, let amount = current.amount {
-                let temp = MealInput(
-                    timestamp: current.timestamp,
-                    carbs: nil,
-                    bolus: amount
-                )
+        let combinedIputs = carbInputs + bolusInputs
+        var seenBuckets: [MealInput.InputType: Set<Int>] = [
+            .carbs: Set(),
+            .bolus: Set()
+        ]
 
-                if !arrayHasElementWithSameTimestampAndProperty(
-                    mealInputs: mealInputs,
-                    dateTime: current.timestamp,
-                    propName: "bolus"
-                ) {
-                    mealInputs.append(temp)
-                } else {
-                    duplicates += 1
-                }
-            }
+        var dedupedInputs: [MealInput] = []
+        dedupedInputs.reserveCapacity(combinedIputs.count)
 
-            // Trio will never send any pump history contents of the following types to oref
-            // Ignoring for JavaScript -> Swift port.
-            // .bolusWizard
-            // .mealBolus
-            // .correctionBolus
-            // .snackBolus
-            // .nsCarbCorrection
-            // .journalCarbs
-            // and the `carbsVal = current.carbInput` handling
-        }
+        for input in combinedIputs {
+            let type: MealInput.InputType = input.carbs != nil ? .carbs : .bolus
+            let tSec = Int(input.timestamp.timeIntervalSince1970)
 
-        // We can also omit the deferred bolus wizard input processing
-        // TODO: log duplicates?
+            // check if any second in [tSec-2 ... tSec+2] is already in our bucket
+            let bucket = seenBuckets[type]!
+            let isDuplicate = (tSec - 2 ... tSec + 2).contains { bucket.contains($0) }
+
+            if !isDuplicate {
+                dedupedInputs.append(input)
+
+                /// copies out bucket, mutates it, writes it back
+                /// ensuring every entry exists at least once, but is properly deduped
+                var newBucket = bucket
+                newBucket.insert(tSec)
+                seenBuckets[type] = newBucket
+            }
+        }
 
-        return mealInputs
+        return dedupedInputs
     }
 }