StatsDataFetcher.swift 25 KB

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