| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- import Foundation
- import Swinject
- protocol TDDStorage {
- func calculateTDD(pumpHistory: [PumpHistoryEvent], basalProfile: [BasalProfileEntry]) async -> TDDResult
- }
- /// Structure containing the results of TDD calculations
- struct TDDResult {
- let total: Decimal
- let bolus: Decimal
- let tempBasal: Decimal
- let scheduledBasal: Decimal
- let weightedAverage: Decimal?
- let hoursOfData: Double
- }
- /// Implementation of the TDD Calculator
- final class BaseTDDStorage: TDDStorage, Injectable {
- init(resolver: Resolver) {
- injectServices(resolver)
- }
- /// Main function to calculate TDD from pump history and basal profile
- /// - Parameters:
- /// - pumpHistory: Array of pump history events
- /// - basalProfile: Array of basal profile entries
- /// - Returns: TDDResult containing all calculated values
- func calculateTDD(pumpHistory: [PumpHistoryEvent], basalProfile: [BasalProfileEntry]) async -> TDDResult {
- debug(.apsManager, "Starting TDD calculation with \(pumpHistory.count) pump events")
- var bolusInsulin: Decimal = 0
- var tempInsulin: Decimal = 0
- var scheduledBasalInsulin: Decimal = 0
- let pumpData = calculatePumpDataHours(pumpHistory)
- debug(.apsManager, "Hours of pump data available: \(pumpData)")
- if pumpData < 23.9, pumpData > 21 {
- let missingHours = 24 - pumpData
- debug(.apsManager, "Filling \(missingHours) missing hours with scheduled basals")
- if let lastEntry = pumpHistory.last {
- let endDate = lastEntry.timestamp
- let endDateAdjusted = endDate.addingTimeInterval(-missingHours * 3600)
- scheduledBasalInsulin = calculateScheduledBasalInsulin(
- from: endDate,
- to: endDateAdjusted,
- basalProfile: basalProfile
- )
- debug(.apsManager, "Added scheduled basal insulin: \(scheduledBasalInsulin)U")
- }
- }
- bolusInsulin = calculateBolusInsulin(pumpHistory)
- debug(.apsManager, "Total bolus insulin: \(bolusInsulin)U")
- tempInsulin = calculateTempBasalInsulin(pumpHistory)
- debug(.apsManager, "Total temp basal insulin: \(tempInsulin)U")
- let total = bolusInsulin + tempInsulin + scheduledBasalInsulin
- let weightedAverage = calculateWeightedAverage()
- debug(.apsManager, """
- TDD Summary:
- - Total: \(total)U
- - Bolus: \(bolusInsulin)U (\((bolusInsulin / total * 100).rounded(toPlaces: 1))%)
- - Temp Basal: \(tempInsulin)U (\((tempInsulin / total * 100).rounded(toPlaces: 1))%)
- - Scheduled Basal: \(scheduledBasalInsulin)U (\((scheduledBasalInsulin / total * 100).rounded(toPlaces: 1))%)
- - weightedAverage: \(weightedAverage ?? 0)
- - Hours of Data: \(pumpData)
- """)
- return TDDResult(
- total: total,
- bolus: bolusInsulin,
- tempBasal: tempInsulin,
- scheduledBasal: scheduledBasalInsulin,
- weightedAverage: weightedAverage,
- hoursOfData: pumpData
- )
- }
- /// Calculates the number of hours of available pump history data
- /// - Parameter pumpHistory: Array of pump history events
- /// - Returns: Number of hours of available data
- private func calculatePumpDataHours(_ pumpHistory: [PumpHistoryEvent]) -> Double {
- guard let firstEvent = pumpHistory.last, // we are fetching in a descending order
- let lastEvent = pumpHistory.first
- else {
- return 0
- }
- let startDate = firstEvent.timestamp
- var endDate = lastEvent.timestamp
- // If last event is a temp basal, use current time
- if lastEvent.type == .tempBasalDuration {
- endDate = Date()
- }
- return Double(endDate.timeIntervalSince(startDate)) / 3600.0
- }
- /// Calculates total bolus insulin from pump history
- /// - Parameter pumpHistory: Array of pump history events
- /// - Returns: Total bolus insulin
- private func calculateBolusInsulin(_ pumpHistory: [PumpHistoryEvent]) -> Decimal {
- pumpHistory
- .filter { $0.type == .bolus }
- .reduce(Decimal(0)) { sum, event in
- sum + (event.amount ?? 0)
- }
- }
- /// Calculates insulin delivered via temporary basal rates
- /// - Parameter pumpHistory: Array of pump history events
- /// - Returns: Total temporary basal insulin
- private func calculateTempBasalInsulin(_ pumpHistory: [PumpHistoryEvent]) -> Decimal {
- var totalInsulin: Decimal = 0
- for (index, event) in pumpHistory.enumerated() {
- guard event.type == .tempBasal,
- let rate = event.amount,
- rate > 0,
- index > 0 else { continue }
- let duration = Decimal(pumpHistory[index - 1].duration ?? 0) / 60 // Convert to hours
- let insulin = accountForIncrements(rate * duration)
- totalInsulin += insulin
- debug(.apsManager, "Temp basal: \(rate)U/hr for \(duration)hr = \(insulin)U")
- }
- return totalInsulin
- }
- /// Calculates insulin delivered via scheduled basal rates
- /// - Parameters:
- /// - from: Start date
- /// - to: End date
- /// - basalProfile: Array of basal profile entries
- /// - Returns: Total scheduled basal insulin
- private func calculateScheduledBasalInsulin(from: Date, to: Date, basalProfile: [BasalProfileEntry]) -> Decimal {
- var totalInsulin: Decimal = 0
- var currentDate = from
- while currentDate < to {
- let timeString = makeBaseString(from: currentDate)
- guard let basalRate = findBasalRate(for: timeString, in: basalProfile) else { continue }
- let nextScheduleTime = findNextScheduleTime(after: timeString, in: basalProfile)
- let duration = calculateDuration(currentTime: timeString, nextScheduleTime: nextScheduleTime, endDate: to)
- let insulin = accountForIncrements(basalRate * Decimal(duration))
- totalInsulin += insulin
- currentDate = currentDate.addingTimeInterval(duration * 3600)
- }
- return totalInsulin
- }
- /// Rounds insulin amounts according to pump increment constraints
- /// - Parameter insulin: Raw insulin amount
- /// - Returns: Rounded insulin amount
- private func accountForIncrements(_ insulin: Decimal) -> Decimal {
- let minimalDose: Decimal = 0.05 // For Omnipod, 0.1 for other pumps
- let incrementsRaw = insulin / minimalDose
- if incrementsRaw >= 1 {
- // Convert to NSDecimalNumber to use its rounding capabilities
- let nsIncrements = NSDecimalNumber(decimal: incrementsRaw)
- let roundedIncrements = nsIncrements.rounding(
- accordingToBehavior:
- NSDecimalNumberHandler(
- roundingMode: .down,
- scale: 0,
- raiseOnExactness: false,
- raiseOnOverflow: false,
- raiseOnUnderflow: false,
- raiseOnDivideByZero: false
- )
- )
- return (roundedIncrements.decimalValue * minimalDose).rounded(toPlaces: 3)
- }
- return 0
- }
- /// Formats a date to time string in "HH:mm:ss" format
- /// - Parameter date: Date to format
- /// - Returns: Formatted time string
- private func makeBaseString(from date: Date) -> String {
- let formatter = DateFormatter()
- formatter.dateFormat = "HH:mm:ss"
- return formatter.string(from: date)
- }
- /// Finds the basal rate for a specific time in the profile
- /// - Parameters:
- /// - timeString: Time string in "HH:mm:ss" format
- /// - profile: Array of basal profile entries
- /// - Returns: Basal rate if found
- private func findBasalRate(for timeString: String, in profile: [BasalProfileEntry]) -> Decimal? {
- profile.first { $0.start == timeString }?.rate
- }
- /// Finds the next scheduled time in the basal profile
- /// - Parameters:
- /// - time: Current time string
- /// - profile: Array of basal profile entries
- /// - Returns: Next scheduled time
- private func findNextScheduleTime(after time: String, in profile: [BasalProfileEntry]) -> String {
- guard let currentIndex = profile.firstIndex(where: { $0.start == time }) else {
- return profile[0].start
- }
- let nextIndex = (currentIndex + 1) % profile.count
- return profile[nextIndex].start
- }
- /// Calculates duration between two schedule times
- /// - Parameters:
- /// - currentTime: Current time string
- /// - nextScheduleTime: Next schedule time string
- /// - endDate: End date for calculations
- /// - Returns: Duration in hours
- private func calculateDuration(currentTime: String, nextScheduleTime: String, endDate _: Date) -> Double {
- let formatter = DateFormatter()
- formatter.dateFormat = "HH:mm:ss"
- guard let time1 = formatter.date(from: currentTime),
- let time2 = formatter.date(from: nextScheduleTime)
- else {
- return 0
- }
- var difference = time2.timeIntervalSince(time1) / 3600
- if difference < 0 {
- difference += 24
- }
- return difference
- }
- /// Calculates weighted average of TDD from historical data
- /// - Returns: Weighted average if available
- private func calculateWeightedAverage() -> Decimal? {
- // Implementation of weighted average calculation
- // Would use historical TDD data from Core Data
- nil
- }
- }
- /// Extension for rounding Decimal numbers
- extension Decimal {
- /// Rounds a decimal to specified number of places
- /// - Parameter places: Number of decimal places
- /// - Returns: Rounded decimal
- func rounded(toPlaces places: Int) -> Decimal {
- var value = self
- var result = Decimal()
- NSDecimalRound(&result, &value, places, .plain)
- return result
- }
- }
|