Skip to content

Commit

Permalink
Merge pull request #350 from loopandlearn/basal-rewrite
Browse files Browse the repository at this point in the history
Fix date parsing for Temp Basals using ISO8601DateFormatter
  • Loading branch information
marionbarker authored Jan 1, 2025
2 parents e57a3ba + 38121e9 commit d676af5
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 165 deletions.
244 changes: 96 additions & 148 deletions LoopFollow/Controllers/Nightscout/Treatments/Basals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,200 +7,148 @@
//

import Foundation

extension MainViewController {

// 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 = NightscoutUtils.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 = NightscoutUtils.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 = NightscoutUtils.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
1 change: 0 additions & 1 deletion LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ extension MainViewController {
}

guard let date = NightscoutUtils.parseDate(carbDate) else {
print("Unable to parse date from: \(carbDate)")
continue
}

Expand Down
41 changes: 25 additions & 16 deletions LoopFollow/Helpers/NightscoutUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,24 +234,33 @@ class NightscoutUtils {
task.resume()
}

static func parseDate(_ dateString: String) -> Date? {
let dateFormatterWithMilliseconds = DateFormatter()
dateFormatterWithMilliseconds.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
dateFormatterWithMilliseconds.timeZone = TimeZone(abbreviation: "UTC")
dateFormatterWithMilliseconds.locale = Locale(identifier: "en_US_POSIX")

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

if let date = dateFormatterWithMilliseconds.date(from: dateString) {
return date
} else if let date = dateFormatterWithoutMilliseconds.date(from: dateString) {
return date
static 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)
}

return nil
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")

let result = dateFormatter.date(from: mutableDate)
if result == nil {
print("Unable to parse string: '\(mutableDate)'")
}
return result
}

static func retrieveJWTToken() async throws -> String {
Expand Down

0 comments on commit d676af5

Please sign in to comment.