Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix date parsing for Temp Basals using ISO8601DateFormatter #350

Merged
merged 3 commits into from
Jan 1, 2025
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 121 additions & 148 deletions LoopFollow/Controllers/Nightscout/Treatments/Basals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,200 +7,173 @@
//

import Foundation

extension MainViewController {

private func parseDate(_ rawString: String) -> Date? {
var mutableDate = rawString

if mutableDate.hasSuffix("Z") {
mutableDate = String(mutableDate.dropLast())
}
else if let offsetRange = mutableDate.range(of: "[\\+\\-]\\d{2}:\\d{2}$",
options: .regularExpression) {
mutableDate.removeSubrange(offsetRange)
}

mutableDate = mutableDate.replacingOccurrences(
of: "\\.\\d+",
with: "",
options: .regularExpression
)

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
dateFormatter.locale = Locale(identifier: "en_US")
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")

return dateFormatter.date(from: mutableDate)
}

// NS Temp Basal Response Processor
func processNSBasals(entries: [[String:AnyObject]]) {
infoManager.clearInfoData(type: .basal)

if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "Process: Basal") }
// due to temp basal durations, we're going to destroy the array and load everything each cycle for the time being.
basalData.removeAll()

var lastEndDot = 0.0

var tempArray = entries
tempArray.reverse()

for i in 0..<tempArray.count {
let currentEntry = tempArray[i] as [String : AnyObject]?
var basalDate: String
if currentEntry?["timestamp"] != nil {
basalDate = currentEntry?["timestamp"] as! String
} else if currentEntry?["created_at"] != nil {
basalDate = currentEntry?["created_at"] as! String
} else {
guard let currentEntry = tempArray[i] as [String : AnyObject]? else { continue }

// Decide which field to parse
let dateString = currentEntry["timestamp"] as? String
?? currentEntry["created_at"] as? String
guard let rawDateStr = dateString,
let dateParsed = parseDate(rawDateStr) else {
continue
}
var strippedZone = String(basalDate.dropLast())
strippedZone = strippedZone.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
dateFormatter.locale = Locale(identifier: "en_US")
dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
guard let dateString = dateFormatter.date(from: strippedZone) else { continue }
let dateTimeStamp = dateString.timeIntervalSince1970
guard let basalRate = currentEntry?["absolute"] as? Double else {
if UserDefaultsRepository.debugLog.value { self.writeDebugLog(value: "ERROR: Null Basal entry")}

let dateTimeStamp = dateParsed.timeIntervalSince1970
guard let basalRate = currentEntry["absolute"] as? Double else {
self.writeDebugLog(value: "ERROR: Null Basal entry")
continue
}

let midnightTime = dateTimeUtils.getTimeIntervalMidnightToday()
// Setting end dots
var duration = 0.0
if let durationValue = currentEntry?["duration"] as? Double {
duration = durationValue
} else {
print("No Duration Found")
}

// This adds scheduled basal wherever there is a break between temps. can't check the prior ending on the first item. it is 24 hours old, so it isn't important for display anyway
let duration = currentEntry["duration"] as? Double ?? 0.0

if i > 0 {
let priorEntry = tempArray[i - 1] as [String : AnyObject]?
var priorBasalDate: String
if priorEntry?["timestamp"] != nil {
priorBasalDate = priorEntry?["timestamp"] as! String
} else if currentEntry?["created_at"] != nil {
priorBasalDate = priorEntry?["created_at"] as! String
} else {
continue
}
var priorStrippedZone = String(priorBasalDate.dropLast())
priorStrippedZone = priorStrippedZone.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
let priorDateFormatter = DateFormatter()
priorDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
priorDateFormatter.locale = Locale(identifier: "en_US")
priorDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
guard let priorDateString = dateFormatter.date(from: priorStrippedZone) else { continue }
let priorDateTimeStamp = priorDateString.timeIntervalSince1970
let priorDuration = priorEntry?["duration"] as? Double ?? 0.0
// if difference between time stamps is greater than the duration of the last entry, there is a gap. Give a 15 second leeway on the timestamp
if Double( dateTimeStamp - priorDateTimeStamp ) > Double( (priorDuration * 60) + 15 ) {

var scheduled = 0.0
var midGap = false
var midGapTime: TimeInterval = 0
var midGapValue: Double = 0
// cycle through basal profiles.
// TODO figure out how to deal with profile changes that happen mid-gap
for b in 0..<self.basalScheduleData.count {

if (priorDateTimeStamp + (priorDuration * 60)) >= basalScheduleData[b].date {
scheduled = basalScheduleData[b].basalRate

// deal with mid-gap scheduled basal change
// don't do it on the last scheudled basal entry
if b < self.basalScheduleData.count - 1 {
if dateTimeStamp > self.basalScheduleData[b + 1].date {
// midGap = true
// TODO: finish this to handle mid-gap items without crashing from overlapping entries
midGapTime = self.basalScheduleData[b + 1].date
midGapValue = self.basalScheduleData[b + 1].basalRate
let priorDateStr = priorEntry?["timestamp"] as? String
?? priorEntry?["created_at"] as? String
if let rawPrior = priorDateStr,
let priorDateParsed = parseDate(rawPrior) {

let priorDateTimeStamp = priorDateParsed.timeIntervalSince1970
let priorDuration = priorEntry?["duration"] as? Double ?? 0.0

if (dateTimeStamp - priorDateTimeStamp) > (priorDuration * 60) + 15 {
var scheduled = 0.0
var midGap = false
var midGapTime: TimeInterval = 0
var midGapValue: Double = 0

for b in 0..<basalScheduleData.count {
let priorEnd = priorDateTimeStamp + (priorDuration * 60)
if priorEnd >= basalScheduleData[b].date {
scheduled = basalScheduleData[b].basalRate
if b < basalScheduleData.count - 1 {
if dateTimeStamp > basalScheduleData[b + 1].date {
midGap = true
midGapTime = basalScheduleData[b + 1].date
midGapValue = basalScheduleData[b + 1].basalRate
}
}
}

}

}

// Make the starting dot at the last ending dot
let startDot = basalGraphStruct(basalRate: scheduled, date: Double(priorDateTimeStamp + (priorDuration * 60)))
basalData.append(startDot)


if midGap {
// Make the ending dot at the new scheduled basal
let endDot1 = basalGraphStruct(basalRate: scheduled, date: Double(midGapTime))
basalData.append(endDot1)
// Make the starting dot at the scheduled Time
let startDot2 = basalGraphStruct(basalRate: midGapValue, date: Double(midGapTime))
basalData.append(startDot2)
// Make the ending dot at the new basal value
let endDot2 = basalGraphStruct(basalRate: midGapValue, date: Double(dateTimeStamp))
basalData.append(endDot2)

} else {
// Make the ending dot at the new starting dot
let endDot = basalGraphStruct(basalRate: scheduled, date: Double(dateTimeStamp))
basalData.append(endDot)

let startDot = basalGraphStruct(basalRate: scheduled,
date: priorDateTimeStamp + (priorDuration * 60))
basalData.append(startDot)

if midGap {
let endDot1 = basalGraphStruct(basalRate: scheduled, date: midGapTime)
basalData.append(endDot1)
let startDot2 = basalGraphStruct(basalRate: midGapValue, date: midGapTime)
basalData.append(startDot2)
let endDot2 = basalGraphStruct(basalRate: midGapValue, date: dateTimeStamp)
basalData.append(endDot2)
} else {
let endDot = basalGraphStruct(basalRate: scheduled, date: dateTimeStamp)
basalData.append(endDot)
}
}


}
}
// Make the starting dot
let startDot = basalGraphStruct(basalRate: basalRate, date: Double(dateTimeStamp))

// Start dot
let startDot = basalGraphStruct(basalRate: basalRate, date: dateTimeStamp)
basalData.append(startDot)

// Make the ending dot
// If it's the last one and has no duration, extend it for 30 minutes past the start. Otherwise set ending at duration
// duration is already set to 0 if there is no duration set on it.
//if i == tempArray.count - 1 && dateTimeStamp + duration <= dateTimeUtils.getNowTimeIntervalUTC() {
if i == tempArray.count - 1 && duration == 0.0 {
lastEndDot = dateTimeStamp + (30 * 60)
} else {
lastEndDot = dateTimeStamp + (duration * 60)

// End dot
var lastDot = dateTimeStamp + (duration * 60)
if i == tempArray.count - 1, duration == 0.0 {
lastDot = dateTimeStamp + (30 * 60)
}
latestBasal = Localizer.formatToLocalizedString(basalRate, maxFractionDigits: 2, minFractionDigits: 0)

// Double check for overlaps of incorrectly ended TBRs and sent it to end when the next one starts if it finds a discrepancy
// Overlap check
if i < tempArray.count - 1 {
let nextEntry = tempArray[i + 1] as [String : AnyObject]?
var nextBasalDate: String
if nextEntry?["timestamp"] != nil {
nextBasalDate = nextEntry?["timestamp"] as! String
} else if currentEntry?["created_at"] != nil {
nextBasalDate = nextEntry?["created_at"] as! String
} else {
continue
}
var nextStrippedZone = String(nextBasalDate.dropLast())
nextStrippedZone = nextStrippedZone.replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression)
let nextDateFormatter = DateFormatter()
nextDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
nextDateFormatter.locale = Locale(identifier: "en_US")
nextDateFormatter.timeZone = TimeZone(abbreviation: "UTC")
guard let nextDateString = dateFormatter.date(from: nextStrippedZone) else { continue }
let nextDateTimeStamp = nextDateString.timeIntervalSince1970
if nextDateTimeStamp < (dateTimeStamp + (duration * 60)) {
lastEndDot = nextDateTimeStamp
let nextDateStr = nextEntry?["timestamp"] as? String
?? nextEntry?["created_at"] as? String
if let rawNext = nextDateStr,
let nextDateParsed = parseDate(rawNext) {

let nextDateTimeStamp = nextDateParsed.timeIntervalSince1970
if nextDateTimeStamp < (dateTimeStamp + (duration * 60)) {
lastDot = nextDateTimeStamp
}
}
}
let endDot = basalGraphStruct(basalRate: basalRate, date: Double(lastEndDot))

let endDot = basalGraphStruct(basalRate: basalRate, date: lastDot)
basalData.append(endDot)


lastEndDot = lastDot
}
// If last basal was prior to right now, we need to create one last scheduled entry

// If last basal was prior to right now, we need to create one last scheduled entry
if lastEndDot <= dateTimeUtils.getNowTimeIntervalUTC() {
var scheduled = 0.0
// cycle through basal profiles.
// TODO figure out how to deal with profile changes that happen mid-gap
for b in 0..<self.basalProfile.count {
let scheduleTimeYesterday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightYesterday()
let scheduleTimeToday = self.basalProfile[b].timeAsSeconds + dateTimeUtils.getTimeIntervalMidnightToday()
// check the prior temp ending to the profile seconds from midnight
for b in 0..<basalProfile.count {
let scheduleTimeToday = basalProfile[b].timeAsSeconds
+ dateTimeUtils.getTimeIntervalMidnightToday()
if lastEndDot >= scheduleTimeToday {
scheduled = basalProfile[b].value
}
}

latestBasal = Localizer.formatToLocalizedString(scheduled, maxFractionDigits: 2, minFractionDigits: 0)
// Make the starting dot at the last ending dot
let startDot = basalGraphStruct(basalRate: scheduled, date: Double(lastEndDot))

latestBasal = Localizer.formatToLocalizedString(scheduled,
maxFractionDigits: 2,
minFractionDigits: 0)

let startDot = basalGraphStruct(basalRate: scheduled, date: lastEndDot)
basalData.append(startDot)
// Make the ending dot 10 minutes after now
let endDot = basalGraphStruct(basalRate: scheduled, date: Double(Date().timeIntervalSince1970 + (60 * 10)))

let endDot = basalGraphStruct(basalRate: scheduled,
date: Date().timeIntervalSince1970 + (60 * 10))
basalData.append(endDot)

}

if UserDefaultsRepository.graphBasal.value {
updateBasalGraph()
}

if let profileBasal = profileManager.currentBasal(), profileBasal != latestBasal {
if let profileBasal = profileManager.currentBasal(),
profileBasal != latestBasal {
latestBasal = "\(profileBasal) → \(latestBasal)"
}
infoManager.updateInfoData(type: .basal, value: latestBasal)
Expand Down