MealTotalTests.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("MealTotal Tests") struct MealTotalTests {
  5. // Helper methods for testing
  6. func createBasicProfile() -> Profile {
  7. var profile = Profile()
  8. profile.dia = 4
  9. profile.maxMealAbsorptionTime = 6
  10. profile.maxCOB = 120
  11. // profile.carbsAbsorptionRate = 30
  12. profile.min5mCarbImpact = 3
  13. profile.carbRatio = 10
  14. profile.currentBasal = 1.0
  15. // Note: In Swift we need to set sensitivities differently than in JS
  16. profile
  17. .isfProfile = ComputedInsulinSensitivities(
  18. units: .mgdL,
  19. userPreferredUnits: .mgdL,
  20. sensitivities: [ComputedInsulinSensitivityEntry(sensitivity: 40, offset: 0, start: "00:00:00")]
  21. )
  22. return profile
  23. }
  24. func createBasicBasalProfile() -> [BasalProfileEntry] {
  25. [BasalProfileEntry(start: "00:00:00", minutes: 0, rate: 1.0)]
  26. }
  27. func createGlucoseData(baseTime: Date, pattern: [Int]) -> [BloodGlucose] {
  28. var result: [BloodGlucose] = []
  29. for (i, bg) in pattern.enumerated() {
  30. let timestamp = baseTime.addingTimeInterval(TimeInterval(i * 5 * 60))
  31. result.append(BloodGlucose(
  32. sgv: bg,
  33. date: Decimal(timestamp.timeIntervalSince1970 * 1000), // JS uses ms
  34. dateString: timestamp
  35. ))
  36. }
  37. return result.reversed()
  38. }
  39. @Test("should calculate carb absorption correctly") func calculateCarbAbsorption() async throws {
  40. let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  41. let mealTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  42. let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00") // 1 hour after meal
  43. // Create glucose data showing rise after carbs
  44. var bgValues = Array(repeating: 100, count: 13)
  45. for i in 3 ..< 8 {
  46. bgValues[i] = 100 + ((i - 2) * 10) // 100, 110, 120, 130, 140
  47. }
  48. for i in 8 ..< 13 {
  49. bgValues[i] = 150 // plateau
  50. }
  51. let glucoseData = createGlucoseData(baseTime: baseTime, pattern: bgValues)
  52. // Create insulin data - bolus at same time as carbs
  53. let pumpHistory = [
  54. PumpHistoryEvent(
  55. id: UUID().uuidString,
  56. type: .bolus,
  57. timestamp: mealTime,
  58. amount: 3.0
  59. )
  60. ]
  61. // Carb treatment
  62. let treatments = [
  63. MealInput(
  64. timestamp: mealTime,
  65. carbs: 30,
  66. bolus: nil
  67. ),
  68. MealInput(
  69. timestamp: mealTime,
  70. carbs: nil,
  71. bolus: 3
  72. )
  73. ]
  74. let profile = createBasicProfile()
  75. let basalProfile = createBasicBasalProfile()
  76. let result = try MealTotal.recentCarbs(
  77. treatments: treatments,
  78. pumpHistory: pumpHistory,
  79. profile: profile,
  80. basalProfile: basalProfile,
  81. glucose: glucoseData,
  82. time: testTime
  83. )
  84. // After 1 hour, we should see partial carb absorption
  85. #expect(result != nil)
  86. #expect(result!.mealCOB.isWithin(12 * 0.25, of: 12) == true, "mealCOB: \(result!.mealCOB.description)")
  87. #expect(
  88. result!.currentDeviation.isWithin(3 * 0.25, of: 3),
  89. "currentDeviation: \(result!.currentDeviation.description)"
  90. )
  91. }
  92. @Test("should return nil when no treatments provided") func emptyObjectWhenNoTreatments() async throws {
  93. let time = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  94. let glucoseData = [
  95. BloodGlucose(
  96. sgv: 100,
  97. date: Decimal(time.timeIntervalSince1970 * 1000),
  98. dateString: time
  99. )
  100. ]
  101. let profile = createBasicProfile()
  102. let basalProfile = createBasicBasalProfile()
  103. let result = try MealTotal.recentCarbs(
  104. treatments: [],
  105. pumpHistory: [],
  106. profile: profile,
  107. basalProfile: basalProfile,
  108. glucose: glucoseData,
  109. time: time
  110. )
  111. #expect(result == nil)
  112. }
  113. @Test("should calculate carbs correctly for treatments within the meal window") func calcCarbsWithinMealWindow() async throws {
  114. let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  115. let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  116. let treatments = [
  117. MealInput(
  118. timestamp: baseTime,
  119. carbs: 20,
  120. bolus: nil
  121. )
  122. ]
  123. // Create glucose pattern with slight rise
  124. let glucoseData = [
  125. BloodGlucose(
  126. sgv: 110,
  127. date: Decimal(baseTime.addingTimeInterval(60 * 60).timeIntervalSince1970 * 1000),
  128. dateString: baseTime.addingTimeInterval(60 * 60)
  129. ),
  130. BloodGlucose(
  131. sgv: 105,
  132. date: Decimal(baseTime.addingTimeInterval(30 * 60).timeIntervalSince1970 * 1000),
  133. dateString: baseTime.addingTimeInterval(30 * 60)
  134. ),
  135. BloodGlucose(
  136. sgv: 100,
  137. date: Decimal(baseTime.timeIntervalSince1970 * 1000),
  138. dateString: baseTime
  139. )
  140. ]
  141. let profile = createBasicProfile()
  142. let basalProfile = createBasicBasalProfile()
  143. let result = try MealTotal.recentCarbs(
  144. treatments: treatments,
  145. pumpHistory: [],
  146. profile: profile,
  147. basalProfile: basalProfile,
  148. glucose: glucoseData,
  149. time: testTime
  150. )
  151. #expect(result != nil)
  152. #expect(result!.carbs == 20)
  153. #expect(
  154. result!.currentDeviation.isWithin(0.67 * 0.25, of: 0.67) == true,
  155. "currentDeviation: \(result!.currentDeviation.description)"
  156. )
  157. #expect(result!.mealCOB.isWithin(14 * 0.25, of: 14) == true, "mealCOB: \(result!.mealCOB.description)")
  158. }
  159. @Test("should ignore treatments outside the meal window") func ignoreTreatmentsOutsideMealWindow() async throws {
  160. let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  161. let treatmentTime = Date.from(isoString: "2016-06-19T06:00:00-04:00") // 6 hours before
  162. let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  163. let treatments = [
  164. MealInput(
  165. timestamp: treatmentTime,
  166. carbs: 20,
  167. bolus: nil
  168. )
  169. ]
  170. // Create glucose pattern with slight rise
  171. let glucoseData = [
  172. BloodGlucose(
  173. sgv: 110,
  174. date: Decimal(baseTime.addingTimeInterval(60 * 60).timeIntervalSince1970 * 1000),
  175. dateString: baseTime.addingTimeInterval(60 * 60)
  176. ),
  177. BloodGlucose(
  178. sgv: 105,
  179. date: Decimal(baseTime.addingTimeInterval(30 * 60).timeIntervalSince1970 * 1000),
  180. dateString: baseTime.addingTimeInterval(30 * 60)
  181. ),
  182. BloodGlucose(
  183. sgv: 100,
  184. date: Decimal(baseTime.timeIntervalSince1970 * 1000),
  185. dateString: baseTime
  186. )
  187. ]
  188. let profile = createBasicProfile()
  189. let basalProfile = createBasicBasalProfile()
  190. let result = try MealTotal.recentCarbs(
  191. treatments: treatments,
  192. pumpHistory: [],
  193. profile: profile,
  194. basalProfile: basalProfile,
  195. glucose: glucoseData,
  196. time: testTime
  197. )
  198. #expect(result != nil)
  199. #expect(result?.carbs == 0)
  200. #expect(result?.mealCOB == 0)
  201. #expect(
  202. result?.currentDeviation.isWithin(0.67 * 0.25, of: 0.67) == true,
  203. "currentDeviation: \(result!.currentDeviation.description)"
  204. )
  205. }
  206. @Test("should respect maxMealAbsorptionTime from profile") func respectMaxMealAbsorptionTime() async throws {
  207. let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  208. let treatmentTime = Date.from(isoString: "2016-06-19T10:00:00-04:00") // 2 hours before
  209. let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  210. let treatments = [
  211. MealInput(
  212. timestamp: treatmentTime,
  213. carbs: 20,
  214. bolus: nil
  215. )
  216. ]
  217. // Create glucose pattern with slight rise
  218. let glucoseData = [
  219. BloodGlucose(
  220. sgv: 110,
  221. date: Decimal(baseTime.addingTimeInterval(60 * 60).timeIntervalSince1970 * 1000),
  222. dateString: baseTime.addingTimeInterval(60 * 60)
  223. ),
  224. BloodGlucose(
  225. sgv: 105,
  226. date: Decimal(baseTime.addingTimeInterval(30 * 60).timeIntervalSince1970 * 1000),
  227. dateString: baseTime.addingTimeInterval(30 * 60)
  228. ),
  229. BloodGlucose(
  230. sgv: 100,
  231. date: Decimal(baseTime.timeIntervalSince1970 * 1000),
  232. dateString: baseTime
  233. )
  234. ]
  235. var profile = createBasicProfile()
  236. profile.maxMealAbsorptionTime = 2 // 2 hour window
  237. let basalProfile = createBasicBasalProfile()
  238. let result = try MealTotal.recentCarbs(
  239. treatments: treatments,
  240. pumpHistory: [],
  241. profile: profile,
  242. basalProfile: basalProfile,
  243. glucose: glucoseData,
  244. time: testTime
  245. )
  246. #expect(result != nil)
  247. #expect(result?.carbs == 0)
  248. #expect(result?.mealCOB == 0)
  249. }
  250. @Test("should respect maxCOB from profile") func respectMaxCOB() async throws {
  251. let baseTime = Date.from(isoString: "2016-06-19T12:00:00-04:00")
  252. let testTime = Date.from(isoString: "2016-06-19T13:00:00-04:00")
  253. let treatments = [
  254. MealInput(
  255. timestamp: baseTime,
  256. carbs: 200,
  257. bolus: nil
  258. )
  259. ]
  260. // Create glucose pattern with slight rise
  261. let glucoseData = [
  262. BloodGlucose(
  263. sgv: 110,
  264. date: Decimal(baseTime.addingTimeInterval(60 * 60).timeIntervalSince1970 * 1000),
  265. dateString: baseTime.addingTimeInterval(60 * 60)
  266. ),
  267. BloodGlucose(
  268. sgv: 105,
  269. date: Decimal(baseTime.addingTimeInterval(30 * 60).timeIntervalSince1970 * 1000),
  270. dateString: baseTime.addingTimeInterval(30 * 60)
  271. ),
  272. BloodGlucose(
  273. sgv: 100,
  274. date: Decimal(baseTime.timeIntervalSince1970 * 1000),
  275. dateString: baseTime
  276. )
  277. ]
  278. let profile = createBasicProfile()
  279. let basalProfile = createBasicBasalProfile()
  280. let result = try MealTotal.recentCarbs(
  281. treatments: treatments,
  282. pumpHistory: [],
  283. profile: profile,
  284. basalProfile: basalProfile,
  285. glucose: glucoseData,
  286. time: testTime
  287. )
  288. #expect(result != nil)
  289. #expect(result!.mealCOB <= 120)
  290. }
  291. }