diff --git a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift index 5819a987d..6d81e58b4 100644 --- a/FreeAPS/Sources/APS/Storage/CarbsStorage.swift +++ b/FreeAPS/Sources/APS/Storage/CarbsStorage.swift @@ -27,93 +27,53 @@ final class BaseCarbsStorage: CarbsStorage, Injectable { injectServices(resolver) } + /** + Processes and stores carbohydrate entries, including handling entries with fat and protein to calculate and distribute future carb equivalents. + + - The function processes fat and protein units (FPUs) by creating carb equivalents for future use. + - Ensures each carb equivalent is at least 1.0 grams by adjusting the interval if necessary. + - Stores the actual carbohydrate entries. + - Saves the data to CoreData for statistical purposes. + - Notifies observers of the carbohydrate data update. + + - Parameters: + - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed and stored. + */ func storeCarbs(_ entries: [CarbsEntry]) { processQueue.sync { let file = OpenAPS.Monitor.carbHistory - var uniqEvents: [CarbsEntry] = [] - - let fat = entries.last?.fat ?? 0 - let protein = entries.last?.protein ?? 0 - - if fat > 0 || protein > 0 { - // -------------------------- FPU-------------------------------------- - let interval = settings.settings.minuteInterval // Interval betwwen carbs - let timeCap = settings.settings.timeCap // Max Duration - let adjustment = settings.settings.individualAdjustmentFactor - let delay = settings.settings.delay // Tme before first future carb entry - let kcal = protein * 4 + fat * 9 - let carbEquivalents = (kcal / 10) * adjustment - let fpus = carbEquivalents / 10 - // Duration in hours used for extended boluses with Warsaw Method. Here used for total duration of the computed carbquivalents instead, excluding the configurable delay. - var computedDuration = 0 - switch fpus { - case ..<2: - computedDuration = 3 - case 2 ..< 3: - computedDuration = 4 - case 3 ..< 4: - computedDuration = 5 - default: - computedDuration = timeCap - } - // Size of each created carb equivalent if 60 minutes interval - var equivalent: Decimal = carbEquivalents / Decimal(computedDuration) - // Adjust for interval setting other than 60 minutes - equivalent /= Decimal(60 / interval) - // Round to 1 fraction digit - // equivalent = Decimal(round(Double(equivalent * 10) / 10)) - let roundedEquivalent: Double = round(Double(equivalent * 10)) / 10 - equivalent = Decimal(roundedEquivalent) - // Number of equivalents - var numberOfEquivalents = carbEquivalents / equivalent - // Only use delay in first loop - var firstIndex = true - // New date for each carb equivalent - var useDate = entries.last?.createdAt ?? Date() - // Group and Identify all FPUs together - let fpuID = UUID().uuidString - // Create an array of all future carb equivalents. - var futureCarbArray = [CarbsEntry]() - while carbEquivalents > 0, numberOfEquivalents > 0 { - if firstIndex { - useDate = useDate.addingTimeInterval(delay.minutes.timeInterval) - firstIndex = false - } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) } - - let eachCarbEntry = CarbsEntry( - id: UUID().uuidString, createdAt: useDate, - carbs: equivalent, fat: 0, protein: 0, note: nil, - enteredBy: CarbsEntry.manual, isFPU: true, - fpuID: fpuID - ) - futureCarbArray.append(eachCarbEntry) - numberOfEquivalents -= 1 - } - // Save the array + var entriesToStore: [CarbsEntry] = [] + + guard let lastEntry = entries.last else { return } + + if let fat = lastEntry.fat, let protein = lastEntry.protein, fat > 0 || protein > 0 { + let (futureCarbArray, carbEquivalents) = processFPU( + entries: entries, + fat: fat, + protein: protein, + createdAt: lastEntry.createdAt + ) if carbEquivalents > 0 { self.storage.transaction { storage in storage.append(futureCarbArray, to: file, uniqBy: \.id) - uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)? + entriesToStore = storage.retrieve(file, as: [CarbsEntry].self)? .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() } .sorted { $0.createdAt > $1.createdAt } ?? [] - storage.save(Array(uniqEvents), as: file) + storage.save(Array(entriesToStore), as: file) } } - } // ------------------------- END OF TPU ---------------------------------------- - // Store the actual (normal) carbs - if entries.last?.carbs ?? 0 > 0 { - uniqEvents = [] + } + + if lastEntry.carbs > 0 { self.storage.transaction { storage in storage.append(entries, to: file, uniqBy: \.createdAt) - uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)? + entriesToStore = storage.retrieve(file, as: [CarbsEntry].self)? .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() } .sorted { $0.createdAt > $1.createdAt } ?? [] - storage.save(Array(uniqEvents), as: file) + storage.save(Array(entriesToStore), as: file) } } - // MARK: Save to CoreData. TEST - var cbs: Decimal = 0 var carbDate = Date() if entries.isNotEmpty { @@ -131,11 +91,96 @@ final class BaseCarbsStorage: CarbsStorage, Injectable { } } broadcaster.notify(CarbsObserver.self, on: processQueue) { - $0.carbsDidUpdate(uniqEvents) + $0.carbsDidUpdate(entriesToStore) } } } + /** + Calculates the duration for processing FPUs (fat and protein units) based on the FPUs and the time cap. + + - The function uses predefined rules to determine the duration based on the number of FPUs. + - Ensures that the duration does not exceed the time cap. + + - Parameters: + - fpus: The number of FPUs calculated from fat and protein. + - timeCap: The maximum allowed duration. + + - Returns: The computed duration in hours. + */ + private func calculateComputedDuration(fpus: Decimal, timeCap: Int) -> Int { + switch fpus { + case ..<2: + return 3 + case 2 ..< 3: + return 4 + case 3 ..< 4: + return 5 + default: + return timeCap + } + } + + /** + Processes fat and protein entries to generate future carb equivalents, ensuring each equivalent is at least 1.0 grams. + + - The function calculates the equivalent carb dosage size and adjusts the interval to ensure each equivalent is at least 1.0 grams. + - Creates future carb entries based on the adjusted carb equivalent size and interval. + + - Parameters: + - entries: An array of `CarbsEntry` objects representing the carbohydrate entries to be processed. + - fat: The amount of fat in the last entry. + - protein: The amount of protein in the last entry. + - createdAt: The creation date of the last entry. + + - Returns: A tuple containing the array of future carb entries and the total carb equivalents. + */ + private func processFPU(entries _: [CarbsEntry], fat: Decimal, protein: Decimal, createdAt: Date) -> ([CarbsEntry], Decimal) { + let interval = settings.settings.minuteInterval + let timeCap = settings.settings.timeCap + let adjustment = settings.settings.individualAdjustmentFactor + let delay = settings.settings.delay + + let kcal = protein * 4 + fat * 9 + let carbEquivalents = (kcal / 10) * adjustment + let fpus = carbEquivalents / 10 + var computedDuration = calculateComputedDuration(fpus: fpus, timeCap: timeCap) + + var carbEquivalentSize: Decimal = carbEquivalents / Decimal(computedDuration) + carbEquivalentSize /= Decimal(60 / interval) + + if carbEquivalentSize < 1.0 { + carbEquivalentSize = 1.0 + computedDuration = Int(carbEquivalents / carbEquivalentSize) + } + + let roundedEquivalent: Double = round(Double(carbEquivalentSize * 10)) / 10 + carbEquivalentSize = Decimal(roundedEquivalent) + var numberOfEquivalents = carbEquivalents / carbEquivalentSize + + var useDate = createdAt + let fpuID = UUID().uuidString + var futureCarbArray = [CarbsEntry]() + var firstIndex = true + + while carbEquivalents > 0, numberOfEquivalents > 0 { + useDate = firstIndex ? useDate.addingTimeInterval(delay.minutes.timeInterval) : useDate + .addingTimeInterval(interval.minutes.timeInterval) + firstIndex = false + + let eachCarbEntry = CarbsEntry( + id: UUID().uuidString, createdAt: useDate, + carbs: carbEquivalentSize, fat: 0, protein: 0, note: nil, + enteredBy: CarbsEntry.manual, isFPU: true, + fpuID: fpuID + ) + futureCarbArray.append(eachCarbEntry) + numberOfEquivalents -= 1 + } + + return (futureCarbArray, carbEquivalents) + } + func syncDate() -> Date { Date().addingTimeInterval(-1.days.timeInterval) }