| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- import CoreData
- import Foundation
- extension Stat.StateModel {
- /// Initializes and fetches Total Daily Dose (TDD) statistics
- ///
- /// This function:
- /// 1. Fetches TDD determinations from CoreData
- /// 2. Maps the determinations to TDD records
- /// 3. Updates the tddStats array on the main thread
- func setupTDDs() {
- Task {
- let tddStats = await fetchAndMapDeterminations()
- await MainActor.run {
- self.tddStats = tddStats
- }
- }
- }
- /// Fetches and processes OpenAPS determinations to calculate Total Daily Doses
- /// - Returns: Array of TDD records sorted by date
- ///
- /// This function:
- /// 1. Fetches OpenAPS determinations from CoreData
- /// 2. Groups determinations by time period (day or hour based on selected duration)
- /// 3. Calculates average insulin doses for each time period
- ///
- /// The grouping logic:
- /// - For Day view: Groups by hour to show hourly distribution
- /// - For other views: Groups by day to show daily totals
- func fetchAndMapDeterminations() async -> [TDD] {
- let results = await CoreDataStack.shared.fetchEntitiesAsync(
- ofType: OrefDetermination.self,
- onContext: determinationFetchContext,
- predicate: NSPredicate.determinationsForStats,
- key: "deliverAt",
- ascending: true,
- propertiesToFetch: ["objectID", "timestamp", "deliverAt", "totalDailyDose"]
- )
- return await determinationFetchContext.perform {
- guard let fetchedResults = results as? [[String: Any]] else { return [] }
- let calendar = Calendar.current
- // Group determinations by day or hour
- let groupedByTime = Dictionary(grouping: fetchedResults) { result -> Date in
- guard let deliverAt = result["deliverAt"] as? Date else { return Date() }
- if self.selectedDurationForInsulinStats == .Day {
- // For Day view, group by hour
- let components = calendar.dateComponents([.year, .month, .day, .hour], from: deliverAt)
- return calendar.date(from: components) ?? Date()
- } else {
- // For other views, group by day
- return calendar.startOfDay(for: deliverAt)
- }
- }
- // Get all unique time points
- let timePoints = groupedByTime.keys.sorted()
- // Calculate totals for each time point
- return timePoints.map { timePoint in
- let determinations = groupedByTime[timePoint, default: []]
- let totalDose = determinations.reduce(Decimal.zero) { sum, determination in
- sum + (determination["totalDailyDose"] as? Decimal ?? 0)
- }
- // Calculate average dose for the time period
- let count = Decimal(determinations.count)
- let averageDose = count > 0 ? totalDose / count : 0
- return TDD(
- totalDailyDose: averageDose,
- timestamp: timePoint
- )
- }
- }
- }
- /// Calculates the average Total Daily Dose for the currently selected time period
- ///
- /// Time periods and their ranges:
- /// - Day: Last 3 days
- /// - Week: Last 7 days
- /// - Month: Last 30 days
- /// - Total: Last 3 months
- ///
- /// Returns 0 if no TDD records are available for the selected period
- var averageTDD: Decimal {
- let calendar = Calendar.current
- let now = Date()
- // Filter TDDs based on selected time frame
- let filteredTDDs: [TDD] = tddStats.filter { tdd in
- guard let timestamp = tdd.timestamp else { return false }
- switch selectedDurationForInsulinStats {
- case .Day:
- // Last 3 days
- let threeDaysAgo = calendar.date(byAdding: .day, value: -3, to: now)!
- return timestamp >= threeDaysAgo
- case .Week:
- // Last week
- let weekAgo = calendar.date(byAdding: .weekOfYear, value: -1, to: now)!
- return timestamp >= weekAgo
- case .Month:
- // Last month
- let monthAgo = calendar.date(byAdding: .month, value: -1, to: now)!
- return timestamp >= monthAgo
- case .Total:
- // Last 3 months
- let threeMonthsAgo = calendar.date(byAdding: .month, value: -3, to: now)!
- return timestamp >= threeMonthsAgo
- }
- }
- let sum = filteredTDDs.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
- return filteredTDDs.isEmpty ? 0 : sum / Decimal(filteredTDDs.count)
- }
- /// Calculates the average Total Daily Dose for a specified date range
- /// - Parameters:
- /// - startDate: Start date of the range
- /// - endDate: End date of the range
- /// - Returns: Average TDD value for the period
- ///
- /// The function:
- /// 1. Filters TDD records within the specified date range
- /// 2. Calculates the sum of all TDDs in the range
- /// 3. Returns the average (sum divided by number of records)
- /// 4. Returns 0 if no records are found
- func calculateAverageTDD(from startDate: Date, to endDate: Date) async -> Decimal {
- let filteredTDDs = tddStats.filter { tdd in
- guard let timestamp = tdd.timestamp else { return false }
- return timestamp >= startDate && timestamp <= endDate
- }
- let sum = filteredTDDs.reduce(Decimal.zero) { $0 + ($1.totalDailyDose ?? 0) }
- return filteredTDDs.isEmpty ? 0 : sum / Decimal(filteredTDDs.count)
- }
- /// Calculates the median Total Daily Dose for a specified date range
- /// - Parameters:
- /// - startDate: Start date of the range
- /// - endDate: End date of the range
- /// - Returns: Median TDD value for the period
- ///
- /// The calculation process:
- /// 1. Filters TDD records within the date range
- /// 2. Sorts all TDD values
- /// 3. For odd number of values: Returns the middle value
- /// 4. For even number of values: Returns average of two middle values
- /// 5. Returns 0 if no records are found
- func calculateMedianTDD(from startDate: Date, to endDate: Date) async -> Decimal {
- let filteredTDDs = tddStats.filter { tdd in
- guard let timestamp = tdd.timestamp else { return false }
- return timestamp >= startDate && timestamp <= endDate
- }
- let sortedDoses = filteredTDDs.compactMap(\.totalDailyDose).sorted()
- guard !sortedDoses.isEmpty else { return 0 }
- let middle = sortedDoses.count / 2
- if sortedDoses.count % 2 == 0 {
- return (sortedDoses[middle - 1] + sortedDoses[middle]) / 2
- } else {
- return sortedDoses[middle]
- }
- }
- }
|