StatsDataFetcher.swift 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. // LoopFollow
  2. // StatsDataFetcher.swift
  3. import Foundation
  4. class StatsDataFetcher {
  5. weak var mainViewController: MainViewController?
  6. weak var dataService: StatsDataService?
  7. init(mainViewController: MainViewController?) {
  8. self.mainViewController = mainViewController
  9. }
  10. func fetchBGData(days: Int, completion: @escaping () -> Void) {
  11. guard let mainVC = mainViewController, IsNightscoutEnabled() else {
  12. completion()
  13. return
  14. }
  15. var parameters: [String: String] = [:]
  16. let utcISODateFormatter = ISO8601DateFormatter()
  17. let startDate = dataService?.startDate ?? dateTimeUtils.displayCalendar().date(byAdding: .day, value: -1 * days, to: Date())!
  18. parameters["count"] = "\(days * 2 * 24 * 60 / 5)"
  19. parameters["find[dateString][$gte]"] = utcISODateFormatter.string(from: startDate)
  20. parameters["find[type][$ne]"] = "cal"
  21. NightscoutUtils.executeRequest(eventType: .sgv, parameters: parameters) { (result: Result<[ShareGlucoseData], Error>) in
  22. switch result {
  23. case let .success(entriesResponse):
  24. var nsData = entriesResponse
  25. DispatchQueue.main.async {
  26. // Transform NS data
  27. for i in 0 ..< nsData.count {
  28. nsData[i].date /= 1000
  29. nsData[i].date.round(FloatingPointRoundingRule.toNearestOrEven)
  30. }
  31. var nsData2: [ShareGlucoseData] = []
  32. var lastAddedTime = Double.infinity
  33. var lastAddedSGV: Int?
  34. let minInterval: Double = 30
  35. for reading in nsData {
  36. if (lastAddedSGV == nil || lastAddedSGV != reading.sgv) || (lastAddedTime - reading.date >= minInterval) {
  37. nsData2.append(reading)
  38. lastAddedTime = reading.date
  39. lastAddedSGV = reading.sgv
  40. }
  41. }
  42. let cutoffTime = self.dataService?.startDate.timeIntervalSince1970 ?? (Date().timeIntervalSince1970 - (Double(days) * 24 * 60 * 60))
  43. mainVC.statsBGData.removeAll { $0.date < cutoffTime }
  44. let existingDates = Set(mainVC.statsBGData.map { Int($0.date) })
  45. for reading in nsData2 {
  46. if !existingDates.contains(Int(reading.date)), reading.date >= cutoffTime {
  47. mainVC.statsBGData.append(reading)
  48. }
  49. }
  50. mainVC.statsBGData.sort { $0.date < $1.date }
  51. completion()
  52. }
  53. case let .failure(error):
  54. LogManager.shared.log(category: .nightscout, message: "Failed to fetch stats BG data: \(error)", limitIdentifier: "Failed to fetch stats BG data")
  55. DispatchQueue.main.async {
  56. completion()
  57. }
  58. }
  59. }
  60. }
  61. func fetchTreatmentsData(days: Int, completion: @escaping () -> Void) {
  62. guard let mainVC = mainViewController, IsNightscoutEnabled(), Storage.shared.downloadTreatments.value else {
  63. completion()
  64. return
  65. }
  66. ensureBasalProfileLoaded(mainVC: mainVC) {
  67. self.fetchAndMergeTreatments(days: days, mainVC: mainVC, completion: completion)
  68. }
  69. }
  70. private func ensureBasalProfileLoaded(mainVC: MainViewController, completion: @escaping () -> Void) {
  71. if !mainVC.basalProfile.isEmpty {
  72. completion()
  73. return
  74. }
  75. let formatter = ISO8601DateFormatter()
  76. formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  77. let parameters: [String: String] = [
  78. "count": "1",
  79. "find[startDate][$lte]": formatter.string(from: Date().addingTimeInterval(60)),
  80. ]
  81. NightscoutUtils.executeRequest(eventType: .profile, parameters: parameters) { (result: Result<[NSProfile], Error>) in
  82. switch result {
  83. case let .success(profiles):
  84. guard let profileData = profiles.first else {
  85. LogManager.shared.log(category: .nightscout, message: "ensureBasalProfileLoaded, no profile records returned")
  86. DispatchQueue.main.async { completion() }
  87. return
  88. }
  89. let profileStore = profileData.store["default"] ??
  90. profileData.store["Default"] ??
  91. profileData.store[profileData.defaultProfile]
  92. DispatchQueue.main.async {
  93. if let profileStore {
  94. mainVC.basalProfile = profileStore.basal.map {
  95. MainViewController.basalProfileStruct(
  96. value: $0.value,
  97. time: $0.time,
  98. timeAsSeconds: $0.timeAsSeconds
  99. )
  100. }
  101. }
  102. completion()
  103. }
  104. case let .failure(error):
  105. LogManager.shared.log(
  106. category: .nightscout,
  107. message: "Failed to fetch profile data for stats basal calculations: \(error.localizedDescription)"
  108. )
  109. DispatchQueue.main.async {
  110. completion()
  111. }
  112. }
  113. }
  114. }
  115. private func fetchAndMergeTreatments(days: Int, mainVC: MainViewController, completion: @escaping () -> Void) {
  116. let utcISODateFormatter = ISO8601DateFormatter()
  117. utcISODateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
  118. utcISODateFormatter.timeZone = TimeZone(abbreviation: "UTC")
  119. let startDate = dataService?.startDate ?? dateTimeUtils.displayCalendar().date(byAdding: .day, value: -1 * days, to: Date())!
  120. let endDate = dataService?.endDate ?? Date()
  121. let startTimeString = utcISODateFormatter.string(from: startDate)
  122. let currentTimeString = utcISODateFormatter.string(from: endDate)
  123. let estimatedCount = max(days * 100, 5000)
  124. let parameters: [String: String] = [
  125. "find[created_at][$gte]": startTimeString,
  126. "find[created_at][$lte]": currentTimeString,
  127. "count": "\(estimatedCount)",
  128. ]
  129. NightscoutUtils.executeDynamicRequest(eventType: .treatments, parameters: parameters) { (result: Result<Any, Error>) in
  130. switch result {
  131. case let .success(data):
  132. if let entries = data as? [[String: AnyObject]] {
  133. DispatchQueue.main.async {
  134. self.fetchAndMergeBolusData(entries: entries, days: days, mainVC: mainVC)
  135. self.fetchAndMergeSMBData(entries: entries, days: days, mainVC: mainVC)
  136. self.fetchAndMergeCarbData(entries: entries, days: days, mainVC: mainVC)
  137. self.fetchAndMergeBasalData(entries: entries, days: days, mainVC: mainVC)
  138. completion()
  139. }
  140. } else {
  141. DispatchQueue.main.async {
  142. completion()
  143. }
  144. }
  145. case let .failure(error):
  146. LogManager.shared.log(category: .nightscout, message: "Failed to fetch stats treatments data: \(error.localizedDescription)")
  147. DispatchQueue.main.async {
  148. completion()
  149. }
  150. }
  151. }
  152. }
  153. private func fetchAndMergeBolusData(entries: [[String: AnyObject]], days: Int, mainVC: MainViewController) {
  154. let cutoffTime = dataService?.startDate.timeIntervalSince1970 ?? (Date().timeIntervalSince1970 - (Double(days) * 24 * 60 * 60))
  155. var bolusEntries: [[String: AnyObject]] = []
  156. for entry in entries {
  157. guard let eventType = entry["eventType"] as? String else { continue }
  158. if eventType == "Correction Bolus" || eventType == "Bolus" || eventType == "External Insulin" {
  159. if let automatic = entry["automatic"] as? Bool, automatic {
  160. continue
  161. }
  162. bolusEntries.append(entry)
  163. } else if eventType == "Meal Bolus" {
  164. bolusEntries.append(entry)
  165. }
  166. }
  167. mainVC.statsBolusData.removeAll { $0.date < cutoffTime }
  168. let existingDates = Set(mainVC.statsBolusData.map { Int($0.date) })
  169. var lastFoundIndex = 0
  170. for currentEntry in bolusEntries.reversed() {
  171. var bolusDate: String
  172. if currentEntry["timestamp"] != nil {
  173. bolusDate = currentEntry["timestamp"] as! String
  174. } else if currentEntry["created_at"] != nil {
  175. bolusDate = currentEntry["created_at"] as! String
  176. } else {
  177. continue
  178. }
  179. guard let parsedDate = NightscoutUtils.parseDate(bolusDate),
  180. let bolus = currentEntry["insulin"] as? Double else { continue }
  181. let dateTimeStamp = parsedDate.timeIntervalSince1970
  182. if dateTimeStamp < cutoffTime { continue }
  183. // Avoid duplicates (use Int to handle floating point precision)
  184. if existingDates.contains(Int(dateTimeStamp)) { continue }
  185. let sgv = mainVC.findNearestBGbyTime(needle: dateTimeStamp, haystack: mainVC.statsBGData, startingIndex: lastFoundIndex)
  186. lastFoundIndex = sgv.foundIndex
  187. let dot = MainViewController.bolusGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv + 20))
  188. mainVC.statsBolusData.append(dot)
  189. }
  190. mainVC.statsBolusData.sort { $0.date < $1.date }
  191. }
  192. private func fetchAndMergeSMBData(entries: [[String: AnyObject]], days: Int, mainVC: MainViewController) {
  193. let cutoffTime = dataService?.startDate.timeIntervalSince1970 ?? (Date().timeIntervalSince1970 - (Double(days) * 24 * 60 * 60))
  194. var smbEntries: [[String: AnyObject]] = []
  195. for entry in entries {
  196. guard let eventType = entry["eventType"] as? String else { continue }
  197. if eventType == "SMB" {
  198. smbEntries.append(entry)
  199. } else if eventType == "Correction Bolus" || eventType == "Bolus" || eventType == "External Insulin" {
  200. if let automatic = entry["automatic"] as? Bool, automatic {
  201. smbEntries.append(entry)
  202. }
  203. }
  204. }
  205. mainVC.statsSMBData.removeAll { $0.date < cutoffTime }
  206. let existingDates = Set(mainVC.statsSMBData.map { Int($0.date) })
  207. var lastFoundIndex = 0
  208. for currentEntry in smbEntries.reversed() {
  209. var bolusDate: String
  210. if currentEntry["timestamp"] != nil {
  211. bolusDate = currentEntry["timestamp"] as! String
  212. } else if currentEntry["created_at"] != nil {
  213. bolusDate = currentEntry["created_at"] as! String
  214. } else {
  215. continue
  216. }
  217. guard let parsedDate = NightscoutUtils.parseDate(bolusDate),
  218. let bolus = currentEntry["insulin"] as? Double else { continue }
  219. let dateTimeStamp = parsedDate.timeIntervalSince1970
  220. if dateTimeStamp < cutoffTime { continue }
  221. if existingDates.contains(Int(dateTimeStamp)) { continue }
  222. let sgv = mainVC.findNearestBGbyTime(needle: dateTimeStamp, haystack: mainVC.statsBGData, startingIndex: lastFoundIndex)
  223. lastFoundIndex = sgv.foundIndex
  224. let dot = MainViewController.bolusGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv + 20))
  225. mainVC.statsSMBData.append(dot)
  226. }
  227. mainVC.statsSMBData.sort { $0.date < $1.date }
  228. }
  229. private func fetchAndMergeCarbData(entries: [[String: AnyObject]], days: Int, mainVC: MainViewController) {
  230. let cutoffTime = dataService?.startDate.timeIntervalSince1970 ?? (Date().timeIntervalSince1970 - (Double(days) * 24 * 60 * 60))
  231. let now = dataService?.endDate.timeIntervalSince1970 ?? Date().timeIntervalSince1970
  232. var carbEntries: [[String: AnyObject]] = []
  233. for entry in entries {
  234. guard let eventType = entry["eventType"] as? String else { continue }
  235. if eventType == "Carb Correction" || eventType == "Meal Bolus" {
  236. carbEntries.append(entry)
  237. }
  238. }
  239. mainVC.statsCarbData.removeAll { $0.date < cutoffTime || $0.date > now }
  240. let existingDates = Set(mainVC.statsCarbData.map { Int($0.date) })
  241. var lastFoundIndex = 0
  242. var lastFoundBolus = 0
  243. for currentEntry in carbEntries.reversed() {
  244. var carbDate: String
  245. if currentEntry["timestamp"] != nil {
  246. carbDate = currentEntry["timestamp"] as! String
  247. } else if currentEntry["created_at"] != nil {
  248. carbDate = currentEntry["created_at"] as! String
  249. } else {
  250. continue
  251. }
  252. let absorptionTime = currentEntry["absorptionTime"] as? Int ?? 0
  253. guard let parsedDate = NightscoutUtils.parseDate(carbDate),
  254. let carbs = currentEntry["carbs"] as? Double else { continue }
  255. let dateTimeStamp = parsedDate.timeIntervalSince1970
  256. if dateTimeStamp < cutoffTime || dateTimeStamp > now { continue }
  257. if existingDates.contains(Int(dateTimeStamp)) { continue }
  258. let sgv = mainVC.findNearestBGbyTime(needle: dateTimeStamp, haystack: mainVC.statsBGData, startingIndex: lastFoundIndex)
  259. lastFoundIndex = sgv.foundIndex
  260. var offset = -50
  261. if sgv.sgv < Double(mainVC.calculateMaxBgGraphValue() - 100) {
  262. let bolusTime = mainVC.findNearestBolusbyTime(timeWithin: 300, needle: dateTimeStamp, haystack: mainVC.statsBolusData, startingIndex: lastFoundBolus)
  263. lastFoundBolus = bolusTime.foundIndex
  264. offset = bolusTime.offset ? 70 : 20
  265. }
  266. let dot = MainViewController.carbGraphStruct(value: Double(carbs), date: Double(dateTimeStamp), sgv: Int(sgv.sgv + Double(offset)), absorptionTime: absorptionTime)
  267. mainVC.statsCarbData.append(dot)
  268. }
  269. mainVC.statsCarbData.sort { $0.date < $1.date }
  270. }
  271. private func fetchAndMergeBasalData(entries: [[String: AnyObject]], days: Int, mainVC: MainViewController) {
  272. let cutoffTime = dataService?.startDate.timeIntervalSince1970 ?? (Date().timeIntervalSince1970 - (Double(days) * 24 * 60 * 60))
  273. let now = dataService?.endDate.timeIntervalSince1970 ?? Date().timeIntervalSince1970
  274. var basalEntries: [[String: AnyObject]] = []
  275. for entry in entries {
  276. guard let eventType = entry["eventType"] as? String else { continue }
  277. if eventType == "Temp Basal" {
  278. basalEntries.append(entry)
  279. }
  280. }
  281. mainVC.statsBasalData.removeAll { $0.date < cutoffTime }
  282. // Clear and rebuild temp basal entries for stats calculation
  283. dataService?.tempBasalEntries.removeAll { $0.startTime < cutoffTime }
  284. var existingTempBasalTimes = Set(dataService?.tempBasalEntries.map { Int($0.startTime) } ?? [])
  285. var existingDates = Set(mainVC.statsBasalData.map { Int($0.date) })
  286. var tempArray = basalEntries
  287. tempArray.reverse()
  288. for i in 0 ..< tempArray.count {
  289. guard let currentEntry = tempArray[i] as [String: AnyObject]? else { continue }
  290. let dateString = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String
  291. guard let rawDateStr = dateString,
  292. let dateParsed = NightscoutUtils.parseDate(rawDateStr)
  293. else {
  294. continue
  295. }
  296. let dateTimeStamp = dateParsed.timeIntervalSince1970
  297. if dateTimeStamp < cutoffTime { continue }
  298. guard let basalRate = currentEntry["absolute"] as? Double else {
  299. continue
  300. }
  301. var duration = currentEntry["duration"] as? Double ?? 0.0
  302. // Store raw temp basal entry for stats calculation
  303. // Adjust duration if it overlaps with next temp basal
  304. var effectiveDuration = duration
  305. if i < tempArray.count - 1 {
  306. let nextEntry = tempArray[i + 1] as [String: AnyObject]?
  307. let nextDateStr = nextEntry?["timestamp"] as? String ?? nextEntry?["created_at"] as? String
  308. if let rawNext = nextDateStr,
  309. let nextDateParsed = NightscoutUtils.parseDate(rawNext)
  310. {
  311. let nextDateTimeStamp = nextDateParsed.timeIntervalSince1970
  312. let tempBasalEnd = dateTimeStamp + (duration * 60)
  313. if nextDateTimeStamp < tempBasalEnd {
  314. // Adjust duration to end when next temp basal starts
  315. effectiveDuration = (nextDateTimeStamp - dateTimeStamp) / 60.0
  316. }
  317. }
  318. }
  319. if !existingTempBasalTimes.contains(Int(dateTimeStamp)) {
  320. let tempBasalEntry = StatsDataService.TempBasalEntry(
  321. rate: basalRate,
  322. startTime: dateTimeStamp,
  323. durationMinutes: effectiveDuration > 0 ? effectiveDuration : 30.0
  324. )
  325. dataService?.tempBasalEntries.append(tempBasalEntry)
  326. existingTempBasalTimes.insert(Int(dateTimeStamp))
  327. }
  328. if i > 0 {
  329. let priorEntry = tempArray[i - 1] as [String: AnyObject]?
  330. let priorDateStr = priorEntry?["timestamp"] as? String ?? priorEntry?["created_at"] as? String
  331. if let rawPrior = priorDateStr,
  332. let priorDateParsed = NightscoutUtils.parseDate(rawPrior)
  333. {
  334. let priorDateTimeStamp = priorDateParsed.timeIntervalSince1970
  335. let priorDuration = priorEntry?["duration"] as? Double ?? 0.0
  336. let priorEndTime = priorDateTimeStamp + (priorDuration * 60)
  337. if (dateTimeStamp - priorEndTime) > 15 {
  338. let gapEntries = createScheduledBasalEntriesForGap(
  339. from: priorEndTime,
  340. to: dateTimeStamp,
  341. profile: mainVC.basalProfile,
  342. existingDates: existingDates
  343. )
  344. for entry in gapEntries {
  345. if !existingDates.contains(Int(entry.date)) {
  346. mainVC.statsBasalData.append(entry)
  347. existingDates.insert(Int(entry.date))
  348. }
  349. }
  350. }
  351. }
  352. }
  353. let startDot = MainViewController.basalGraphStruct(basalRate: basalRate, date: dateTimeStamp)
  354. if !existingDates.contains(Int(startDot.date)) {
  355. mainVC.statsBasalData.append(startDot)
  356. existingDates.insert(Int(startDot.date))
  357. }
  358. var endTime = dateTimeStamp + (duration * 60)
  359. if i == tempArray.count - 1, duration == 0.0 {
  360. endTime = dateTimeStamp + (30 * 60)
  361. }
  362. if i < tempArray.count - 1 {
  363. let nextEntry = tempArray[i + 1] as [String: AnyObject]?
  364. let nextDateStr = nextEntry?["timestamp"] as? String ?? nextEntry?["created_at"] as? String
  365. if let rawNext = nextDateStr,
  366. let nextDateParsed = NightscoutUtils.parseDate(rawNext)
  367. {
  368. let nextDateTimeStamp = nextDateParsed.timeIntervalSince1970
  369. if nextDateTimeStamp < endTime {
  370. endTime = nextDateTimeStamp
  371. }
  372. }
  373. }
  374. let endDot = MainViewController.basalGraphStruct(basalRate: basalRate, date: endTime)
  375. if !existingDates.contains(Int(endDot.date)) {
  376. mainVC.statsBasalData.append(endDot)
  377. existingDates.insert(Int(endDot.date))
  378. }
  379. }
  380. if !tempArray.isEmpty {
  381. let firstEntry = tempArray.first as [String: AnyObject]?
  382. let firstDateStr = firstEntry?["timestamp"] as? String ?? firstEntry?["created_at"] as? String
  383. if let rawFirst = firstDateStr,
  384. let firstDateParsed = NightscoutUtils.parseDate(rawFirst)
  385. {
  386. let firstTempBasalStart = firstDateParsed.timeIntervalSince1970
  387. if firstTempBasalStart > cutoffTime {
  388. let gapEntries = createScheduledBasalEntriesForGap(
  389. from: cutoffTime,
  390. to: firstTempBasalStart,
  391. profile: mainVC.basalProfile,
  392. existingDates: existingDates
  393. )
  394. for entry in gapEntries {
  395. if !existingDates.contains(Int(entry.date)) {
  396. mainVC.statsBasalData.append(entry)
  397. existingDates.insert(Int(entry.date))
  398. }
  399. }
  400. }
  401. }
  402. } else if !mainVC.basalProfile.isEmpty {
  403. let gapEntries = createScheduledBasalEntriesForGap(
  404. from: cutoffTime,
  405. to: now,
  406. profile: mainVC.basalProfile,
  407. existingDates: existingDates
  408. )
  409. for entry in gapEntries {
  410. if !existingDates.contains(Int(entry.date)) {
  411. mainVC.statsBasalData.append(entry)
  412. existingDates.insert(Int(entry.date))
  413. }
  414. }
  415. }
  416. if !tempArray.isEmpty {
  417. let lastEntry = tempArray.last as [String: AnyObject]?
  418. let lastDateStr = lastEntry?["timestamp"] as? String ?? lastEntry?["created_at"] as? String
  419. let lastDuration = lastEntry?["duration"] as? Double ?? 30.0
  420. if let rawLast = lastDateStr,
  421. let lastDateParsed = NightscoutUtils.parseDate(rawLast)
  422. {
  423. let lastTempBasalEnd = lastDateParsed.timeIntervalSince1970 + (lastDuration * 60)
  424. if lastTempBasalEnd < now {
  425. let gapEntries = createScheduledBasalEntriesForGap(
  426. from: lastTempBasalEnd,
  427. to: now,
  428. profile: mainVC.basalProfile,
  429. existingDates: existingDates
  430. )
  431. for entry in gapEntries {
  432. if !existingDates.contains(Int(entry.date)) {
  433. mainVC.statsBasalData.append(entry)
  434. existingDates.insert(Int(entry.date))
  435. }
  436. }
  437. }
  438. }
  439. }
  440. mainVC.statsBasalData.sort { $0.date < $1.date }
  441. }
  442. private func createScheduledBasalEntriesForGap(
  443. from startTime: TimeInterval,
  444. to endTime: TimeInterval,
  445. profile: [MainViewController.basalProfileStruct],
  446. existingDates: Set<Int>
  447. ) -> [MainViewController.basalGraphStruct] {
  448. guard !profile.isEmpty, endTime > startTime else { return [] }
  449. var entries: [MainViewController.basalGraphStruct] = []
  450. let sortedProfile = profile.sorted { $0.timeAsSeconds < $1.timeAsSeconds }
  451. let calendar = dateTimeUtils.displayCalendar()
  452. var currentTime = startTime
  453. while currentTime < endTime {
  454. let currentDate = Date(timeIntervalSince1970: currentTime)
  455. let dayStart = calendar.startOfDay(for: currentDate).timeIntervalSince1970
  456. let nextDayStart = dayStart + 24 * 60 * 60
  457. for i in 0 ..< sortedProfile.count {
  458. let segmentRate = sortedProfile[i].value
  459. let segmentStartInDay = dayStart + sortedProfile[i].timeAsSeconds
  460. let segmentEndInDay: TimeInterval
  461. if i < sortedProfile.count - 1 {
  462. segmentEndInDay = dayStart + sortedProfile[i + 1].timeAsSeconds
  463. } else {
  464. segmentEndInDay = nextDayStart
  465. }
  466. let overlapStart = max(currentTime, segmentStartInDay)
  467. let overlapEnd = min(endTime, segmentEndInDay)
  468. if overlapEnd > overlapStart, !existingDates.contains(Int(overlapStart)) {
  469. let startEntry = MainViewController.basalGraphStruct(basalRate: segmentRate, date: overlapStart)
  470. entries.append(startEntry)
  471. if overlapEnd < endTime, !existingDates.contains(Int(overlapEnd)) {
  472. let endEntry = MainViewController.basalGraphStruct(basalRate: segmentRate, date: overlapEnd)
  473. entries.append(endEntry)
  474. }
  475. }
  476. }
  477. currentTime = nextDayStart
  478. }
  479. return entries
  480. }
  481. }