| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- import Foundation
- import Testing
- @testable import Trio
- @Suite("Meal glucose bucketing tests") struct MealCobBucketingTests {
- // Default test profile - matches JS exactly
- func createDefaultProfile() -> Profile {
- var profile = Profile()
- profile.dia = 4
- profile.maxMealAbsorptionTime = 6
- profile.min5mCarbImpact = 3
- profile.carbRatio = 10
- return profile
- }
- // Helper to create glucose entry - matches JS structure
- func createGlucoseEntry(glucose: Int, timeMs: Double) -> BloodGlucose {
- let date = Date(timeIntervalSince1970: timeMs / 1000)
- return BloodGlucose(
- sgv: glucose,
- date: Decimal(timeMs),
- dateString: date,
- glucose: glucose
- )
- }
- // Note: glucose_data is expected in reverse chronological order (newest first)
- // The bucketGlucoseData function maintains this order in its output
- @Test(
- "should handle normal 5-minute interval data without modification"
- ) func shouldHandleNormal5MinuteIntervalDataWithoutModification() async throws {
- let mealTime = Date.from(isoString: "2024-01-01T12:00:00-05:00")
- let mealTimeMs = mealTime.timeIntervalSince1970 * 1000
- // Create regular 5-minute interval data (chronological order)
- var glucose_data = [
- createGlucoseEntry(glucose: 100, timeMs: mealTimeMs),
- createGlucoseEntry(glucose: 105, timeMs: mealTimeMs + 5 * 60 * 1000),
- createGlucoseEntry(glucose: 110, timeMs: mealTimeMs + 10 * 60 * 1000),
- createGlucoseEntry(glucose: 115, timeMs: mealTimeMs + 15 * 60 * 1000)
- ]
- glucose_data.reverse() // Convert to reverse chronological order
- let result = try MealCob.bucketGlucoseForCob(
- glucose: glucose_data,
- profile: createDefaultProfile(),
- mealDate: mealTime,
- carbImpactDate: nil
- )
- // Should return same number of entries
- #expect(result.count == 4)
- // Values should be unchanged (in reverse chronological order)
- #expect(result[0].glucose == 115)
- #expect(result[1].glucose == 110)
- #expect(result[2].glucose == 105)
- #expect(result[3].glucose == 100)
- }
- @Test("should interpolate missing data when gap > 8 minutes") func shouldInterpolateMissingDataWhenGapGreaterThan8Minutes(
- ) async throws {
- let mealTime = Date.from(isoString: "2024-01-01T12:00:00-05:00")
- let mealTimeMs = mealTime.timeIntervalSince1970 * 1000
- // Create data with a 21-minute gap (chronological order)
- var glucose_data = [
- createGlucoseEntry(glucose: 99, timeMs: mealTimeMs),
- createGlucoseEntry(glucose: 120, timeMs: mealTimeMs + 21 * 60 * 1000) // 21 min gap
- ]
- glucose_data.reverse() // Convert to reverse chronological order
- let result = try MealCob.bucketGlucoseForCob(
- glucose: glucose_data,
- profile: createDefaultProfile(),
- mealDate: mealTime,
- carbImpactDate: nil
- )
- // Should have interpolated 4 additional points (5, 10, 15, 20 minutes)
- #expect(result.count == 5)
- // Check interpolated values (in reverse chronological order)
- #expect(result[0].glucose == 120) // original (newest)
- #expect(result[1].glucose == 115) // interpolated
- #expect(result[2].glucose == 110) // interpolated
- #expect(result[3].glucose == 105) // interpolated
- #expect(result[4].glucose == 100) // interpolated
- // Check that dates are properly set
- #expect(result[1].date == mealTime.addingTimeInterval(16 * 60))
- #expect(result[2].date == mealTime.addingTimeInterval(11 * 60))
- #expect(result[3].date == mealTime.addingTimeInterval(6 * 60))
- #expect(result[4].date == mealTime.addingTimeInterval(1 * 60))
- }
- @Test("should stop processing after maxMealAbsorptionTime") func shouldStopProcessingAfterMaxMealAbsorptionTime() async throws {
- let mealTime = Date.from(isoString: "2024-01-01T12:00:00-05:00")
- let mealTimeMs = mealTime.timeIntervalSince1970 * 1000
- // Create data spanning 8 hours (chronological order)
- var glucose_data: [BloodGlucose] = []
- for i in 0 ... 96 { // 96 * 5 min = 8 hours
- glucose_data.append(createGlucoseEntry(
- glucose: 100 + i,
- timeMs: mealTimeMs + Double(i) * 5 * 60 * 1000
- ))
- }
- glucose_data.reverse() // Convert to reverse chronological order
- // Set maxMealAbsorptionTime to 2 hours
- var profile = createDefaultProfile()
- profile.maxMealAbsorptionTime = 2
- let result = try MealCob.bucketGlucoseForCob(
- glucose: glucose_data,
- profile: profile,
- mealDate: mealTime,
- carbImpactDate: nil
- )
- // JS test expects 72 entries (not 25 as in original Swift test)
- print(result)
- #expect(result.count == 72)
- // Check specific values to match JS test
- #expect(result[0].glucose == 196)
- #expect(result[1].glucose == 195)
- #expect(result[12].glucose == 178)
- #expect(result[24].glucose == 160)
- }
- @Test("should only process data within 45 minutes in CI mode") func shouldOnlyProcessDataWithin45MinutesInCIMode() async throws {
- let mealTime = Date.from(isoString: "2024-01-01T12:00:00-05:00")
- let ciTime = Date.from(isoString: "2024-01-01T14:00:00-05:00") // 2 hours after meal
- let mealTimeMs = mealTime.timeIntervalSince1970 * 1000
- // Create data spanning 3 hours (chronological order)
- var glucose_data: [BloodGlucose] = []
- for i in 0 ... 36 { // 36 * 5 min = 3 hours
- glucose_data.append(createGlucoseEntry(
- glucose: 100 + i,
- timeMs: mealTimeMs + Double(i) * 5 * 60 * 1000
- ))
- }
- glucose_data.reverse() // Convert to reverse chronological order
- let result = try MealCob.bucketGlucoseForCob(
- glucose: glucose_data,
- profile: createDefaultProfile(),
- mealDate: mealTime,
- carbImpactDate: ciTime
- )
- // JS test shows this captures more than 45 minutes due to the bucketing logic
- for entry in result {
- let minutesFromCI = abs(ciTime.timeIntervalSince(entry.date)) / 60
- #expect(minutesFromCI <= 120) // JS test uses 120, not 45
- }
- // JS test expects 21 entries
- #expect(result.count == 21)
- }
- @Test("should stop processing when pre-meal BG is found") func shouldStopProcessingWhenPreMealBGIsFound() async throws {
- let mealTime = Date.from(isoString: "2024-01-01T12:00:00-05:00")
- let mealTimeMs = mealTime.timeIntervalSince1970 * 1000
- // Create data that includes pre-meal values (chronological order)
- var glucose_data = [
- createGlucoseEntry(glucose: 90, timeMs: mealTimeMs - 10 * 60 * 1000), // 10 min before meal
- createGlucoseEntry(glucose: 95, timeMs: mealTimeMs - 5 * 60 * 1000), // 5 min before meal
- createGlucoseEntry(glucose: 100, timeMs: mealTimeMs),
- createGlucoseEntry(glucose: 105, timeMs: mealTimeMs + 5 * 60 * 1000),
- createGlucoseEntry(glucose: 110, timeMs: mealTimeMs + 10 * 60 * 1000),
- createGlucoseEntry(glucose: 115, timeMs: mealTimeMs + 15 * 60 * 1000) // 15 min after
- ]
- glucose_data.reverse() // Convert to reverse chronological order
- let result = try MealCob.bucketGlucoseForCob(
- glucose: glucose_data,
- profile: createDefaultProfile(),
- mealDate: mealTime,
- carbImpactDate: nil
- )
- // JS test expects 5 entries (includes one pre-meal entry due to bug)
- #expect(result.count == 5)
- // Values should be unchanged (in reverse chronological order)
- #expect(result[0].glucose == 115)
- #expect(result[1].glucose == 110)
- #expect(result[2].glucose == 105)
- #expect(result[3].glucose == 100)
- #expect(result[4].glucose == 95) // This pre-meal entry is included due to JS bug
- }
- @Test(
- "should average glucose values when readings are very close (≤ 2 minutes)"
- ) func shouldAverageGlucoseValuesWhenReadingsAreVeryClose() async throws {
- let mealTime = Date.from(isoString: "2024-01-01T12:00:00-05:00")
- let mealTimeMs = mealTime.timeIntervalSince1970 * 1000
- // Create data with readings 1 minute apart (chronological order)
- var glucose_data = [
- createGlucoseEntry(glucose: 100, timeMs: mealTimeMs),
- createGlucoseEntry(glucose: 102, timeMs: mealTimeMs + 1 * 60 * 1000), // 1 min later
- createGlucoseEntry(glucose: 104, timeMs: mealTimeMs + 2 * 60 * 1000), // 2 min later
- createGlucoseEntry(glucose: 110, timeMs: mealTimeMs + 5 * 60 * 1000) // 5 min later
- ]
- glucose_data.reverse() // Convert to reverse chronological order
- let result = try MealCob.bucketGlucoseForCob(
- glucose: glucose_data,
- profile: createDefaultProfile(),
- mealDate: mealTime,
- carbImpactDate: nil
- )
- // Close readings should be averaged (in reverse chronological order)
- #expect(result.count == 2)
- #expect(result[0].glucose == 110)
- // JS test shows averaging bug results in 101.5, not 102
- #expect(result[1].glucose == 101.5)
- }
- @Test("should cap interpolation at 240 minutes for very large gaps") func shouldCapInterpolationAt240MinutesForVeryLargeGaps(
- ) async throws {
- let mealTime = Date.from(isoString: "2024-01-01T12:00:00-05:00")
- let mealTimeMs = mealTime.timeIntervalSince1970 * 1000
- // Create data with a 6-hour (360 minute) gap (chronological order)
- var glucose_data = [
- createGlucoseEntry(glucose: 100, timeMs: mealTimeMs),
- createGlucoseEntry(glucose: 200, timeMs: mealTimeMs + 360 * 60 * 1000) // 6 hour gap
- ]
- glucose_data.reverse() // Convert to reverse chronological order
- let result = try MealCob.bucketGlucoseForCob(
- glucose: glucose_data,
- profile: createDefaultProfile(),
- mealDate: mealTime,
- carbImpactDate: nil
- )
- // JS test expects 48 entries due to capping at 240 minutes
- #expect(result.count == 48)
- // Check that interpolation stopped at 240 minutes
- let gapMinutes = result[0].date.timeIntervalSince(result[result.count - 1].date) / 60
- #expect(gapMinutes == 235)
- }
- }
|