| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- //
- // UnfinalizedDose.swift
- // OmniKit
- //
- // Created by Pete Schwamb on 9/5/18.
- // Copyright © 2018 Pete Schwamb. All rights reserved.
- //
- import Foundation
- import LoopKit
- public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConvertible {
- public typealias RawValue = [String: Any]
- enum DoseType: Int {
- case bolus = 0
- case tempBasal
- case suspend
- case resume
- }
-
- enum ScheduledCertainty: Int {
- case certain = 0
- case uncertain
-
- public var localizedDescription: String {
- switch self {
- case .certain:
- return LocalizedString("Certain", comment: "String describing a dose that was certainly scheduled")
- case .uncertain:
- return LocalizedString("Uncertain", comment: "String describing a dose that was possibly scheduled")
- }
- }
- }
-
- private let insulinFormatter: NumberFormatter = {
- let formatter = NumberFormatter()
- formatter.numberStyle = .decimal
- formatter.maximumFractionDigits = 3
- return formatter
- }()
-
- private let shortDateFormatter: DateFormatter = {
- let timeFormatter = DateFormatter()
- timeFormatter.dateStyle = .short
- timeFormatter.timeStyle = .medium
- return timeFormatter
- }()
-
- private let dateFormatter = ISO8601DateFormatter()
-
- fileprivate var uniqueKey: Data {
- return "\(doseType) \(scheduledUnits ?? units) \(dateFormatter.string(from: startTime))".data(using: .utf8)!
- }
-
- let doseType: DoseType
- public var units: Double
- var scheduledUnits: Double? // Tracks the scheduled units, as boluses may be canceled before finishing, at which point units would reflect actual delivered volume.
- var scheduledTempRate: Double? // Tracks the original temp rate, as during finalization the units are discretized to pump pulses, changing the actual rate
- let startTime: Date
- var duration: TimeInterval?
- var scheduledCertainty: ScheduledCertainty
- var insulinType: InsulinType?
- var automatic: Bool?
-
- var finishTime: Date? {
- get {
- return duration != nil ? startTime.addingTimeInterval(duration!) : nil
- }
- set {
- duration = newValue?.timeIntervalSince(startTime)
- }
- }
-
- private var nominalProgress: Double {
- guard let duration = duration else {
- return 0
- }
- let elapsed = -startTime.timeIntervalSinceNow
- return elapsed / duration
- }
- // A value from 0 to 1 giving the nominal progress percentage for a bolus or a temp basal
- public var progress: Double {
- return min(nominalProgress, 1)
- }
-
- // Is a bolus or a temp basal nominally finished
- public var isFinished: Bool {
- return progress >= 1
- }
- // Has a bolus operation had enough time to positively finish
- public var isBolusPositivelyFinished: Bool {
- // Pod faults if any pulse takes 20% or more of nominal to deliver
- return nominalProgress >= 1.2
- }
-
- // Units per hour
- public var rate: Double {
- guard let duration = duration else {
- return 0
- }
- return units / duration.hours
- }
- public var finalizedUnits: Double? {
- guard isFinished else {
- return nil
- }
- return units
- }
- init(bolusAmount: Double, startTime: Date, scheduledCertainty: ScheduledCertainty, insulinType: InsulinType, automatic: Bool?) {
- self.doseType = .bolus
- self.units = bolusAmount
- self.startTime = startTime
- self.duration = TimeInterval(bolusAmount / Pod.bolusDeliveryRate)
- self.scheduledCertainty = scheduledCertainty
- self.scheduledUnits = nil
- self.insulinType = insulinType
- self.automatic = automatic
- }
-
- init(tempBasalRate: Double, startTime: Date, duration: TimeInterval, scheduledCertainty: ScheduledCertainty, insulinType: InsulinType) {
- self.doseType = .tempBasal
- self.units = tempBasalRate * duration.hours
- self.startTime = startTime
- self.duration = duration
- self.scheduledCertainty = scheduledCertainty
- self.scheduledUnits = nil
- self.insulinType = insulinType
- self.automatic = nil
- }
- init(suspendStartTime: Date, scheduledCertainty: ScheduledCertainty) {
- self.doseType = .suspend
- self.units = 0
- self.startTime = suspendStartTime
- self.scheduledCertainty = scheduledCertainty
- self.automatic = nil
- }
- init(resumeStartTime: Date, scheduledCertainty: ScheduledCertainty, insulinType: InsulinType) {
- self.doseType = .resume
- self.units = 0
- self.startTime = resumeStartTime
- self.scheduledCertainty = scheduledCertainty
- self.insulinType = insulinType
- self.automatic = nil
- }
- public mutating func cancel(at date: Date, withRemaining remaining: Double? = nil) {
- guard let finishTime = finishTime, date < finishTime else {
- return
- }
- scheduledUnits = units
- let newDuration = date.timeIntervalSince(startTime)
- switch doseType {
- case .bolus:
- let oldRate = rate
- if let remaining = remaining {
- units = units - remaining
- } else {
- units = oldRate * newDuration.hours
- }
- case .tempBasal:
- scheduledTempRate = rate
- units = floor(rate * newDuration.hours * Pod.pulsesPerUnit) / Pod.pulsesPerUnit
- print("Temp basal scheduled units: \(String(describing: scheduledUnits)), delivered units: \(units), duration: \(newDuration.minutes)")
- default:
- break
- }
- duration = newDuration
- }
- public var isMutable: Bool {
- switch doseType {
- case .bolus, .tempBasal:
- return !isFinished
- default:
- return false
- }
- }
- public var description: String {
- let unitsStr = insulinFormatter.string(from: units) ?? ""
- let startTimeStr = shortDateFormatter.string(from: startTime)
- let durationStr = duration?.format(using: [.minute, .second]) ?? ""
- switch doseType {
- case .bolus:
- if let scheduledUnits = scheduledUnits {
- let scheduledUnitsStr = insulinFormatter.string(from: scheduledUnits) ?? "?"
- return String(format: LocalizedString("InterruptedBolus: %1$@ U (%2$@ U scheduled) %3$@ %4$@ %5$@", comment: "The format string describing a bolus that was interrupted. (1: The amount delivered)(2: The amount scheduled)(3: Start time of the dose)(4: duration)(5: scheduled certainty)"), unitsStr, scheduledUnitsStr, startTimeStr, durationStr, scheduledCertainty.localizedDescription)
- } else {
- return String(format: LocalizedString("Bolus: %1$@U %2$@ %3$@ %4$@", comment: "The format string describing a bolus. (1: The amount delivered)(2: Start time of the dose)(3: duration)(4: scheduled certainty)"), unitsStr, startTimeStr, durationStr, scheduledCertainty.localizedDescription)
- }
- case .tempBasal:
- let volumeStr = insulinFormatter.string(from: units) ?? "?"
- let rateStr = NumberFormatter.localizedString(from: NSNumber(value: scheduledTempRate ?? rate), number: .decimal)
- return String(format: LocalizedString("TempBasal: %1$@ U/hour %2$@ %3$@ %4$@ U %5$@", comment: "The format string describing a temp basal. (1: The rate)(2: Start time)(3: duration)(4: volume)(5: scheduled certainty"), rateStr, startTimeStr, durationStr, volumeStr, scheduledCertainty.localizedDescription)
- case .suspend:
- return String(format: LocalizedString("Suspend: %1$@ %2$@", comment: "The format string describing a suspend. (1: Time)(2: Scheduled certainty"), startTimeStr, scheduledCertainty.localizedDescription)
- case .resume:
- return String(format: LocalizedString("Resume: %1$@ %2$@", comment: "The format string describing a resume. (1: Time)(2: Scheduled certainty"), startTimeStr, scheduledCertainty.localizedDescription)
- }
- }
-
- // RawRepresentable
- public init?(rawValue: RawValue) {
- guard
- let rawDoseType = rawValue["doseType"] as? Int,
- let doseType = DoseType(rawValue: rawDoseType),
- let units = rawValue["units"] as? Double,
- let startTime = rawValue["startTime"] as? Date,
- let rawScheduledCertainty = rawValue["scheduledCertainty"] as? Int,
- let scheduledCertainty = ScheduledCertainty(rawValue: rawScheduledCertainty)
- else {
- return nil
- }
-
- self.doseType = doseType
- self.units = units
- self.startTime = startTime
- self.scheduledCertainty = scheduledCertainty
-
- if let scheduledUnits = rawValue["scheduledUnits"] as? Double {
- self.scheduledUnits = scheduledUnits
- }
- if let scheduledTempRate = rawValue["scheduledTempRate"] as? Double {
- self.scheduledTempRate = scheduledTempRate
- }
- if let duration = rawValue["duration"] as? Double {
- self.duration = duration
- }
-
- if let rawInsulinType = rawValue["insulinType"] as? InsulinType.RawValue {
- self.insulinType = InsulinType(rawValue: rawInsulinType)
- }
- }
-
- public var rawValue: RawValue {
- var rawValue: RawValue = [
- "doseType": doseType.rawValue,
- "units": units,
- "startTime": startTime,
- "scheduledCertainty": scheduledCertainty.rawValue
- ]
-
- if let scheduledUnits = scheduledUnits {
- rawValue["scheduledUnits"] = scheduledUnits
- }
- if let scheduledTempRate = scheduledTempRate {
- rawValue["scheduledTempRate"] = scheduledTempRate
- }
- if let duration = duration {
- rawValue["duration"] = duration
- }
-
- if let insulinType = insulinType {
- rawValue["insulinType"] = insulinType.rawValue
- }
-
- return rawValue
- }
- }
- private extension TimeInterval {
- func format(using units: NSCalendar.Unit) -> String? {
- let formatter = DateComponentsFormatter()
- formatter.allowedUnits = units
- formatter.unitsStyle = .full
- formatter.zeroFormattingBehavior = .dropLeading
- formatter.maximumUnitCount = 2
-
- return formatter.string(from: self)
- }
- }
- extension NewPumpEvent {
- init(_ dose: UnfinalizedDose) {
- let title = String(describing: dose)
- let entry = DoseEntry(dose)
- self.init(date: dose.startTime, dose: entry, isMutable: dose.isMutable, raw: dose.uniqueKey, title: title)
- }
- }
- extension DoseEntry {
- init (_ dose: UnfinalizedDose) {
- switch dose.doseType {
- case .bolus:
- self = DoseEntry(type: .bolus, startDate: dose.startTime, endDate: dose.finishTime, value: dose.scheduledUnits ?? dose.units, unit: .units, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType, automatic: dose.automatic)
- case .tempBasal:
- self = DoseEntry(type: .tempBasal, startDate: dose.startTime, endDate: dose.finishTime, value: dose.scheduledTempRate ?? dose.rate, unit: .unitsPerHour, deliveredUnits: dose.finalizedUnits, insulinType: dose.insulinType)
- case .suspend:
- self = DoseEntry(suspendDate: dose.startTime)
- case .resume:
- self = DoseEntry(resumeDate: dose.startTime, insulinType: dose.insulinType)
- }
- }
- }
|