MealCobTests.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("MealCob Tests") struct MealCobTests {
  5. // Helper function to create basic profile for testing
  6. func createBasicProfile() -> Profile {
  7. var profile = Profile()
  8. profile.dia = 4
  9. profile.maxMealAbsorptionTime = 6
  10. profile.min5mCarbImpact = 3
  11. profile.carbRatio = 10
  12. profile.currentBasal = 1.0
  13. profile.isfProfile = ComputedInsulinSensitivities(
  14. units: .mgdL,
  15. userPreferredUnits: .mgdL,
  16. sensitivities: [ComputedInsulinSensitivityEntry(sensitivity: 40, offset: 0, start: "00:00:00")]
  17. )
  18. return profile
  19. }
  20. // Helper function to create basal profile
  21. func createBasalProfile() -> [BasalProfileEntry] {
  22. [BasalProfileEntry(start: "00:00:00", minutes: 0, rate: 1.0)]
  23. }
  24. // Helper function to create glucose data from values and timestamps
  25. func createGlucoseData(startTime: Date, values: [Int], intervalMinutes: Int = 5) -> [BloodGlucose] {
  26. values.enumerated().map { i, glucose in
  27. let timestamp = startTime.addingTimeInterval(TimeInterval(i * intervalMinutes * 60))
  28. return BloodGlucose(
  29. sgv: glucose,
  30. date: Decimal(timestamp.timeIntervalSince1970 * 1000), // JS uses ms
  31. dateString: timestamp
  32. )
  33. }.reversed()
  34. }
  35. @Test("should detect carb absorption with rising glucose") func detectCarbAbsorptionWithRisingGlucose() async throws {
  36. let mealTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  37. var carbImpactTime = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  38. // Create glucose data showing significant rise after meal
  39. let glucoseValues = [100, 105, 110, 115, 120, 130, 140, 150, 155, 160, 160, 160, 160]
  40. let glucoseData = createGlucoseData(startTime: mealTime, values: glucoseValues)
  41. var profile = createBasicProfile()
  42. let basalProfile = createBasalProfile()
  43. let pumpHistory: [PumpHistoryEvent] = []
  44. // Test with carbImpactTime
  45. var result = try MealCob.detectCarbAbsorption(
  46. clock: &carbImpactTime, // no pump events, set to whatever
  47. glucose: glucoseData,
  48. pumpHistory: pumpHistory,
  49. basalProfile: basalProfile,
  50. profile: &profile,
  51. mealDate: mealTime,
  52. carbImpactDate: carbImpactTime
  53. )
  54. #expect(result.carbsAbsorbed.isWithin(0.01, of: 9.75))
  55. // Test without carbImpactTime
  56. result = try MealCob.detectCarbAbsorption(
  57. clock: &carbImpactTime, // no pump events, set to whatever
  58. glucose: glucoseData,
  59. pumpHistory: pumpHistory,
  60. basalProfile: basalProfile,
  61. profile: &profile,
  62. mealDate: mealTime,
  63. carbImpactDate: nil
  64. )
  65. #expect(result.carbsAbsorbed.isWithin(0.01, of: 14.75))
  66. }
  67. @Test("should handle stable glucose (no carb absorption)") func handleStableGlucose() async throws {
  68. let mealTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  69. var carbImpactTime = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  70. // Create stable glucose data
  71. let glucoseValues = [100, 100, 100, 100, 100, 100]
  72. let glucoseData = createGlucoseData(startTime: mealTime, values: glucoseValues)
  73. var profile = createBasicProfile()
  74. let basalProfile = createBasalProfile()
  75. let pumpHistory: [PumpHistoryEvent] = []
  76. let result = try MealCob.detectCarbAbsorption(
  77. clock: &carbImpactTime, // no pump events, set to whatever
  78. glucose: glucoseData,
  79. pumpHistory: pumpHistory,
  80. basalProfile: basalProfile,
  81. profile: &profile,
  82. mealDate: mealTime,
  83. carbImpactDate: carbImpactTime
  84. )
  85. #expect(result.carbsAbsorbed == 0)
  86. }
  87. @Test("should handle falling glucose (negative deviation)") func handleFallingGlucose() async throws {
  88. let mealTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  89. var carbImpactTime = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  90. // Create falling glucose data: 150 -> 125
  91. let glucoseValues = [150, 145, 140, 135, 130, 125]
  92. let glucoseData = createGlucoseData(startTime: mealTime, values: glucoseValues)
  93. var profile = createBasicProfile()
  94. let basalProfile = createBasalProfile()
  95. let pumpHistory: [PumpHistoryEvent] = []
  96. let result = try MealCob.detectCarbAbsorption(
  97. clock: &carbImpactTime, // no pump events, set to whatever
  98. glucose: glucoseData,
  99. pumpHistory: pumpHistory,
  100. basalProfile: basalProfile,
  101. profile: &profile,
  102. mealDate: mealTime,
  103. carbImpactDate: carbImpactTime
  104. )
  105. #expect(result.carbsAbsorbed == 0) // No carbs absorbed when glucose is falling
  106. }
  107. @Test("should stop processing when pre-meal BG is found") func stopProcessingWhenPreMealBGFound() async throws {
  108. let mealTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  109. var carbImpactTime = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  110. // Include glucose data from before meal time
  111. let glucoseData = [
  112. BloodGlucose(
  113. sgv: 150,
  114. date: Decimal(mealTime.addingTimeInterval(60 * 60).timeIntervalSince1970 * 1000), // 1 hour after meal
  115. dateString: mealTime.addingTimeInterval(60 * 60)
  116. ),
  117. BloodGlucose(
  118. sgv: 120,
  119. date: Decimal(mealTime.addingTimeInterval(30 * 60).timeIntervalSince1970 * 1000), // 30 minutes after meal
  120. dateString: mealTime.addingTimeInterval(30 * 60)
  121. ),
  122. BloodGlucose(
  123. sgv: 100,
  124. date: Decimal(mealTime.addingTimeInterval(-30 * 60).timeIntervalSince1970 * 1000),
  125. // 30 minutes before meal (pre-meal)
  126. dateString: mealTime.addingTimeInterval(-30 * 60)
  127. )
  128. ]
  129. var profile = createBasicProfile()
  130. let basalProfile = createBasalProfile()
  131. let pumpHistory: [PumpHistoryEvent] = []
  132. let result = try MealCob.detectCarbAbsorption(
  133. clock: &carbImpactTime, // no pump events, set to whatever
  134. glucose: glucoseData,
  135. pumpHistory: pumpHistory,
  136. basalProfile: basalProfile,
  137. profile: &profile,
  138. mealDate: mealTime,
  139. carbImpactDate: carbImpactTime
  140. )
  141. #expect(result.carbsAbsorbed.isWithin(0.01, of: 3.75))
  142. }
  143. @Test("should respect maxMealAbsorptionTime") func respectMaxMealAbsorptionTime() async throws {
  144. let mealTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  145. var carbImpactTime = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  146. // Create glucose data spanning longer than maxMealAbsorptionTime
  147. var glucoseValues: [Int] = []
  148. for i in 0 ..< 100 { // 100 * 5 minutes = ~8 hours
  149. let value = Int(100 + sin(Double(i) * 0.1) * 20) // Sinusoidal pattern
  150. glucoseValues.append(value)
  151. }
  152. let glucoseData = createGlucoseData(
  153. startTime: mealTime.addingTimeInterval(-2 * 60 * 60), // Start 2 hours before meal
  154. values: glucoseValues
  155. )
  156. var profile = createBasicProfile()
  157. profile.maxMealAbsorptionTime = 2 // Only 2 hours
  158. let basalProfile = createBasalProfile()
  159. let pumpHistory: [PumpHistoryEvent] = []
  160. let result = try MealCob.detectCarbAbsorption(
  161. clock: &carbImpactTime, // no pump events, set to whatever
  162. glucose: glucoseData,
  163. pumpHistory: pumpHistory,
  164. basalProfile: basalProfile,
  165. profile: &profile,
  166. mealDate: mealTime,
  167. carbImpactDate: carbImpactTime
  168. )
  169. #expect(result.carbsAbsorbed.isWithin(0.01, of: 40.5))
  170. }
  171. @Test("should handle minimum carb impact from profile") func handleMinimumCarbImpactFromProfile() async throws {
  172. var mealTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  173. // Create glucose data with slight rise to trigger carb absorption
  174. let glucoseValues = [100, 101, 102, 103, 104, 105]
  175. let glucoseData = createGlucoseData(startTime: mealTime, values: glucoseValues)
  176. var profile = createBasicProfile()
  177. profile.min5mCarbImpact = 5 // Higher minimum impact
  178. let basalProfile = createBasalProfile()
  179. let pumpHistory: [PumpHistoryEvent] = []
  180. let result = try MealCob.detectCarbAbsorption(
  181. clock: &mealTime, // no pump events, set to whatever
  182. glucose: glucoseData,
  183. pumpHistory: pumpHistory,
  184. basalProfile: basalProfile,
  185. profile: &profile,
  186. mealDate: mealTime,
  187. carbImpactDate: nil
  188. )
  189. #expect(result.carbsAbsorbed.isWithin(0.01, of: 3.75))
  190. }
  191. }