Преглед изворни кода

Core logging functionality for IoB swift function

Sam King пре 1 година
родитељ
комит
09b4843758

+ 20 - 24
Trio.xcodeproj/project.pbxproj

@@ -210,15 +210,14 @@
 		3B1C5C302D68E220004E9273 /* ComputedPumpHistoryEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C2D2D68E220004E9273 /* ComputedPumpHistoryEvent.swift */; };
 		3B1C5C332D68E233004E9273 /* PumpHistory+copy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C312D68E233004E9273 /* PumpHistory+copy.swift */; };
 		3B1C5C342D68E233004E9273 /* TimeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C322D68E233004E9273 /* TimeExtensions.swift */; };
-		3B1C5C402D68E269004E9273 /* iob_result.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B1C5C3A2D68E269004E9273 /* iob_result.json */; };
-		3B1C5C412D68E269004E9273 /* iob_history.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B1C5C392D68E269004E9273 /* iob_history.json */; };
-		3B1C5C422D68E269004E9273 /* pump_history.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B1C5C3B2D68E269004E9273 /* pump_history.json */; };
 		3B1C5C432D68E269004E9273 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C3D2D68E269004E9273 /* Extensions.swift */; };
 		3B1C5C442D68E269004E9273 /* IobJsonTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C3E2D68E269004E9273 /* IobJsonTypes.swift */; };
 		3B1C5C452D68E269004E9273 /* IobTotalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C382D68E269004E9273 /* IobTotalTests.swift */; };
-		3B1C5C462D68E269004E9273 /* IobJsonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C372D68E269004E9273 /* IobJsonTests.swift */; };
 		3B1C5C472D68E269004E9273 /* IobCalculateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C352D68E269004E9273 /* IobCalculateTests.swift */; };
 		3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C362D68E269004E9273 /* IobHistoryTests.swift */; };
+		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
+		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
+		3B4550532D862C0000551B0D /* PumpHistoryEvent+Duplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */; };
 		3B5CD1EC2D4912A600CE213C /* OpenAPSSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD1E92D4912A600CE213C /* OpenAPSSwift.swift */; };
 		3B5CD1ED2D4912A600CE213C /* JSONBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD1EA2D4912A600CE213C /* JSONBridge.swift */; };
 		3B5CD2982D4AEA3C00CE213C /* Carbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD2922D4AEA3C00CE213C /* Carbs.swift */; };
@@ -237,6 +236,10 @@
 		3B5CD2CB2D4AECD500CE213C /* ProfileTargetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD2C62D4AECD500CE213C /* ProfileTargetsTests.swift */; };
 		3B5CD2CD2D4AECD500CE213C /* ProfileIsfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD2C42D4AECD500CE213C /* ProfileIsfTests.swift */; };
 		3B5CD2CE2D4AECD500CE213C /* ProfileBasalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD2C12D4AECD500CE213C /* ProfileBasalTests.swift */; };
+		3B5F45B62D6A239500F70982 /* DoubleApproximateMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5F45B52D6A239000F70982 /* DoubleApproximateMatching.swift */; };
+		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
+		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
+		3BC26E552D7418830066ACD6 /* IobSuspendTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BC26E542D7418830066ACD6 /* IobSuspendTests.swift */; };
 		3BCE75B32D4B38AE009E9453 /* InsulinSensitivities+Convert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */; };
 		3BCE75B52D4B391F009E9453 /* Decimal+rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BCE75B42D4B3917009E9453 /* Decimal+rounding.swift */; };
 		3BEA3AE02D58F79700A67A1D /* OrefFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEA3ADE2D58F79700A67A1D /* OrefFunction.swift */; };
@@ -245,10 +248,6 @@
 		3BEA3AE32D58F79700A67A1D /* JSONCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BEA3ADC2D58F79700A67A1D /* JSONCompare.swift */; };
 		3BF8D0C12D5175BE001B3F84 /* ProfileJsNativeCompareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF8D0C02D5175B3001B3F84 /* ProfileJsNativeCompareTests.swift */; };
 		3BF8D14B2D530397001B3F84 /* JSONCompareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF8D14A2D530397001B3F84 /* JSONCompareTests.swift */; };
-		3B2F77862D7E52ED005ED9FA /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77852D7E52ED005ED9FA /* TDD.swift */; };
-		3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */; };
-		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
-		3BAD36CC2D7D420E00CC298D /* CoreDataInitializationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; };
@@ -976,13 +975,12 @@
 		3B1C5C322D68E233004E9273 /* TimeExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeExtensions.swift; sourceTree = "<group>"; };
 		3B1C5C352D68E269004E9273 /* IobCalculateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobCalculateTests.swift; sourceTree = "<group>"; };
 		3B1C5C362D68E269004E9273 /* IobHistoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobHistoryTests.swift; sourceTree = "<group>"; };
-		3B1C5C372D68E269004E9273 /* IobJsonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobJsonTests.swift; sourceTree = "<group>"; };
 		3B1C5C382D68E269004E9273 /* IobTotalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobTotalTests.swift; sourceTree = "<group>"; };
-		3B1C5C392D68E269004E9273 /* iob_history.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = iob_history.json; sourceTree = "<group>"; };
-		3B1C5C3A2D68E269004E9273 /* iob_result.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = iob_result.json; sourceTree = "<group>"; };
-		3B1C5C3B2D68E269004E9273 /* pump_history.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = pump_history.json; sourceTree = "<group>"; };
 		3B1C5C3D2D68E269004E9273 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
 		3B1C5C3E2D68E269004E9273 /* IobJsonTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobJsonTypes.swift; sourceTree = "<group>"; };
+		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
+		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
+		3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpHistoryEvent+Duplicates.swift"; sourceTree = "<group>"; };
 		3B5CD1E92D4912A600CE213C /* OpenAPSSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPSSwift.swift; sourceTree = "<group>"; };
 		3B5CD1EA2D4912A600CE213C /* JSONBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONBridge.swift; sourceTree = "<group>"; };
 		3B5CD2912D4AEA3C00CE213C /* Basal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Basal.swift; sourceTree = "<group>"; };
@@ -1001,12 +999,12 @@
 		3B5CD2C42D4AECD500CE213C /* ProfileIsfTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileIsfTests.swift; sourceTree = "<group>"; };
 		3B5CD2C52D4AECD500CE213C /* ProfileJavascriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileJavascriptTests.swift; sourceTree = "<group>"; };
 		3B5CD2C62D4AECD500CE213C /* ProfileTargetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTargetsTests.swift; sourceTree = "<group>"; };
-		3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InsulinSensitivities+Convert.swift"; sourceTree = "<group>"; };
-		3BCE75B42D4B3917009E9453 /* Decimal+rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+rounding.swift"; sourceTree = "<group>"; };
-		3B2F77852D7E52ED005ED9FA /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
-		3B2F77872D7E5387005ED9FA /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
+		3B5F45B52D6A239000F70982 /* DoubleApproximateMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleApproximateMatching.swift; sourceTree = "<group>"; };
 		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BAD36CB2D7D420500CC298D /* CoreDataInitializationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataInitializationCoordinator.swift; sourceTree = "<group>"; };
+		3BC26E542D7418830066ACD6 /* IobSuspendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobSuspendTests.swift; sourceTree = "<group>"; };
+		3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InsulinSensitivities+Convert.swift"; sourceTree = "<group>"; };
+		3BCE75B42D4B3917009E9453 /* Decimal+rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+rounding.swift"; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BEA3ADB2D58F79700A67A1D /* AlgorithmComparison.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlgorithmComparison.swift; sourceTree = "<group>"; };
 		3BEA3ADC2D58F79700A67A1D /* JSONCompare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCompare.swift; sourceTree = "<group>"; };
@@ -2446,9 +2444,6 @@
 		3B1C5C3C2D68E269004E9273 /* json */ = {
 			isa = PBXGroup;
 			children = (
-				3B1C5C392D68E269004E9273 /* iob_history.json */,
-				3B1C5C3A2D68E269004E9273 /* iob_result.json */,
-				3B1C5C3B2D68E269004E9273 /* pump_history.json */,
 			);
 			path = json;
 			sourceTree = "<group>";
@@ -2501,6 +2496,8 @@
 		3B5CD2A42D4AEA5D00CE213C /* Extensions */ = {
 			isa = PBXGroup;
 			children = (
+				3B4550522D862BF200551B0D /* PumpHistoryEvent+Duplicates.swift */,
+				3B5F45B52D6A239000F70982 /* DoubleApproximateMatching.swift */,
 				3B5CD2A32D4AEA5D00CE213C /* Date+MinutesFromMidnight.swift */,
 				3BCE75B42D4B3917009E9453 /* Decimal+rounding.swift */,
 				3BCE75B22D4B38A0009E9453 /* InsulinSensitivities+Convert.swift */,
@@ -2529,7 +2526,7 @@
 				3B1C5C3F2D68E269004E9273 /* utils */,
 				3B1C5C352D68E269004E9273 /* IobCalculateTests.swift */,
 				3B1C5C362D68E269004E9273 /* IobHistoryTests.swift */,
-				3B1C5C372D68E269004E9273 /* IobJsonTests.swift */,
+				3BC26E542D7418830066ACD6 /* IobSuspendTests.swift */,
 				3B1C5C382D68E269004E9273 /* IobTotalTests.swift */,
 				3BF8D14A2D530397001B3F84 /* JSONCompareTests.swift */,
 				3BF8D0C02D5175B3001B3F84 /* ProfileJsNativeCompareTests.swift */,
@@ -3783,9 +3780,6 @@
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				3B1C5C402D68E269004E9273 /* iob_result.json in Resources */,
-				3B1C5C412D68E269004E9273 /* iob_history.json in Resources */,
-				3B1C5C422D68E269004E9273 /* pump_history.json in Resources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -4033,6 +4027,7 @@
 				DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */,
 				DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */,
 				DD1745482C55C61D00211FAC /* AutosensSettingsStateModel.swift in Sources */,
+				3B4550532D862C0000551B0D /* PumpHistoryEvent+Duplicates.swift in Sources */,
 				DD1745462C55C61500211FAC /* AutosensSettingsProvider.swift in Sources */,
 				DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
@@ -4203,6 +4198,7 @@
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				BD249DA72D42FE4600412DEB /* Calendar+GlucoseStatsChart.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
+				3B5F45B62D6A239500F70982 /* DoubleApproximateMatching.swift in Sources */,
 				58D08B342C8DF9A700AA37D3 /* CobIobChart.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
 				BD249D9B2D42FCDB00412DEB /* LoopChartSetup.swift in Sources */,
@@ -4472,9 +4468,9 @@
 				BD8FC05B2D6618AF00B95AED /* DeterminationStorageTests.swift in Sources */,
 				CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */,
 				3B1C5C432D68E269004E9273 /* Extensions.swift in Sources */,
+				3BC26E552D7418830066ACD6 /* IobSuspendTests.swift in Sources */,
 				3B1C5C442D68E269004E9273 /* IobJsonTypes.swift in Sources */,
 				3B1C5C452D68E269004E9273 /* IobTotalTests.swift in Sources */,
-				3B1C5C462D68E269004E9273 /* IobJsonTests.swift in Sources */,
 				3B1C5C472D68E269004E9273 /* IobCalculateTests.swift in Sources */,
 				3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,

+ 6 - 1
Trio/Sources/APS/APSManager.swift

@@ -452,7 +452,11 @@ final class BaseAPSManager: APSManager, Injectable {
 
             _ = try await autosenseResult
             try await openAPS.createProfiles(useSwiftOref: settings.useSwiftOref)
-            let determination = try await openAPS.determineBasal(currentTemp: await currentTemp, clock: now)
+            let determination = try await openAPS.determineBasal(
+                currentTemp: await currentTemp,
+                clock: now,
+                useSwiftOref: settings.useSwiftOref
+            )
 
             if let determination = determination {
                 // Capture weak self in closure
@@ -478,6 +482,7 @@ final class BaseAPSManager: APSManager, Injectable {
             return try await openAPS.determineBasal(
                 currentTemp: temp,
                 clock: Date(),
+                useSwiftOref: settings.useSwiftOref,
                 simulatedCarbsAmount: simulatedCarbsAmount,
                 simulatedBolusAmount: simulatedBolusAmount,
                 simulation: true

+ 58 - 17
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -265,7 +265,8 @@ final class OpenAPS {
 
     func determineBasal(
         currentTemp: TempBasal,
-        clock: Date = Date(),
+        clock: Date,
+        useSwiftOref: Bool,
         simulatedCarbsAmount: Decimal? = nil,
         simulatedBolusAmount: Decimal? = nil,
         simulation: Bool = false
@@ -324,7 +325,8 @@ final class OpenAPS {
             pumphistory: pumpHistoryJSON,
             profile: profile,
             clock: clock,
-            autosens: autosens.isEmpty ? .null : autosens
+            autosens: autosens.isEmpty ? .null : autosens,
+            useSwiftOref: useSwiftOref
         )
 
         // TODO: refactor this to core data
@@ -563,22 +565,61 @@ final class OpenAPS {
         }
     }
 
-    private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async throws -> RawJSON {
-        try await withCheckedThrowingContinuation { continuation in
-            jsWorker.inCommonContext { worker in
-                worker.evaluateBatch(scripts: [
-                    Script(name: Prepare.log),
-                    Script(name: Bundle.iob),
-                    Script(name: Prepare.iob)
-                ])
-                let result = worker.call(function: Function.generate, with: [
-                    pumphistory,
-                    profile,
-                    clock,
-                    autosens
-                ])
-                continuation.resume(returning: result)
+    private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON, useSwiftOref: Bool) async throws -> RawJSON {
+        // FIXME: For now we'll just remove duplicate suspends here (ISSUE-399)
+        var pumphistory = pumphistory
+        if let pumpHistoryArray = try? JSONBridge.pumpHistory(from: pumphistory) {
+            pumphistory = pumpHistoryArray.removingDuplicateSuspendResumeEvents().rawJSON
+        }
+
+        let startJavascriptAt = Date()
+        let jsResult = await iobJavascript(pumphistory: pumphistory, profile: profile, clock: clock, autosens: autosens)
+        let javascriptDuration = Date().timeIntervalSince(startJavascriptAt)
+
+        // Important: we want to make sure that this flag ensures that none
+        // of the native code runs
+        guard useSwiftOref else {
+            return try jsResult.returnOrThrow()
+        }
+
+        let startSwiftAt = Date()
+        let (swiftResult, iobInputs) = OpenAPSSwift
+            .iob(pumphistory: pumphistory, profile: profile, clock: clock, autosens: autosens)
+        let swiftDuration = Date().timeIntervalSince(startSwiftAt)
+
+        JSONCompare.logDifferences(
+            function: .iob,
+            swift: swiftResult,
+            swiftDuration: swiftDuration,
+            javascript: jsResult,
+            javascriptDuration: javascriptDuration,
+            iobInputs: iobInputs
+        )
+
+        return try jsResult.returnOrThrow()
+    }
+
+    func iobJavascript(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async -> OrefFunctionResult {
+        do {
+            let result = try await withCheckedThrowingContinuation { continuation in
+                jsWorker.inCommonContext { worker in
+                    worker.evaluateBatch(scripts: [
+                        Script(name: Prepare.log),
+                        Script(name: Bundle.iob),
+                        Script(name: Prepare.iob)
+                    ])
+                    let result = worker.call(function: Function.generate, with: [
+                        pumphistory,
+                        profile,
+                        clock,
+                        autosens
+                    ])
+                    continuation.resume(returning: result)
+                }
             }
+            return .success(result)
+        } catch {
+            return .failure(error)
         }
     }
 

+ 21 - 0
Trio/Sources/APS/OpenAPSSwift/Extensions/DoubleApproximateMatching.swift

@@ -0,0 +1,21 @@
+extension Double {
+    func isApproximatelyEqual(to other: Double, epsilon: Double?) -> Bool {
+        // If no epsilon provided, require exact match
+        guard let epsilon = epsilon else {
+            return self == other
+        }
+
+        // Handle exact equality
+        if self == other {
+            return true
+        }
+
+        // Handle infinity and NaN
+        if isInfinite || other.isInfinite || isNaN || other.isNaN {
+            return self == other
+        }
+
+        // For IOB values, use simple absolute difference
+        return abs(self - other) <= epsilon
+    }
+}

+ 18 - 0
Trio/Sources/APS/OpenAPSSwift/Extensions/PumpHistoryEvent+Duplicates.swift

@@ -0,0 +1,18 @@
+import Foundation
+
+extension Array where Element == PumpHistoryEvent {
+    /// Removes duplicate PumpSuspend events from the array
+    /// - Returns: A new array with duplicate suspend events removed
+    func removingDuplicateSuspendResumeEvents() -> [PumpHistoryEvent] {
+        var seenSuspendResume = Set<Date>()
+
+        return filter { event in
+            if event.type != .pumpSuspend, event.type != .pumpResume {
+                return true
+            }
+
+            // Make suspend/resume events unique by timestamp
+            return seenSuspendResume.insert(event.timestamp).inserted
+        }
+    }
+}

+ 27 - 0
Trio/Sources/APS/OpenAPSSwift/Logging/AlgorithmComparison.swift

@@ -63,10 +63,19 @@ enum ComparisonResultType: String, Codable {
     case comparisonError // The comparison algorithm itself failed
 }
 
+/// For tracking inputs to IoB when there is a mismatch
+struct IobInputs: Codable {
+    let history: [PumpHistoryEvent]
+    let profile: Profile
+    let clock: Date
+    let autosens: Autosens?
+}
+
 /// Represents a complete comparison between JS and Swift implementations
 struct AlgorithmComparison: Codable {
     let id: UUID
     let createdAt: Date
+    let timezone: String
     let function: OrefFunction
     let resultType: ComparisonResultType
 
@@ -82,6 +91,21 @@ struct AlgorithmComparison: Codable {
     let swiftException: AlgorithmException?
     let comparisonError: AlgorithmException?
 
+    // Inputs for mismatches
+    let iobInput: IobInputs?
+
+    #if targetEnvironment(simulator)
+        static let isSimulator = true
+    #else
+        static let isSimulator = false
+    #endif
+
+    #if DEBUG
+        static let isDebugBuild = true
+    #else
+        static let isDebugBuild = false
+    #endif
+
     init(
         function: OrefFunction,
         resultType: ComparisonResultType,
@@ -91,6 +115,7 @@ struct AlgorithmComparison: Codable {
         jsException: AlgorithmException? = nil,
         swiftException: AlgorithmException? = nil,
         comparisonError: AlgorithmException? = nil,
+        iobInputs: IobInputs? = nil,
         id: UUID = UUID(),
         createdAt: Date = Date()
     ) {
@@ -104,5 +129,7 @@ struct AlgorithmComparison: Codable {
         self.jsException = jsException
         self.swiftException = swiftException
         self.comparisonError = comparisonError
+        iobInput = iobInputs
+        timezone = TimeZone.current.identifier
     }
 }

+ 74 - 16
Trio/Sources/APS/OpenAPSSwift/Logging/JSONCompare.swift

@@ -83,21 +83,21 @@ enum JSONCompare {
         swift: OrefFunctionResult,
         swiftDuration: TimeInterval,
         javascript: OrefFunctionResult,
-        javascriptDuration: TimeInterval
+        javascriptDuration: TimeInterval,
+        iobInputs: IobInputs? = nil
     ) {
         let comparison = createComparison(
             function: function,
             swift: swift,
             swiftDuration: swiftDuration,
             javascript: javascript,
-            javascriptDuration: javascriptDuration
+            javascriptDuration: javascriptDuration,
+            iobInputs: iobInputs
         )
 
         Task {
             do {
                 try await log?.logComparison(comparison: comparison)
-                debug(.openAPS, "\(function) -> n: \(swiftDuration)s, js: \(javascriptDuration)s")
-                prettyPrint(comparison.differences ?? [:])
             } catch {
                 warning(.openAPS, "logComparison exception: \(error)", error: error)
             }
@@ -109,7 +109,8 @@ enum JSONCompare {
         swift: OrefFunctionResult,
         swiftDuration: TimeInterval,
         javascript: OrefFunctionResult,
-        javascriptDuration: TimeInterval
+        javascriptDuration: TimeInterval,
+        iobInputs: IobInputs?
     ) -> AlgorithmComparison {
         switch (swift, javascript) {
         case let (.success(swiftJson), .success(javascriptJson)):
@@ -121,7 +122,8 @@ enum JSONCompare {
                     resultType: resultType,
                     jsDuration: javascriptDuration,
                     swiftDuration: swiftDuration,
-                    differences: differences.isEmpty ? nil : differences
+                    differences: differences.isEmpty ? nil : differences,
+                    iobInputs: differences.isEmpty ? nil : iobInputs
                 )
             } catch {
                 return AlgorithmComparison(
@@ -146,7 +148,8 @@ enum JSONCompare {
                 function: function,
                 resultType: .swiftOnlyException,
                 jsDuration: javascriptDuration,
-                swiftException: AlgorithmException(error: swiftError)
+                swiftException: AlgorithmException(error: swiftError),
+                iobInputs: iobInputs
             )
 
         case let (.success, .failure(jsError)):
@@ -154,7 +157,8 @@ enum JSONCompare {
                 function: function,
                 resultType: .jsOnlyException,
                 swiftDuration: swiftDuration,
-                jsException: AlgorithmException(error: jsError)
+                jsException: AlgorithmException(error: jsError),
+                iobInputs: iobInputs
             )
         }
     }
@@ -170,7 +174,45 @@ enum JSONCompare {
         }
     }
 
-    static func differences(
+    static func differences(function: OrefFunction, swift: String, javascript: String) throws -> [String: ValueDifference] {
+        let differences = try {
+            switch function.returnType() {
+            case .array:
+                return try differencesArray(function: function, swift: swift, javascript: javascript)
+            case .dictionary:
+                return try differencesDictionary(function: function, swift: swift, javascript: javascript)
+            }
+        }()
+
+        let keysToIgnore = function.keysToIgnore()
+        return differences.filter { !keysToIgnore.contains($0.key) }
+    }
+
+    private static func differencesArray(
+        function: OrefFunction,
+        swift: String,
+        javascript: String
+    ) throws -> [String: ValueDifference] {
+        guard let jsData = javascript.data(using: .utf8),
+              let swiftData = swift.data(using: .utf8),
+              let jsArray = try JSONSerialization.jsonObject(with: jsData) as? [Any],
+              let swiftArray = try JSONSerialization.jsonObject(with: swiftData) as? [Any]
+        else {
+            throw NSError(domain: "JSONBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])
+        }
+
+        // Converting arrays into dictionaries for comparison
+        let jsDict = Dictionary(uniqueKeysWithValues: jsArray.enumerated().map { index, value in
+            ("[\(index)]", value)
+        })
+        let swiftDict = Dictionary(uniqueKeysWithValues: swiftArray.enumerated().map { index, value in
+            ("[\(index)]", value)
+        })
+
+        return compareDict(function: function, swiftDict: swiftDict, jsDict: jsDict)
+    }
+
+    private static func differencesDictionary(
         function: OrefFunction,
         swift: String,
         javascript: String
@@ -183,14 +225,23 @@ enum JSONCompare {
             throw NSError(domain: "JSONBridge", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON format"])
         }
 
+        return compareDict(function: function, swiftDict: swiftDict, jsDict: jsDict)
+    }
+
+    private static func compareDict(
+        function: OrefFunction,
+        swiftDict: [String: Any],
+        jsDict: [String: Any]
+    ) -> [String: ValueDifference] {
         var differences: [String: ValueDifference] = [:]
+        let approximateKeys = function.approximateMatchingNumbers()
 
         // Check all keys present in either dictionary
         Set(jsDict.keys).union(swiftDict.keys).forEach { key in
             let jsValue = jsDict[key].map(convertToJSONValue) ?? .null
             let swiftValue = swiftDict[key].map(convertToJSONValue) ?? .null
 
-            if !valuesAreEqual(jsValue, swiftValue) {
+            if !valuesAreEqual(jsValue, swiftValue, approximately: approximateKeys[key], approximateKeys: approximateKeys) {
                 differences[key] = ValueDifference(
                     js: jsValue,
                     swift: swiftValue,
@@ -200,8 +251,7 @@ enum JSONCompare {
             }
         }
 
-        let keysToIgnore = function.keysToIgnore()
-        return differences.filter { !keysToIgnore.contains($0.key) }
+        return differences
     }
 
     private static func convertToJSONValue(_ value: Any) -> JSONValue {
@@ -230,22 +280,30 @@ enum JSONCompare {
         }
     }
 
-    private static func valuesAreEqual(_ value1: JSONValue, _ value2: JSONValue) -> Bool {
+    private static func valuesAreEqual(
+        _ value1: JSONValue,
+        _ value2: JSONValue,
+        approximately: Double?,
+        approximateKeys: [String: Double]
+    ) -> Bool {
         switch (value1, value2) {
         case (.null, .null):
             return true
         case let (.string(s1), .string(s2)):
             return s1 == s2
         case let (.number(n1), .number(n2)):
-            return n1 == n2
+            let match = n1.isApproximatelyEqual(to: n2, epsilon: approximately)
+            return match
         case let (.boolean(b1), .boolean(b2)):
             return b1 == b2
         case let (.array(a1), .array(a2)):
-            return a1.count == a2.count && zip(a1, a2).allSatisfy(valuesAreEqual)
+            return a1.count == a2.count && zip(a1, a2).allSatisfy { v1, v2 in
+                valuesAreEqual(v1, v2, approximately: approximately, approximateKeys: approximateKeys)
+            }
         case let (.object(o1), .object(o2)):
             return o1.keys == o2.keys && o1.keys.allSatisfy { key in
                 guard let v1 = o1[key], let v2 = o2[key] else { return false }
-                return valuesAreEqual(v1, v2)
+                return valuesAreEqual(v1, v2, approximately: approximateKeys[key], approximateKeys: approximateKeys)
             }
         default:
             return false

+ 24 - 5
Trio/Sources/APS/OpenAPSSwift/Logging/JsSwiftOrefComparisonLogger.swift

@@ -51,11 +51,12 @@ actor JsSwiftOrefComparisonLogger {
 
     // MARK: - Logger implementation
 
-    private let minBatchSize = 8
-    private let maxStoredEntries = 256
+    private let minBatchSize = 16
+    private let maxStoredEntries = 4096
     private let fileManager = FileManager.default
     private let encoder = JSONEncoder()
     private let decoder = JSONDecoder()
+    private var isUploading = false
 
     // server settings for getting a signed Google Cloud Storage URL
     // that we can PUT to
@@ -81,7 +82,17 @@ actor JsSwiftOrefComparisonLogger {
 
     private func readComparisons() throws -> [AlgorithmComparison] {
         let data = try Data(contentsOf: storageUrl)
-        return try decoder.decode([AlgorithmComparison].self, from: data)
+        do {
+            return try decoder.decode([AlgorithmComparison].self, from: data)
+        } catch DecodingError.keyNotFound {
+            // this can happen when we change the AlgorithmComparison
+            // struct, we can just drop the values that are cached and try again
+            try "[]".write(to: storageUrl, atomically: true, encoding: .utf8)
+            let data = try Data(contentsOf: storageUrl)
+            return try decoder.decode([AlgorithmComparison].self, from: data)
+        } catch {
+            throw error
+        }
     }
 
     private func writeComparisons(_ comparisons: [AlgorithmComparison]) throws {
@@ -98,8 +109,16 @@ actor JsSwiftOrefComparisonLogger {
 
         try writeComparisons(comparisons)
 
-        if comparisons.count >= minBatchSize {
-            try await uploadCurrentBatch()
+        // upload when we have enough entries and avoid uploading duplicates
+        if comparisons.count >= minBatchSize, !isUploading {
+            isUploading = true
+            do {
+                try await uploadCurrentBatch()
+                isUploading = false
+            } catch {
+                isUploading = false
+                throw error
+            }
         }
     }
 

+ 41 - 0
Trio/Sources/APS/OpenAPSSwift/Logging/OrefFunction.swift

@@ -1,3 +1,5 @@
+import Foundation
+
 /// After the port from Javascript to Swift is complete, we should remove the logging module:
 /// https://github.com/nightscout/Trio-dev/issues/293
 
@@ -14,7 +16,13 @@ enum OrefFunctionResult {
 }
 
 enum OrefFunction: String, Codable {
+    enum ReturnType {
+        case array
+        case dictionary
+    }
+
     case makeProfile
+    case iob
 
     // since we're removing some keys from our Profile that exist in Javascript
     // we need to let the difference function know which keys to ignore when
@@ -23,6 +31,39 @@ enum OrefFunction: String, Codable {
         switch self {
         case .makeProfile:
             return Set(["calc_glucose_noise", "enableEnliteBgproxy", "exercise_mode", "offline_hotspot"])
+        case .iob:
+            // we're only checking the first result for now
+            return Set(stride(from: 1, to: 48, by: 1).map { String("[\($0)]") })
+        }
+    }
+
+    // Some values might be slightly different due to Double vs Decimal
+    // and minor algorithmic differences
+    func approximateMatchingNumbers() -> [String: Double] {
+        switch self {
+        case .makeProfile:
+            return [:]
+        case .iob:
+            // for iob we can get rounding errors because of Double vs Decimal
+            // so we leave a little extra room for our comparisons
+            return [
+                "iob": 0.1,
+                "activity": 0.01,
+                "basaliob": 0.25,
+                "bolusiob": 0.25,
+                "netbasalinsulin": 0.25,
+                "bolusinsulin": 0.25,
+                "duration": 0.1
+            ]
+        }
+    }
+
+    func returnType() -> ReturnType {
+        switch self {
+        case .makeProfile:
+            return .dictionary
+        case .iob:
+            return .array
         }
     }
 }

+ 4 - 0
Trio/Sources/APS/OpenAPSSwift/Models/ComputedPumpHistoryEvent.swift

@@ -22,6 +22,10 @@ struct ComputedPumpHistoryEvent: Codable, Equatable, Identifiable {
     let started_at: Date
     let date: UInt64
 
+    var end: Date {
+        timestamp + (duration ?? durationMin.map { Decimal($0) } ?? 0).minutesToSeconds
+    }
+
     init(
         id: String,
         type: EventType,

+ 19 - 0
Trio/Sources/APS/OpenAPSSwift/OpenAPSSwift.swift

@@ -40,4 +40,23 @@ struct OpenAPSSwift {
             return .failure(error)
         }
     }
+
+    static func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) -> (OrefFunctionResult, IobInputs?) {
+        var iobInputs: IobInputs?
+
+        do {
+            let pumpHistory = try JSONBridge.pumpHistory(from: pumphistory)
+            let profile = try JSONBridge.profile(from: profile)
+            let clock = try JSONBridge.clock(from: clock)
+            let autosens = try JSONBridge.autosens(from: autosens)
+
+            iobInputs = IobInputs(history: pumpHistory, profile: profile, clock: clock, autosens: autosens)
+
+            let iobResult = try IobGenerator.generate(history: pumpHistory, profile: profile, clock: clock, autosens: autosens)
+
+            return try (.success(JSONBridge.to(iobResult)), iobInputs)
+        } catch {
+            return (.failure(error), iobInputs)
+        }
+    }
 }

+ 0 - 108
TrioTests/OpenAPSSwiftTests/IobJsonTests.swift

@@ -1,108 +0,0 @@
-import Foundation
-import Testing
-@testable import Trio
-
-class BundleReference {}
-
-@Suite("IoB using real pump history JSON", .disabled()) struct IobJsonTests {
-    @Test("should produce the same JSON IobResult as Javascript") func createIobResultFromJson() async throws {
-        let testBundle = Bundle(for: BundleReference.self)
-        guard let path = testBundle.path(forResource: "pump_history", ofType: "json"),
-              let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
-              let pumpHistory: [PumpHistoryEvent] = try! JSONBridge.from(string: String(data: data, encoding: .utf8)!),
-              let path2 = testBundle.path(forResource: "iob_result", ofType: "json"),
-              let data2 = try? Data(contentsOf: URL(fileURLWithPath: path2)),
-              let iobResultsJson: [IobResult] = try! JSONBridge.from(string: String(data: data2, encoding: .utf8)!)
-        else {
-            #expect(Bool(false))
-            return
-        }
-
-        let basalProfile = [
-            BasalProfileEntry(start: "00:00", minutes: 0, rate: 0.5)
-        ]
-
-        var profile = Profile()
-        profile.dia = 10
-        profile.basalprofile = basalProfile
-        profile.currentBasal = 1
-        profile.maxDailyBasal = 1
-        profile.curve = .ultraRapid
-
-        let clock = Date("2025-02-18T23:23:31.036Z")!
-
-        let iobResult = try IobGenerator.generate(history: pumpHistory, profile: profile, clock: clock, autosens: nil)
-
-        #expect(iobResult.count == iobResultsJson.count)
-        for (swift, javascript) in zip(iobResult, iobResultsJson) {
-            #expect(swift.approximatelyEquals(javascript))
-        }
-    }
-
-    @Test("should produce the same JSON history as Javascript") func createIobHistoryFromJson() async throws {
-        let testBundle = Bundle(for: BundleReference.self)
-        guard let path = testBundle.path(forResource: "pump_history", ofType: "json"),
-              let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
-              let path2 = testBundle.path(forResource: "iob_history", ofType: "json"),
-              let data2 = try? Data(contentsOf: URL(fileURLWithPath: path2)),
-              let pumpHistory: [PumpHistoryEvent] = try! JSONBridge.from(string: String(data: data, encoding: .utf8)!),
-              let iobHistoryJson: [HistoryRecord] = try! JSONBridge.from(string: String(data: data2, encoding: .utf8)!)
-        else {
-            #expect(Bool(false))
-            return
-        }
-
-        let basalProfile = [
-            BasalProfileEntry(start: "00:00", minutes: 0, rate: 0.5)
-        ]
-
-        var profile = Profile()
-        profile.dia = 10
-        profile.basalprofile = basalProfile
-        profile.currentBasal = 1
-        profile.maxDailyBasal = 1
-        profile.curve = .ultraRapid
-
-        let clock = Date("2025-02-18T23:23:31.036Z")!
-
-        let computedHistory = pumpHistory.map { $0.computedEvent() }
-
-        let history = try IobHistory.calcTempTreatments(
-            history: computedHistory,
-            profile: profile,
-            clock: clock,
-            autosens: nil,
-            zeroTempDuration: nil
-        )
-
-        #expect(history.count == iobHistoryJson.count)
-        let historyBolusCount = history.filter({ $0.insulin != nil }).count
-        let jsonBolusCount = iobHistoryJson.filter({ record in
-            switch record {
-            case .insulin: return true
-            case .basal: return false
-            }
-        }).count
-        #expect(historyBolusCount == jsonBolusCount)
-
-        let historyBasalCount = history.filter({ $0.rate != nil }).count
-        let jsonBasalCount = iobHistoryJson.filter({ record in
-            switch record {
-            case .insulin: return false
-            case .basal: return true
-            }
-        }).count
-        #expect(historyBasalCount == jsonBasalCount)
-
-        let historyInsulin = history.compactMap(\.insulin).reduce(0, +)
-        let jsonInsulin = iobHistoryJson.compactMap({ record in
-            switch record {
-            case let .insulin(r):
-                return r.insulin
-            case .basal:
-                return nil
-            }
-        }).reduce(0, +)
-        #expect(historyInsulin == jsonInsulin)
-    }
-}

+ 379 - 0
TrioTests/OpenAPSSwiftTests/IobSuspendTests.swift

@@ -0,0 +1,379 @@
+import Foundation
+import Testing
+@testable import Trio
+
+@Suite("IOB Suspend Logic Tests") struct IobSuspendTests {
+    // Helper function to create a basic basal profile
+    func createBasicBasalProfile() -> [BasalProfileEntry] {
+        [
+            BasalProfileEntry(
+                start: "00:00:00",
+                minutes: 0,
+                rate: 1
+            )
+        ]
+    }
+
+    // Helper function to create a multi-rate basal profile
+    func createMultiRateBasalProfile() -> [BasalProfileEntry] {
+        [
+            BasalProfileEntry(
+                start: "00:00:00",
+                minutes: 0,
+                rate: 1
+            ),
+            BasalProfileEntry(
+                start: "00:30:00",
+                minutes: 30,
+                rate: 2
+            )
+        ]
+    }
+
+    @Test("should handle basic suspend and resume") func handleBasicSuspendAndResume() async throws {
+        let basalprofile = createBasicBasalProfile()
+
+        // Create fixed test dates (matching JavaScript test)
+        let formatter = ISO8601DateFormatter()
+        formatter.formatOptions = [.withInternetDateTime]
+
+        let now = formatter.date(from: "2016-06-13T01:00:00Z")!
+        let timestamp30mAgo = formatter.date(from: "2016-06-13T00:30:00Z")!
+        let timestamp15mAgo = formatter.date(from: "2016-06-13T00:45:00Z")!
+
+        let pumpHistory = [
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasal,
+                timestamp: timestamp30mAgo,
+                duration: nil,
+                rate: 2,
+                temp: .absolute
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasalDuration,
+                timestamp: timestamp30mAgo,
+                durationMin: 30
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpSuspend,
+                timestamp: timestamp15mAgo
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpResume,
+                timestamp: now
+            )
+        ]
+
+        var profile = Profile()
+        profile.currentBasal = 1
+        profile.maxDailyBasal = 1
+        profile.dia = 3
+        profile.basalprofile = basalprofile
+        profile.suspendZerosIob = true
+
+        let treatments = try IobHistory.calcTempTreatments(
+            history: pumpHistory,
+            profile: profile,
+            clock: now,
+            autosens: nil,
+            zeroTempDuration: nil
+        )
+
+        // Calculate expected insulin impact:
+        // 15m at 2 U/h - 1 U/h = 0.25U
+        // 15m at 0 U/h - 1 U/h = -0.25U
+        // Total: 0U
+        #expect(treatments.netInsulin().isWithin(0.01, of: 0.0))
+    }
+
+    @Test("should handle suspend prior to history window") func handleSuspendPriorToHistoryWindow() async throws {
+        let basalprofile = createBasicBasalProfile()
+
+        // Create fixed test dates (matching JavaScript test)
+        let formatter = ISO8601DateFormatter()
+        formatter.formatOptions = [.withInternetDateTime]
+
+        let now = formatter.date(from: "2016-06-13T08:00:00Z")!
+        let resumeTime = formatter.date(from: "2016-06-13T07:00:00Z")!
+        let tempStartTime = formatter.date(from: "2016-06-13T07:30:00Z")!
+
+        let pumpHistory = [
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpResume,
+                timestamp: resumeTime
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasal,
+                timestamp: tempStartTime,
+                duration: nil,
+                rate: 2,
+                temp: .absolute
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasalDuration,
+                timestamp: tempStartTime,
+                durationMin: 30
+            )
+        ]
+
+        var profile = Profile()
+        profile.currentBasal = 1
+        profile.maxDailyBasal = 1
+        profile.dia = 10 // Longer DIA to match JS test
+        profile.basalprofile = basalprofile
+        profile.suspendZerosIob = true
+
+        let treatments = try IobHistory.calcTempTreatments(
+            history: pumpHistory,
+            profile: profile,
+            clock: now,
+            autosens: nil,
+            zeroTempDuration: nil
+        )
+
+        // Calculate expected insulin impact:
+        // 7h at 0 U/h - 1U/h = -7 (this may need adjustment based on implementation)
+        // 30m at 2 U/h - 1 U/h = 0.5U
+        // Total: approximately -6.5U
+
+        // Note: This test case may need adjustments based on how you implement the suspend
+        // prior to history window logic in your Swift port
+        let netInsulin = treatments.netInsulin()
+
+        // The exact value might vary due to implementation details, but the general direction should be consistent
+        #expect(netInsulin < -6.0)
+    }
+
+    @Test("should handle current suspension") func handleCurrentSuspension() async throws {
+        let basalprofile = createBasicBasalProfile()
+
+        // Setting up the dates for the test
+        let now = Calendar.current.startOfDay(for: Date()) + 60.minutesToSeconds
+        let suspendTime = now - 30.minutesToSeconds
+        let tempStartTime = now - 45.minutesToSeconds
+
+        let pumpHistory = [
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasal,
+                timestamp: tempStartTime,
+                duration: nil,
+                rate: 2,
+                temp: .absolute
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasalDuration,
+                timestamp: tempStartTime,
+                durationMin: 30
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpSuspend,
+                timestamp: suspendTime
+            )
+        ]
+
+        var profile = Profile()
+        profile.currentBasal = 1
+        profile.maxDailyBasal = 1
+        profile.dia = 3
+        profile.basalprofile = basalprofile
+        profile.suspendZerosIob = true
+
+        let treatments = try IobHistory.calcTempTreatments(
+            history: pumpHistory,
+            profile: profile,
+            clock: now,
+            autosens: nil,
+            zeroTempDuration: nil
+        )
+
+        // Calculate expected insulin impact:
+        // 15m at 2 U/h - 1U/h = 0.25
+        // 30m at 0 U/h - 1U/h = -0.5
+        // Total: -0.25U
+        #expect(treatments.netInsulin().isWithin(0.01, of: -0.25))
+    }
+
+    @Test("should handle multiple suspend-resume cycles") func handleMultipleSuspendResumeCycles() async throws {
+        let basalprofile = createBasicBasalProfile()
+
+        // Setting up the dates for the test
+        let now = Calendar.current.startOfDay(for: Date()) + 90.minutesToSeconds
+
+        // Create history with 2 suspend-resume cycles
+        let suspend1 = now - 90.minutesToSeconds
+        let resume1 = now - 75.minutesToSeconds
+        let tempStart = now - 60.minutesToSeconds
+        let suspend2 = now - 45.minutesToSeconds
+        let resume2 = now - 30.minutesToSeconds
+
+        let pumpHistory = [
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpSuspend,
+                timestamp: suspend1
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpResume,
+                timestamp: resume1
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasal,
+                timestamp: tempStart,
+                duration: nil,
+                rate: 2,
+                temp: .absolute
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasalDuration,
+                timestamp: tempStart,
+                durationMin: 60
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpSuspend,
+                timestamp: suspend2
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpResume,
+                timestamp: resume2
+            )
+        ]
+
+        var profile = Profile()
+        profile.currentBasal = 1
+        profile.maxDailyBasal = 1
+        profile.dia = 3
+        profile.basalprofile = basalprofile
+        profile.suspendZerosIob = true
+
+        let treatments = try IobHistory.calcTempTreatments(
+            history: pumpHistory,
+            profile: profile,
+            clock: now,
+            autosens: nil,
+            zeroTempDuration: nil
+        )
+
+        // Calculate expected insulin impact:
+        // 15m at 0 U/h - 1 U/h = -0.25
+        // 15m at 2 U/h - 1 U/h = 0.25
+        // 15m at 0 U/h - 1 U/h = -0.25
+        // 30m at 2 U/h - 1 U/h = 0.5
+        // Total: 0.25U
+        #expect(treatments.netInsulin().isWithin(0.01, of: 0.25))
+    }
+
+    @Test("should handle suspend with basal profile changes") func handleSuspendWithBasalProfileChanges() async throws {
+        let basalprofile = createMultiRateBasalProfile()
+
+        // Create fixed test dates (matching JavaScript test)
+        let formatter = ISO8601DateFormatter()
+        formatter.formatOptions = [.withInternetDateTime]
+
+        let calendar = Calendar.current
+
+        let currentTime = Date()
+        let startTime = Calendar.current.startOfDay(for: currentTime) + 15.minutesToSeconds
+        let suspendTime = Calendar.current.startOfDay(for: currentTime) + 30.minutesToSeconds
+        let resumeTime = Calendar.current.startOfDay(for: currentTime) + 45.minutesToSeconds
+        let endTime = Calendar.current.startOfDay(for: currentTime) + 60.minutesToSeconds
+
+        let pumpHistory = [
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasal,
+                timestamp: startTime,
+                duration: nil,
+                rate: 3,
+                temp: .absolute
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasalDuration,
+                timestamp: startTime,
+                durationMin: 45
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpSuspend,
+                timestamp: suspendTime
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpResume,
+                timestamp: resumeTime
+            )
+        ]
+
+        var profile = Profile()
+        profile.currentBasal = 1
+        profile.maxDailyBasal = 2
+        profile.dia = 3
+        profile.basalprofile = basalprofile
+        profile.suspendZerosIob = true
+
+        let treatments = try IobHistory.calcTempTreatments(
+            history: pumpHistory,
+            profile: profile,
+            clock: endTime,
+            autosens: nil,
+            zeroTempDuration: nil
+        )
+
+        // Calculate expected insulin impact:
+        // 15m at 3 U/h - 1 U/h = 0.5U (from start to basal change)
+        // 15m at 0 U/h - 2 U/h = -0.5U (from basal change and suspend)
+        // 15m at 3 U/h - 2 U/h = 0.25U (resume to finish)
+        // Total: 0.25U
+        #expect(treatments.netInsulin().isWithin(0.01, of: 0.25))
+    }
+
+    @Test("should properly handle IOB impact with suspends") func handleIobImpactWithSuspends() async throws {
+        let basalprofile = createBasicBasalProfile()
+
+        // Setting up the dates for the test
+        let now = Calendar.current.startOfDay(for: Date()) + 90.minutesToSeconds
+
+        let tempStart = now - 60.minutesToSeconds
+        let suspendTime = now - 30.minutesToSeconds
+        let resumeTime = now
+
+        let pumpHistory = [
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasal,
+                timestamp: tempStart,
+                duration: nil,
+                rate: 2,
+                temp: .absolute
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .tempBasalDuration,
+                timestamp: tempStart,
+                durationMin: 30
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpSuspend,
+                timestamp: suspendTime
+            ),
+            ComputedPumpHistoryEvent.forTest(
+                type: .pumpResume,
+                timestamp: resumeTime
+            )
+        ]
+
+        var profile = Profile()
+        profile.currentBasal = 1
+        profile.maxDailyBasal = 1
+        profile.dia = 3
+        profile.basalprofile = basalprofile
+        profile.suspendZerosIob = true
+
+        let treatments = try IobHistory.calcTempTreatments(
+            history: pumpHistory,
+            profile: profile,
+            clock: now,
+            autosens: nil,
+            zeroTempDuration: nil
+        )
+
+        // Calculate expected insulin impact:
+        // 30m at 2 U/h - 1 U/h = 0.5U (from temp start to temp end)
+        // 30m at 0 U/h - 1 U/h = -0.5U (from suspend to resume)
+        // Total: 0U
+        #expect(treatments.netInsulin().isWithin(0.01, of: 0.0))
+    }
+}

+ 14 - 7
TrioTests/OpenAPSSwiftTests/ProfileJsNativeCompareTests.swift

@@ -90,7 +90,8 @@ import Testing
             swift: profileSwift,
             swiftDuration: 0.1,
             javascript: profileJs,
-            javascriptDuration: 0.1
+            javascriptDuration: 0.1,
+            iobInputs: nil
         )
 
         #expect(comparison.resultType == .matching)
@@ -119,7 +120,8 @@ import Testing
             swift: .success(matchingJSON),
             swiftDuration: 0.1,
             javascript: .success(matchingJSON),
-            javascriptDuration: 0.2
+            javascriptDuration: 0.2,
+            iobInputs: nil
         )
 
         #expect(comparison.resultType == .matching)
@@ -137,7 +139,8 @@ import Testing
             swift: .success(differentJSON),
             swiftDuration: 0.1,
             javascript: .success(matchingJSON),
-            javascriptDuration: 0.2
+            javascriptDuration: 0.2,
+            iobInputs: nil
         )
 
         #expect(comparison.resultType == .valueDifference)
@@ -157,7 +160,8 @@ import Testing
             swift: .failure(error),
             swiftDuration: 0.1,
             javascript: .failure(error),
-            javascriptDuration: 0.2
+            javascriptDuration: 0.2,
+            iobInputs: nil
         )
 
         #expect(comparison.resultType == .matchingExceptions)
@@ -174,7 +178,8 @@ import Testing
             swift: .failure(error),
             swiftDuration: 0.1,
             javascript: .success(matchingJSON),
-            javascriptDuration: 0.2
+            javascriptDuration: 0.2,
+            iobInputs: nil
         )
 
         #expect(comparison.resultType == .swiftOnlyException)
@@ -193,7 +198,8 @@ import Testing
             swift: .success(matchingJSON),
             swiftDuration: 0.1,
             javascript: .failure(error),
-            javascriptDuration: 0.2
+            javascriptDuration: 0.2,
+            iobInputs: nil
         )
 
         #expect(comparison.resultType == .jsOnlyException)
@@ -211,7 +217,8 @@ import Testing
             swift: .success(invalidJSON),
             swiftDuration: 0.1,
             javascript: .success(matchingJSON),
-            javascriptDuration: 0.2
+            javascriptDuration: 0.2,
+            iobInputs: nil
         )
 
         #expect(comparison.resultType == .comparisonError)

Разлика између датотеке није приказан због своје велике величине
+ 0 - 3196
TrioTests/OpenAPSSwiftTests/json/iob_history.json


+ 0 - 874
TrioTests/OpenAPSSwiftTests/json/iob_result.json

@@ -1,874 +0,0 @@
-[
-  {
-    "iob": 0.539,
-    "activity": 0.0046,
-    "basaliob": 0.149,
-    "bolusiob": 0.39,
-    "netbasalinsulin": -1.75,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-18T23:23:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": 0.539,
-      "activity": 0.0046,
-      "basaliob": 0.149,
-      "bolusiob": 0.39,
-      "netbasalinsulin": -1.75,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-18T23:23:31.036Z"
-    },
-    "lastBolusTime": 1739920771036,
-    "lastTemp": {
-      "rate": 0,
-      "timestamp": "2025-02-18T23:12:39.306Z",
-      "started_at": "2025-02-18T23:12:39.306Z",
-      "date": 1739920359306,
-      "duration": 11.86
-    }
-  },
-  {
-    "iob": 0.516,
-    "activity": 0.0047,
-    "basaliob": 0.142,
-    "bolusiob": 0.374,
-    "netbasalinsulin": -1.7,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-18T23:28:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": 0.466,
-      "activity": 0.0046,
-      "basaliob": 0.092,
-      "bolusiob": 0.374,
-      "netbasalinsulin": -1.75,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-18T23:28:31.036Z"
-    }
-  },
-  {
-    "iob": 0.492,
-    "activity": 0.0047,
-    "basaliob": 0.135,
-    "bolusiob": 0.358,
-    "netbasalinsulin": -1.65,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-18T23:33:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": 0.393,
-      "activity": 0.0045,
-      "basaliob": 0.036,
-      "bolusiob": 0.358,
-      "netbasalinsulin": -1.75,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-18T23:33:31.036Z"
-    }
-  },
-  {
-    "iob": 0.469,
-    "activity": 0.0047,
-    "basaliob": 0.128,
-    "bolusiob": 0.341,
-    "netbasalinsulin": -1.65,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-18T23:38:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": 0.321,
-      "activity": 0.0043,
-      "basaliob": -0.02,
-      "bolusiob": 0.341,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-18T23:38:31.036Z"
-    }
-  },
-  {
-    "iob": 0.446,
-    "activity": 0.0046,
-    "basaliob": 0.121,
-    "bolusiob": 0.325,
-    "netbasalinsulin": -1.6,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-18T23:43:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": 0.25,
-      "activity": 0.0041,
-      "basaliob": -0.075,
-      "bolusiob": 0.325,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-18T23:43:31.036Z"
-    }
-  },
-  {
-    "iob": 0.423,
-    "activity": 0.0045,
-    "basaliob": 0.115,
-    "bolusiob": 0.308,
-    "netbasalinsulin": -1.55,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-18T23:48:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": 0.18,
-      "activity": 0.0038,
-      "basaliob": -0.128,
-      "bolusiob": 0.308,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-18T23:48:31.036Z"
-    }
-  },
-  {
-    "iob": 0.401,
-    "activity": 0.0044,
-    "basaliob": 0.108,
-    "bolusiob": 0.292,
-    "netbasalinsulin": -1.55,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-18T23:53:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": 0.162,
-      "activity": 0.0034,
-      "basaliob": -0.13,
-      "bolusiob": 0.292,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-18T23:53:31.036Z"
-    }
-  },
-  {
-    "iob": 0.379,
-    "activity": 0.0043,
-    "basaliob": 0.102,
-    "bolusiob": 0.277,
-    "netbasalinsulin": -1.5,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-18T23:58:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": 0.096,
-      "activity": 0.003,
-      "basaliob": -0.181,
-      "bolusiob": 0.277,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-18T23:58:31.036Z"
-    }
-  },
-  {
-    "iob": 0.358,
-    "activity": 0.0041,
-    "basaliob": 0.096,
-    "bolusiob": 0.262,
-    "netbasalinsulin": -1.45,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:03:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": 0.032,
-      "activity": 0.0026,
-      "basaliob": -0.23,
-      "bolusiob": 0.262,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:03:31.036Z"
-    }
-  },
-  {
-    "iob": 0.338,
-    "activity": 0.004,
-    "basaliob": 0.091,
-    "bolusiob": 0.247,
-    "netbasalinsulin": -1.4,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:08:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.03,
-      "activity": 0.0022,
-      "basaliob": -0.277,
-      "bolusiob": 0.247,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:08:31.036Z"
-    }
-  },
-  {
-    "iob": 0.318,
-    "activity": 0.0038,
-    "basaliob": 0.085,
-    "bolusiob": 0.233,
-    "netbasalinsulin": -1.35,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:13:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.09,
-      "activity": 0.0018,
-      "basaliob": -0.323,
-      "bolusiob": 0.233,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:13:31.036Z"
-    }
-  },
-  {
-    "iob": 0.299,
-    "activity": 0.0037,
-    "basaliob": 0.08,
-    "bolusiob": 0.219,
-    "netbasalinsulin": -1.3,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:18:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.148,
-      "activity": 0.0013,
-      "basaliob": -0.367,
-      "bolusiob": 0.219,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:18:31.036Z"
-    }
-  },
-  {
-    "iob": 0.281,
-    "activity": 0.0035,
-    "basaliob": 0.075,
-    "bolusiob": 0.206,
-    "netbasalinsulin": -1.3,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:23:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.153,
-      "activity": 0.0009,
-      "basaliob": -0.359,
-      "bolusiob": 0.206,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:23:31.036Z"
-    }
-  },
-  {
-    "iob": 0.264,
-    "activity": 0.0034,
-    "basaliob": 0.071,
-    "bolusiob": 0.194,
-    "netbasalinsulin": -1.25,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:28:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.206,
-      "activity": 0.0004,
-      "basaliob": -0.4,
-      "bolusiob": 0.194,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:28:31.036Z"
-    }
-  },
-  {
-    "iob": 0.248,
-    "activity": 0.0032,
-    "basaliob": 0.066,
-    "bolusiob": 0.182,
-    "netbasalinsulin": -1.2,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:33:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.258,
-      "activity": 0,
-      "basaliob": -0.439,
-      "bolusiob": 0.182,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:33:31.036Z"
-    }
-  },
-  {
-    "iob": 0.232,
-    "activity": 0.0031,
-    "basaliob": 0.062,
-    "bolusiob": 0.17,
-    "netbasalinsulin": -1.15,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:38:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.307,
-      "activity": -0.0004,
-      "basaliob": -0.477,
-      "bolusiob": 0.17,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:38:31.036Z"
-    }
-  },
-  {
-    "iob": 0.217,
-    "activity": 0.0029,
-    "basaliob": 0.058,
-    "bolusiob": 0.159,
-    "netbasalinsulin": -1.1,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:43:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.353,
-      "activity": -0.0008,
-      "basaliob": -0.513,
-      "bolusiob": 0.159,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:43:31.036Z"
-    }
-  },
-  {
-    "iob": 0.203,
-    "activity": 0.0027,
-    "basaliob": 0.054,
-    "bolusiob": 0.149,
-    "netbasalinsulin": -1.1,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:48:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.398,
-      "activity": -0.0012,
-      "basaliob": -0.547,
-      "bolusiob": 0.149,
-      "netbasalinsulin": -1.85,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:48:31.036Z"
-    }
-  },
-  {
-    "iob": 0.19,
-    "activity": 0.0026,
-    "basaliob": 0.05,
-    "bolusiob": 0.139,
-    "netbasalinsulin": -1.05,
-    "bolusinsulin": 7.25,
-    "time": "2025-02-19T00:53:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.391,
-      "activity": -0.0016,
-      "basaliob": -0.53,
-      "bolusiob": 0.139,
-      "netbasalinsulin": -1.8,
-      "bolusinsulin": 7.25,
-      "time": "2025-02-19T00:53:31.036Z"
-    }
-  },
-  {
-    "iob": 0.177,
-    "activity": 0.0025,
-    "basaliob": 0.047,
-    "bolusiob": 0.13,
-    "netbasalinsulin": -1.1,
-    "bolusinsulin": 6.65,
-    "time": "2025-02-19T00:58:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.432,
-      "activity": -0.002,
-      "basaliob": -0.562,
-      "bolusiob": 0.13,
-      "netbasalinsulin": -1.9,
-      "bolusinsulin": 6.65,
-      "time": "2025-02-19T00:58:31.036Z"
-    }
-  },
-  {
-    "iob": 0.165,
-    "activity": 0.0023,
-    "basaliob": 0.044,
-    "bolusiob": 0.122,
-    "netbasalinsulin": -1.25,
-    "bolusinsulin": 6.25,
-    "time": "2025-02-19T01:03:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.471,
-      "activity": -0.0024,
-      "basaliob": -0.592,
-      "bolusiob": 0.122,
-      "netbasalinsulin": -2.1,
-      "bolusinsulin": 6.25,
-      "time": "2025-02-19T01:03:31.036Z"
-    }
-  },
-  {
-    "iob": 0.154,
-    "activity": 0.0022,
-    "basaliob": 0.041,
-    "bolusiob": 0.113,
-    "netbasalinsulin": -1.25,
-    "bolusinsulin": 6.25,
-    "time": "2025-02-19T01:08:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.508,
-      "activity": -0.0027,
-      "basaliob": -0.621,
-      "bolusiob": 0.113,
-      "netbasalinsulin": -2.15,
-      "bolusinsulin": 6.25,
-      "time": "2025-02-19T01:08:31.036Z"
-    }
-  },
-  {
-    "iob": 0.144,
-    "activity": 0.0021,
-    "basaliob": 0.038,
-    "bolusiob": 0.106,
-    "netbasalinsulin": -1.3,
-    "bolusinsulin": 6.25,
-    "time": "2025-02-19T01:13:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.544,
-      "activity": -0.0031,
-      "basaliob": -0.649,
-      "bolusiob": 0.106,
-      "netbasalinsulin": -2.25,
-      "bolusinsulin": 6.25,
-      "time": "2025-02-19T01:13:31.036Z"
-    }
-  },
-  {
-    "iob": 0.134,
-    "activity": 0.0019,
-    "basaliob": 0.035,
-    "bolusiob": 0.098,
-    "netbasalinsulin": -1.35,
-    "bolusinsulin": 6.25,
-    "time": "2025-02-19T01:18:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.577,
-      "activity": -0.0034,
-      "basaliob": -0.676,
-      "bolusiob": 0.098,
-      "netbasalinsulin": -2.35,
-      "bolusinsulin": 6.25,
-      "time": "2025-02-19T01:18:31.036Z"
-    }
-  },
-  {
-    "iob": 0.124,
-    "activity": 0.0018,
-    "basaliob": 0.033,
-    "bolusiob": 0.091,
-    "netbasalinsulin": -1.35,
-    "bolusinsulin": 5.65,
-    "time": "2025-02-19T01:23:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.56,
-      "activity": -0.0037,
-      "basaliob": -0.651,
-      "bolusiob": 0.091,
-      "netbasalinsulin": -2.35,
-      "bolusinsulin": 5.65,
-      "time": "2025-02-19T01:23:31.036Z"
-    }
-  },
-  {
-    "iob": 0.115,
-    "activity": 0.0017,
-    "basaliob": 0.03,
-    "bolusiob": 0.085,
-    "netbasalinsulin": -1.3,
-    "bolusinsulin": 5.25,
-    "time": "2025-02-19T01:28:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.59,
-      "activity": -0.004,
-      "basaliob": -0.675,
-      "bolusiob": 0.085,
-      "netbasalinsulin": -2.35,
-      "bolusinsulin": 5.25,
-      "time": "2025-02-19T01:28:31.036Z"
-    }
-  },
-  {
-    "iob": 0.107,
-    "activity": 0.0016,
-    "basaliob": 0.028,
-    "bolusiob": 0.079,
-    "netbasalinsulin": -1.25,
-    "bolusinsulin": 4.75,
-    "time": "2025-02-19T01:33:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.62,
-      "activity": -0.0043,
-      "basaliob": -0.699,
-      "bolusiob": 0.079,
-      "netbasalinsulin": -2.35,
-      "bolusinsulin": 4.75,
-      "time": "2025-02-19T01:33:31.036Z"
-    }
-  },
-  {
-    "iob": 0.1,
-    "activity": 0.0015,
-    "basaliob": 0.026,
-    "bolusiob": 0.073,
-    "netbasalinsulin": -1.2,
-    "bolusinsulin": 4.3,
-    "time": "2025-02-19T01:38:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.648,
-      "activity": -0.0045,
-      "basaliob": -0.721,
-      "bolusiob": 0.073,
-      "netbasalinsulin": -2.35,
-      "bolusinsulin": 4.3,
-      "time": "2025-02-19T01:38:31.036Z"
-    }
-  },
-  {
-    "iob": 0.092,
-    "activity": 0.0014,
-    "basaliob": 0.024,
-    "bolusiob": 0.068,
-    "netbasalinsulin": -1.15,
-    "bolusinsulin": 3.85,
-    "time": "2025-02-19T01:43:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.674,
-      "activity": -0.0048,
-      "basaliob": -0.742,
-      "bolusiob": 0.068,
-      "netbasalinsulin": -2.35,
-      "bolusinsulin": 3.85,
-      "time": "2025-02-19T01:43:31.036Z"
-    }
-  },
-  {
-    "iob": 0.086,
-    "activity": 0.0013,
-    "basaliob": 0.023,
-    "bolusiob": 0.063,
-    "netbasalinsulin": -1.1,
-    "bolusinsulin": 3.25,
-    "time": "2025-02-19T01:48:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.7,
-      "activity": -0.005,
-      "basaliob": -0.763,
-      "bolusiob": 0.063,
-      "netbasalinsulin": -2.35,
-      "bolusinsulin": 3.25,
-      "time": "2025-02-19T01:48:31.036Z"
-    }
-  },
-  {
-    "iob": 0.079,
-    "activity": 0.0012,
-    "basaliob": 0.021,
-    "bolusiob": 0.058,
-    "netbasalinsulin": -1.05,
-    "bolusinsulin": 2.75,
-    "time": "2025-02-19T01:53:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.674,
-      "activity": -0.0052,
-      "basaliob": -0.732,
-      "bolusiob": 0.058,
-      "netbasalinsulin": -2.3,
-      "bolusinsulin": 2.75,
-      "time": "2025-02-19T01:53:31.036Z"
-    }
-  },
-  {
-    "iob": 0.073,
-    "activity": 0.0011,
-    "basaliob": 0.019,
-    "bolusiob": 0.054,
-    "netbasalinsulin": -1,
-    "bolusinsulin": 2.5,
-    "time": "2025-02-19T01:58:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.697,
-      "activity": -0.0055,
-      "basaliob": -0.751,
-      "bolusiob": 0.054,
-      "netbasalinsulin": -2.3,
-      "bolusinsulin": 2.5,
-      "time": "2025-02-19T01:58:31.036Z"
-    }
-  },
-  {
-    "iob": 0.068,
-    "activity": 0.0011,
-    "basaliob": 0.018,
-    "bolusiob": 0.05,
-    "netbasalinsulin": -0.95,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:03:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.72,
-      "activity": -0.0057,
-      "basaliob": -0.77,
-      "bolusiob": 0.05,
-      "netbasalinsulin": -2.3,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:03:31.036Z"
-    }
-  },
-  {
-    "iob": 0.063,
-    "activity": 0.001,
-    "basaliob": 0.017,
-    "bolusiob": 0.046,
-    "netbasalinsulin": -0.9,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:08:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.741,
-      "activity": -0.0058,
-      "basaliob": -0.787,
-      "bolusiob": 0.046,
-      "netbasalinsulin": -2.3,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:08:31.036Z"
-    }
-  },
-  {
-    "iob": 0.058,
-    "activity": 0.0009,
-    "basaliob": 0.015,
-    "bolusiob": 0.043,
-    "netbasalinsulin": -0.85,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:13:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.761,
-      "activity": -0.006,
-      "basaliob": -0.804,
-      "bolusiob": 0.043,
-      "netbasalinsulin": -2.3,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:13:31.036Z"
-    }
-  },
-  {
-    "iob": 0.054,
-    "activity": 0.0009,
-    "basaliob": 0.014,
-    "bolusiob": 0.04,
-    "netbasalinsulin": -0.8,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:18:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.781,
-      "activity": -0.0062,
-      "basaliob": -0.82,
-      "bolusiob": 0.04,
-      "netbasalinsulin": -2.3,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:18:31.036Z"
-    }
-  },
-  {
-    "iob": 0.049,
-    "activity": 0.0008,
-    "basaliob": 0.013,
-    "bolusiob": 0.036,
-    "netbasalinsulin": -0.75,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:23:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.749,
-      "activity": -0.0063,
-      "basaliob": -0.786,
-      "bolusiob": 0.036,
-      "netbasalinsulin": -2.25,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:23:31.036Z"
-    }
-  },
-  {
-    "iob": 0.046,
-    "activity": 0.0007,
-    "basaliob": 0.012,
-    "bolusiob": 0.034,
-    "netbasalinsulin": -0.7,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:28:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.767,
-      "activity": -0.0065,
-      "basaliob": -0.801,
-      "bolusiob": 0.034,
-      "netbasalinsulin": -2.25,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:28:31.036Z"
-    }
-  },
-  {
-    "iob": 0.042,
-    "activity": 0.0007,
-    "basaliob": 0.011,
-    "bolusiob": 0.031,
-    "netbasalinsulin": -0.65,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:33:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.785,
-      "activity": -0.0066,
-      "basaliob": -0.816,
-      "bolusiob": 0.031,
-      "netbasalinsulin": -2.25,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:33:31.036Z"
-    }
-  },
-  {
-    "iob": 0.039,
-    "activity": 0.0006,
-    "basaliob": 0.01,
-    "bolusiob": 0.029,
-    "netbasalinsulin": -0.6,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:38:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.801,
-      "activity": -0.0067,
-      "basaliob": -0.83,
-      "bolusiob": 0.029,
-      "netbasalinsulin": -2.25,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:38:31.036Z"
-    }
-  },
-  {
-    "iob": 0.036,
-    "activity": 0.0006,
-    "basaliob": 0.009,
-    "bolusiob": 0.026,
-    "netbasalinsulin": -0.55,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:43:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.817,
-      "activity": -0.0069,
-      "basaliob": -0.844,
-      "bolusiob": 0.026,
-      "netbasalinsulin": -2.25,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:43:31.036Z"
-    }
-  },
-  {
-    "iob": 0.033,
-    "activity": 0.0005,
-    "basaliob": 0.009,
-    "bolusiob": 0.024,
-    "netbasalinsulin": -0.5,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:48:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.833,
-      "activity": -0.007,
-      "basaliob": -0.857,
-      "bolusiob": 0.024,
-      "netbasalinsulin": -2.25,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:48:31.036Z"
-    }
-  },
-  {
-    "iob": 0.03,
-    "activity": 0.0005,
-    "basaliob": 0.008,
-    "bolusiob": 0.022,
-    "netbasalinsulin": -0.45,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:53:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.798,
-      "activity": -0.0071,
-      "basaliob": -0.82,
-      "bolusiob": 0.022,
-      "netbasalinsulin": -2.2,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:53:31.036Z"
-    }
-  },
-  {
-    "iob": 0.028,
-    "activity": 0.0005,
-    "basaliob": 0.007,
-    "bolusiob": 0.021,
-    "netbasalinsulin": -0.4,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T02:58:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.812,
-      "activity": -0.0072,
-      "basaliob": -0.833,
-      "bolusiob": 0.021,
-      "netbasalinsulin": -2.2,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T02:58:31.036Z"
-    }
-  },
-  {
-    "iob": 0.026,
-    "activity": 0.0004,
-    "basaliob": 0.007,
-    "bolusiob": 0.019,
-    "netbasalinsulin": -0.35,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T03:03:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.826,
-      "activity": -0.0073,
-      "basaliob": -0.845,
-      "bolusiob": 0.019,
-      "netbasalinsulin": -2.2,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T03:03:31.036Z"
-    }
-  },
-  {
-    "iob": 0.023,
-    "activity": 0.0004,
-    "basaliob": 0.006,
-    "bolusiob": 0.017,
-    "netbasalinsulin": -0.3,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T03:08:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.84,
-      "activity": -0.0073,
-      "basaliob": -0.857,
-      "bolusiob": 0.017,
-      "netbasalinsulin": -2.2,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T03:08:31.036Z"
-    }
-  },
-  {
-    "iob": 0.022,
-    "activity": 0.0004,
-    "basaliob": 0.006,
-    "bolusiob": 0.016,
-    "netbasalinsulin": -0.25,
-    "bolusinsulin": 2.3,
-    "time": "2025-02-19T03:13:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.853,
-      "activity": -0.0074,
-      "basaliob": -0.869,
-      "bolusiob": 0.016,
-      "netbasalinsulin": -2.2,
-      "bolusinsulin": 2.3,
-      "time": "2025-02-19T03:13:31.036Z"
-    }
-  },
-  {
-    "iob": 0.02,
-    "activity": 0.0003,
-    "basaliob": 0.005,
-    "bolusiob": 0.015,
-    "netbasalinsulin": -0.2,
-    "bolusinsulin": 2.05,
-    "time": "2025-02-19T03:18:31.036Z",
-    "iobWithZeroTemp": {
-      "iob": -0.866,
-      "activity": -0.0075,
-      "basaliob": -0.88,
-      "bolusiob": 0.015,
-      "netbasalinsulin": -2.2,
-      "bolusinsulin": 2.05,
-      "time": "2025-02-19T03:18:31.036Z"
-    }
-  }
-]

Разлика између датотеке није приказан због своје велике величине
+ 0 - 3439
TrioTests/OpenAPSSwiftTests/json/pump_history.json