| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- 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)
- }
- }
|