import Foundation import Testing @testable import Trio @Suite("MealTotal Tests") struct MealTotalTests { // Helper methods for testing func createBasicProfile() -> Profile { var profile = Profile() profile.dia = 4 profile.maxMealAbsorptionTime = 6 profile.maxCOB = 120 // profile.carbsAbsorptionRate = 30 profile.min5mCarbImpact = 3 profile.carbRatio = 10 profile.currentBasal = 1.0 // Note: In Swift we need to set sensitivities differently than in JS profile .isfProfile = ComputedInsulinSensitivities( units: .mgdL, userPreferredUnits: .mgdL, sensitivities: [ComputedInsulinSensitivityEntry(sensitivity: 40, offset: 0, start: "00:00:00")] ) return profile } func createBasicBasalProfile() -> [BasalProfileEntry] { [BasalProfileEntry(start: "00:00:00", minutes: 0, rate: 1.0)] } func createGlucoseData(baseTime: Date, pattern: [Int]) -> [BloodGlucose] { var result: [BloodGlucose] = [] for (i, bg) in pattern.enumerated() { let timestamp = baseTime.addingTimeInterval(TimeInterval(i * 5 * 60)) result.append(BloodGlucose( sgv: bg, date: Decimal(timestamp.timeIntervalSince1970 * 1000), // JS uses ms dateString: timestamp )) } return result.reversed() } @Test("should calculate carb absorption correctly") func calculateCarbAbsorption() async throws { let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00") let mealTime = Date.from(isoString: "2016-06-19T12:00:00-04:00") let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00") // 1 hour after meal // Create glucose data showing rise after carbs var bgValues = Array(repeating: 100, count: 13) for i in 3 ..< 8 { bgValues[i] = 100 + ((i - 2) * 10) // 100, 110, 120, 130, 140 } for i in 8 ..< 13 { bgValues[i] = 150 // plateau } let glucoseData = createGlucoseData(baseTime: baseTime, pattern: bgValues) // Create insulin data - bolus at same time as carbs let pumpHistory = [ PumpHistoryEvent( id: UUID().uuidString, type: .bolus, timestamp: mealTime, amount: 3.0 ) ] // Carb treatment let treatments = [ MealInput( timestamp: mealTime, carbs: 30, bolus: nil ) ] let profile = createBasicProfile() let basalProfile = createBasicBasalProfile() let result = try MealTotal.recentCarbs( treatments: treatments, pumpHistory: pumpHistory, profile: profile, basalProfile: basalProfile, glucose: glucoseData, time: testTime ) // After 1 hour, we should see partial carb absorption #expect(result != nil) // at this level JS is rounding, thus the 0.5 #expect(result!.mealCOB.isWithin(0.5, of: 10) == true, "mealCOB: \(result!.mealCOB.description)") #expect( result!.currentDeviation == 3.6, "currentDeviation: \(result!.currentDeviation!.description)" ) } @Test("should return result with zero carbs when treatments is empty array") func emptyObjectWhenNoTreatments() async throws { let time = Date.from(isoString: "2016-06-19T13:00:00-04:00") let glucoseData = [ BloodGlucose( sgv: 100, date: Decimal(time.timeIntervalSince1970 * 1000), dateString: time ) ] let profile = createBasicProfile() let basalProfile = createBasicBasalProfile() let result = try MealTotal.recentCarbs( treatments: [], pumpHistory: [], profile: profile, basalProfile: basalProfile, glucose: glucoseData, time: time ) // With empty treatments, JS returns a full result object // with zero carbs/COB and sentinel deviation values #expect(result != nil) #expect(result?.carbs == 0) #expect(result?.mealCOB == 0) #expect(result?.currentDeviation == nil) #expect(result?.maxDeviation == 0) #expect(result?.minDeviation == 999) #expect(result?.slopeFromMaxDeviation == 0) #expect(result?.slopeFromMinDeviation == 999) #expect(result?.allDeviations == []) #expect(result?.lastCarbTime == 0) } @Test("should calculate carbs correctly for treatments within the meal window") func calcCarbsWithinMealWindow() async throws { let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00") let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00") let treatments = [ MealInput( timestamp: baseTime, carbs: 20, bolus: nil ) ] // Create glucose pattern with slight rise let glucoseData = [ BloodGlucose( sgv: 110, date: Decimal(baseTime.addingTimeInterval(60 * 60).timeIntervalSince1970 * 1000), dateString: baseTime.addingTimeInterval(60 * 60) ), BloodGlucose( sgv: 105, date: Decimal(baseTime.addingTimeInterval(30 * 60).timeIntervalSince1970 * 1000), dateString: baseTime.addingTimeInterval(30 * 60) ), BloodGlucose( sgv: 100, date: Decimal(baseTime.timeIntervalSince1970 * 1000), dateString: baseTime ) ] let profile = createBasicProfile() let basalProfile = createBasicBasalProfile() let result = try MealTotal.recentCarbs( treatments: treatments, pumpHistory: [], profile: profile, basalProfile: basalProfile, glucose: glucoseData, time: testTime ) #expect(result != nil) #expect(result!.carbs == 20) #expect( result!.currentDeviation!.isWithin(0.02, of: 0.67) == true, "currentDeviation: \(result!.currentDeviation!.description)" ) #expect(result!.mealCOB.isWithin(0.25, of: 14) == true, "mealCOB: \(result!.mealCOB.description)") } @Test("should ignore treatments outside the meal window") func ignoreTreatmentsOutsideMealWindow() async throws { let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00") let treatmentTime = Date.from(isoString: "2016-06-19T06:00:00-04:00") // 6 hours before let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00") let treatments = [ MealInput( timestamp: treatmentTime, carbs: 20, bolus: nil ) ] // Create glucose pattern with slight rise let glucoseData = [ BloodGlucose( sgv: 110, date: Decimal(baseTime.addingTimeInterval(60 * 60).timeIntervalSince1970 * 1000), dateString: baseTime.addingTimeInterval(60 * 60) ), BloodGlucose( sgv: 105, date: Decimal(baseTime.addingTimeInterval(30 * 60).timeIntervalSince1970 * 1000), dateString: baseTime.addingTimeInterval(30 * 60) ), BloodGlucose( sgv: 100, date: Decimal(baseTime.timeIntervalSince1970 * 1000), dateString: baseTime ) ] let profile = createBasicProfile() let basalProfile = createBasicBasalProfile() let result = try MealTotal.recentCarbs( treatments: treatments, pumpHistory: [], profile: profile, basalProfile: basalProfile, glucose: glucoseData, time: testTime ) #expect(result != nil) #expect(result?.carbs == 0) #expect(result?.mealCOB == 0) #expect( result?.currentDeviation!.isWithin(0.02, of: 0.67) == true, "currentDeviation: \(result!.currentDeviation!.description)" ) } @Test("should respect maxMealAbsorptionTime from profile") func respectMaxMealAbsorptionTime() async throws { let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00") let treatmentTime = Date.from(isoString: "2016-06-19T10:00:00-04:00") // 2 hours before let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00") let treatments = [ MealInput( timestamp: treatmentTime, carbs: 20, bolus: nil ) ] // Create glucose pattern with slight rise let glucoseData = [ BloodGlucose( sgv: 110, date: Decimal(baseTime.addingTimeInterval(60 * 60).timeIntervalSince1970 * 1000), dateString: baseTime.addingTimeInterval(60 * 60) ), BloodGlucose( sgv: 105, date: Decimal(baseTime.addingTimeInterval(30 * 60).timeIntervalSince1970 * 1000), dateString: baseTime.addingTimeInterval(30 * 60) ), BloodGlucose( sgv: 100, date: Decimal(baseTime.timeIntervalSince1970 * 1000), dateString: baseTime ) ] var profile = createBasicProfile() profile.maxMealAbsorptionTime = 2 // 2 hour window let basalProfile = createBasicBasalProfile() let result = try MealTotal.recentCarbs( treatments: treatments, pumpHistory: [], profile: profile, basalProfile: basalProfile, glucose: glucoseData, time: testTime ) #expect(result != nil) #expect(result?.carbs == 0) #expect(result?.mealCOB == 0) } @Test("should respect maxCOB from profile") func respectMaxCOB() async throws { let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00") let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00") let treatments = [ MealInput( timestamp: baseTime, carbs: 200, bolus: nil ) ] // Create glucose pattern with slight rise let glucoseData = [ BloodGlucose( sgv: 110, date: Decimal(baseTime.addingTimeInterval(60 * 60).timeIntervalSince1970 * 1000), dateString: baseTime.addingTimeInterval(60 * 60) ), BloodGlucose( sgv: 105, date: Decimal(baseTime.addingTimeInterval(30 * 60).timeIntervalSince1970 * 1000), dateString: baseTime.addingTimeInterval(30 * 60) ), BloodGlucose( sgv: 100, date: Decimal(baseTime.timeIntervalSince1970 * 1000), dateString: baseTime ) ] let profile = createBasicProfile() let basalProfile = createBasicBasalProfile() let result = try MealTotal.recentCarbs( treatments: treatments, pumpHistory: [], profile: profile, basalProfile: basalProfile, glucose: glucoseData, time: testTime ) #expect(result != nil) #expect(result!.mealCOB <= 120) } }