|
|
@@ -110,7 +110,6 @@ extension Treatments {
|
|
|
var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
|
|
|
var predictionsForChart: Predictions?
|
|
|
var simulatedDetermination: Determination?
|
|
|
- @MainActor var determinationObjectIDs: [NSManagedObjectID] = []
|
|
|
|
|
|
var minForecast: [Int] = []
|
|
|
var maxForecast: [Int] = []
|
|
|
@@ -128,9 +127,61 @@ extension Treatments {
|
|
|
var showDeterminationFailureAlert = false
|
|
|
var determinationFailureMessage = ""
|
|
|
|
|
|
- // Queue for handling Core Data change notifications
|
|
|
- private let queue = DispatchQueue(label: "TreatmentsStateModel.queue", qos: .userInitiated)
|
|
|
- private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
|
|
|
+ // MARK: - NSFetchedResultsControllers
|
|
|
+
|
|
|
+ //
|
|
|
+ // Glucose, the latest determination and the last pump bolus are driven by
|
|
|
+ // NSFetchedResultsControllers bound to the viewContext. They keep their `fetchedObjects`
|
|
|
+ // continuously in sync and notify us through their delegate's `onContentChange` closure.
|
|
|
+
|
|
|
+ @ObservationIgnored let glucoseControllerDelegate = FetchedResultsControllerDelegate()
|
|
|
+ @ObservationIgnored private(set) lazy var glucoseController: NSFetchedResultsController<GlucoseStored> = {
|
|
|
+ let request = NSFetchRequest<GlucoseStored>(entityName: "GlucoseStored")
|
|
|
+ request.sortDescriptors = [NSSortDescriptor(keyPath: \GlucoseStored.date, ascending: false)]
|
|
|
+ request.predicate = NSPredicate.glucose
|
|
|
+ request.fetchBatchSize = 50
|
|
|
+ let controller = NSFetchedResultsController(
|
|
|
+ fetchRequest: request,
|
|
|
+ managedObjectContext: viewContext,
|
|
|
+ sectionNameKeyPath: nil,
|
|
|
+ cacheName: nil
|
|
|
+ )
|
|
|
+ controller.delegate = glucoseControllerDelegate
|
|
|
+ return controller
|
|
|
+ }()
|
|
|
+
|
|
|
+ @ObservationIgnored let determinationControllerDelegate = FetchedResultsControllerDelegate()
|
|
|
+ @ObservationIgnored private(set) lazy var determinationController: NSFetchedResultsController<OrefDetermination> = {
|
|
|
+ let request = NSFetchRequest<OrefDetermination>(entityName: "OrefDetermination")
|
|
|
+ request.sortDescriptors = [NSSortDescriptor(keyPath: \OrefDetermination.deliverAt, ascending: false)]
|
|
|
+ request.predicate = NSPredicate.predicateFor30MinAgoForDetermination
|
|
|
+ request.fetchLimit = 1
|
|
|
+ let controller = NSFetchedResultsController(
|
|
|
+ fetchRequest: request,
|
|
|
+ managedObjectContext: viewContext,
|
|
|
+ sectionNameKeyPath: nil,
|
|
|
+ cacheName: nil
|
|
|
+ )
|
|
|
+ controller.delegate = determinationControllerDelegate
|
|
|
+ return controller
|
|
|
+ }()
|
|
|
+
|
|
|
+ @ObservationIgnored let lastBolusControllerDelegate = FetchedResultsControllerDelegate()
|
|
|
+ @ObservationIgnored private(set) lazy var lastBolusController: NSFetchedResultsController<PumpEventStored> = {
|
|
|
+ let request = NSFetchRequest<PumpEventStored>(entityName: "PumpEventStored")
|
|
|
+ request.sortDescriptors = [NSSortDescriptor(keyPath: \PumpEventStored.timestamp, ascending: false)]
|
|
|
+ request.predicate = NSPredicate.lastPumpBolus
|
|
|
+ request.fetchLimit = 1
|
|
|
+ let controller = NSFetchedResultsController(
|
|
|
+ fetchRequest: request,
|
|
|
+ managedObjectContext: viewContext,
|
|
|
+ sectionNameKeyPath: nil,
|
|
|
+ cacheName: nil
|
|
|
+ )
|
|
|
+ controller.delegate = lastBolusControllerDelegate
|
|
|
+ return controller
|
|
|
+ }()
|
|
|
+
|
|
|
private var subscriptions = Set<AnyCancellable>()
|
|
|
|
|
|
typealias PumpEvent = PumpEventStored.EventType
|
|
|
@@ -150,13 +201,6 @@ extension Treatments {
|
|
|
}
|
|
|
|
|
|
debug(.bolusState, "subscribe fired")
|
|
|
- coreDataPublisher =
|
|
|
- changedObjectsOnManagedObjectContextDidSavePublisher()
|
|
|
- .receive(on: queue)
|
|
|
- .share()
|
|
|
- .eraseToAnyPublisher()
|
|
|
- registerHandlers()
|
|
|
- registerSubscribers()
|
|
|
setupBolusStateConcurrently()
|
|
|
subscribeToBolusProgress()
|
|
|
}
|
|
|
@@ -183,13 +227,12 @@ extension Treatments {
|
|
|
private func setupBolusStateConcurrently() {
|
|
|
debug(.bolusState, "Setting up bolus state concurrently...")
|
|
|
Task {
|
|
|
+ // Load settings and observers first so the determination controller's initial
|
|
|
+ // population (which runs calculateInsulin) sees correct values.
|
|
|
do {
|
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
|
group.addTask {
|
|
|
- self.setupGlucoseArray()
|
|
|
- }
|
|
|
- group.addTask {
|
|
|
- self.setupDeterminationsAndForecasts()
|
|
|
+ await self.getAllSettingsValues()
|
|
|
}
|
|
|
group.addTask {
|
|
|
await self.setupSettings()
|
|
|
@@ -197,9 +240,6 @@ extension Treatments {
|
|
|
group.addTask {
|
|
|
self.registerObservers()
|
|
|
}
|
|
|
- group.addTask {
|
|
|
- self.setupLastBolus()
|
|
|
- }
|
|
|
|
|
|
// Wait for all tasks to complete
|
|
|
try await group.waitForAll()
|
|
|
@@ -207,6 +247,11 @@ extension Treatments {
|
|
|
} catch let error as NSError {
|
|
|
debug(.default, "Failed to setup bolus state concurrently: \(error)")
|
|
|
}
|
|
|
+
|
|
|
+ // NSFetchedResultsControllers are bound to the viewContext, so set them up on the main actor.
|
|
|
+ await self.setupGlucoseController()
|
|
|
+ await self.setupDeterminationController()
|
|
|
+ await self.setupLastBolusController()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -266,20 +311,6 @@ extension Treatments {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func setupDeterminationsAndForecasts() {
|
|
|
- Task {
|
|
|
- async let getAllSettingsDefaults: () = getAllSettingsValues()
|
|
|
- async let setupDeterminations: () = setupDeterminationsArray()
|
|
|
-
|
|
|
- await getAllSettingsDefaults
|
|
|
- await setupDeterminations
|
|
|
-
|
|
|
- // Determination has updated, so we can use this to draw the initial Forecast Chart
|
|
|
- let forecastData = await mapForecastsForChart()
|
|
|
- await updateForecasts(with: forecastData)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
private func registerObservers() {
|
|
|
broadcaster.register(DeterminationObserver.self, observer: self)
|
|
|
broadcaster.register(BolusFailureObserver.self, observer: self)
|
|
|
@@ -729,82 +760,29 @@ extension Treatments.StateModel: DeterminationObserver, BolusFailureObserver {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-extension Treatments.StateModel {
|
|
|
- private func registerHandlers() {
|
|
|
- coreDataPublisher?.filteredByEntityName("OrefDetermination").sink { [weak self] _ in
|
|
|
- guard let self = self else { return }
|
|
|
- Task {
|
|
|
- await self.setupDeterminationsArray()
|
|
|
- let forecastData = await self.mapForecastsForChart()
|
|
|
- await self.updateForecasts(with: forecastData)
|
|
|
- }
|
|
|
- }.store(in: &subscriptions)
|
|
|
-
|
|
|
- // Due to the Batch insert this only is used for observing Deletion of Glucose entries
|
|
|
- coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
|
|
|
- guard let self = self else { return }
|
|
|
- self.setupGlucoseArray()
|
|
|
- }.store(in: &subscriptions)
|
|
|
-
|
|
|
- // Refresh `lastPumpBolus` whenever a new pump event lands (mirrors HomeStateModel)
|
|
|
- coreDataPublisher?.filteredByEntityName("PumpEventStored").sink { [weak self] _ in
|
|
|
- self?.setupLastBolus()
|
|
|
- }.store(in: &subscriptions)
|
|
|
- }
|
|
|
-
|
|
|
- private func registerSubscribers() {
|
|
|
- glucoseStorage.updatePublisher
|
|
|
- .receive(on: DispatchQueue.global(qos: .background))
|
|
|
- .sink { [weak self] _ in
|
|
|
- guard let self = self else { return }
|
|
|
- self.setupGlucoseArray()
|
|
|
- }
|
|
|
- .store(in: &subscriptions)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// MARK: - Setup Glucose and Determinations
|
|
|
+// MARK: - Setup Glucose, Determinations and Last Bolus
|
|
|
|
|
|
extension Treatments.StateModel {
|
|
|
- // Glucose
|
|
|
- private func setupGlucoseArray() {
|
|
|
- Task {
|
|
|
- do {
|
|
|
- let ids = try await self.fetchGlucose()
|
|
|
- let glucoseObjects: [GlucoseStored] = try await CoreDataStack.shared
|
|
|
- .getNSManagedObject(with: ids, context: viewContext)
|
|
|
- await updateGlucoseArray(with: glucoseObjects)
|
|
|
- } catch {
|
|
|
- debug(
|
|
|
- .default,
|
|
|
- "\(DebuggingIdentifiers.failed) Error setting up glucose array: \(error)"
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private func fetchGlucose() async throws -> [NSManagedObjectID] {
|
|
|
- let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
|
|
|
- glucoseFetchContext.name = "TreatmentsStateModel.fetchGlucose"
|
|
|
+ // MARK: - Glucose Controller
|
|
|
|
|
|
- let results = try await CoreDataStack.shared.fetchEntitiesAsync(
|
|
|
- ofType: GlucoseStored.self,
|
|
|
- onContext: glucoseFetchContext,
|
|
|
- predicate: NSPredicate.glucose,
|
|
|
- key: "date",
|
|
|
- ascending: false
|
|
|
- )
|
|
|
-
|
|
|
- return try await glucoseFetchContext.perform {
|
|
|
- guard let fetchedResults = results as? [GlucoseStored] else {
|
|
|
- throw CoreDataError.fetchError(function: #function, file: #file)
|
|
|
+ @MainActor func setupGlucoseController() {
|
|
|
+ glucoseControllerDelegate.onContentChange = { [weak self] in
|
|
|
+ Task { @MainActor in
|
|
|
+ self?.updateGlucoseFromController()
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- return fetchedResults.map(\.objectID)
|
|
|
+ do {
|
|
|
+ try glucoseController.performFetch()
|
|
|
+ updateGlucoseFromController()
|
|
|
+ } catch {
|
|
|
+ debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform glucose fetch: \(error)")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- @MainActor private func updateGlucoseArray(with objects: [GlucoseStored]) {
|
|
|
+ @MainActor private func updateGlucoseFromController() {
|
|
|
+ guard let objects = glucoseController.fetchedObjects else { return }
|
|
|
+
|
|
|
// Store all objects for the forecast graph
|
|
|
glucoseFromPersistence = objects
|
|
|
|
|
|
@@ -832,107 +810,84 @@ extension Treatments.StateModel {
|
|
|
deltaBG = delta
|
|
|
}
|
|
|
|
|
|
- // Determinations
|
|
|
- private func setupDeterminationsArray() async {
|
|
|
- do {
|
|
|
- let fetchedObjectIDs = try await determinationStorage.fetchLastDeterminationObjectID(
|
|
|
- predicate: NSPredicate.predicateFor30MinAgoForDetermination
|
|
|
- )
|
|
|
+ // MARK: - Determination Controller
|
|
|
|
|
|
- await MainActor.run {
|
|
|
- determinationObjectIDs = fetchedObjectIDs
|
|
|
+ @MainActor func setupDeterminationController() {
|
|
|
+ determinationControllerDelegate.onContentChange = { [weak self] in
|
|
|
+ Task { @MainActor in
|
|
|
+ guard let self else { return }
|
|
|
+ self.updateDeterminationFromController()
|
|
|
+ self.insulinCalculated = await self.calculateInsulin()
|
|
|
+ let forecastData = self.mapForecastsFromController()
|
|
|
+ await self.updateForecasts(with: forecastData)
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- let determinationObjects: [OrefDetermination] = try await CoreDataStack.shared
|
|
|
- .getNSManagedObject(with: determinationObjectIDs, context: viewContext)
|
|
|
-
|
|
|
- updateDeterminationsArray(with: determinationObjects)
|
|
|
- } catch let error as CoreDataError {
|
|
|
- debug(.default, "Core Data error: \(error)")
|
|
|
+ do {
|
|
|
+ try determinationController.performFetch()
|
|
|
+ updateDeterminationFromController()
|
|
|
+ Task { @MainActor in
|
|
|
+ self.insulinCalculated = await self.calculateInsulin()
|
|
|
+ let forecastData = self.mapForecastsFromController()
|
|
|
+ await self.updateForecasts(with: forecastData)
|
|
|
+ }
|
|
|
} catch {
|
|
|
- debug(.default, "Unexpected error: \(error)")
|
|
|
+ debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform determination fetch: \(error)")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private func mapForecastsForChart() async -> Determination? {
|
|
|
- guard let determinationID = await MainActor.run(body: { determinationObjectIDs.first }) else {
|
|
|
+ @MainActor private func updateDeterminationFromController() {
|
|
|
+ guard let objects = determinationController.fetchedObjects,
|
|
|
+ let mostRecentDetermination = objects.first else { return }
|
|
|
+
|
|
|
+ determination = objects
|
|
|
+
|
|
|
+ // setup vars for bolus calculation
|
|
|
+ insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
|
|
|
+ evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
|
|
|
+ minPredBG = (mostRecentDetermination.minPredBGFromReason ?? 0) as Decimal
|
|
|
+ lastLoopDate = apsManager.lastLoopDate as Date?
|
|
|
+ insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
|
|
|
+ target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
|
|
|
+ isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal
|
|
|
+ cob = mostRecentDetermination.cob as Int16
|
|
|
+ iob = (mostRecentDetermination.iob ?? 0) as Decimal
|
|
|
+ basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
|
|
|
+ carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor private func mapForecastsFromController() -> Determination? {
|
|
|
+ guard let determinationObject = determinationController.fetchedObjects?.first else {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
- let context = CoreDataStack.shared.newTaskContext()
|
|
|
- context.name = "TreatmentsStateModel.mapForecastsForChart"
|
|
|
-
|
|
|
- return await context.perform {
|
|
|
- let request = NSFetchRequest<Forecast>(entityName: "Forecast")
|
|
|
- request.predicate = NSPredicate(format: "orefDetermination = %@", determinationID)
|
|
|
- request.relationshipKeyPathsForPrefetching = ["forecastValues"]
|
|
|
-
|
|
|
- let forecasts: [Forecast]
|
|
|
- do {
|
|
|
- forecasts = try context.fetch(request)
|
|
|
- } catch {
|
|
|
- debug(
|
|
|
- .default,
|
|
|
- "\(DebuggingIdentifiers.failed) Error mapping forecasts for chart: \(error)"
|
|
|
- )
|
|
|
- return nil
|
|
|
- }
|
|
|
-
|
|
|
- func values(for type: String) -> [Int]? {
|
|
|
- let result = forecasts.first { $0.type == type }?
|
|
|
- .forecastValuesArray
|
|
|
- .map { Int($0.value) }
|
|
|
- return (result?.isEmpty ?? true) ? nil : result
|
|
|
- }
|
|
|
-
|
|
|
- let predictions = Predictions(
|
|
|
- iob: values(for: "iob"),
|
|
|
- zt: values(for: "zt"),
|
|
|
- cob: values(for: "cob"),
|
|
|
- uam: values(for: "uam")
|
|
|
- )
|
|
|
-
|
|
|
- return Determination(
|
|
|
- id: UUID(),
|
|
|
- reason: "",
|
|
|
- units: 0,
|
|
|
- insulinReq: 0,
|
|
|
- sensitivityRatio: 0,
|
|
|
- rate: 0,
|
|
|
- duration: 0,
|
|
|
- iob: 0,
|
|
|
- cob: 0,
|
|
|
- predictions: predictions.isEmpty ? nil : predictions,
|
|
|
- carbsReq: 0,
|
|
|
- temp: nil,
|
|
|
- reservoir: 0,
|
|
|
- insulinForManualBolus: 0,
|
|
|
- manualBolusErrorString: 0,
|
|
|
- carbRatio: 0,
|
|
|
- received: false
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
+ let forecastsSet = determinationObject.forecasts ?? []
|
|
|
+ let predictions = Predictions(
|
|
|
+ iob: forecastsSet.extractValues(for: "iob"),
|
|
|
+ zt: forecastsSet.extractValues(for: "zt"),
|
|
|
+ cob: forecastsSet.extractValues(for: "cob"),
|
|
|
+ uam: forecastsSet.extractValues(for: "uam")
|
|
|
+ )
|
|
|
|
|
|
- private func updateDeterminationsArray(with objects: [OrefDetermination]) {
|
|
|
- Task { @MainActor in
|
|
|
- guard let mostRecentDetermination = objects.first else { return }
|
|
|
- determination = objects
|
|
|
-
|
|
|
- // setup vars for bolus calculation
|
|
|
- insulinRequired = (mostRecentDetermination.insulinReq ?? 0) as Decimal
|
|
|
- evBG = (mostRecentDetermination.eventualBG ?? 0) as Decimal
|
|
|
- minPredBG = (mostRecentDetermination.minPredBGFromReason ?? 0) as Decimal
|
|
|
- lastLoopDate = apsManager.lastLoopDate as Date?
|
|
|
- insulin = (mostRecentDetermination.insulinForManualBolus ?? 0) as Decimal
|
|
|
- target = (mostRecentDetermination.currentTarget ?? currentBGTarget as NSDecimalNumber) as Decimal
|
|
|
- isf = (mostRecentDetermination.insulinSensitivity ?? currentISF as NSDecimalNumber) as Decimal
|
|
|
- cob = mostRecentDetermination.cob as Int16
|
|
|
- iob = (mostRecentDetermination.iob ?? 0) as Decimal
|
|
|
- basal = (mostRecentDetermination.tempBasal ?? 0) as Decimal
|
|
|
- carbRatio = (mostRecentDetermination.carbRatio ?? currentCarbRatio as NSDecimalNumber) as Decimal
|
|
|
- insulinCalculated = await calculateInsulin()
|
|
|
- }
|
|
|
+ return Determination(
|
|
|
+ id: UUID(),
|
|
|
+ reason: "",
|
|
|
+ units: 0,
|
|
|
+ insulinReq: 0,
|
|
|
+ sensitivityRatio: 0,
|
|
|
+ rate: 0,
|
|
|
+ duration: 0,
|
|
|
+ iob: 0,
|
|
|
+ cob: 0,
|
|
|
+ predictions: predictions.isEmpty ? nil : predictions,
|
|
|
+ carbsReq: 0,
|
|
|
+ temp: nil,
|
|
|
+ reservoir: 0,
|
|
|
+ insulinForManualBolus: 0,
|
|
|
+ manualBolusErrorString: 0,
|
|
|
+ carbRatio: 0,
|
|
|
+ received: false
|
|
|
+ )
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1011,46 +966,35 @@ private extension Predictions {
|
|
|
// MARK: - Last Pump Bolus
|
|
|
|
|
|
extension Treatments.StateModel {
|
|
|
- /// Mirrors `HomeStateModel.setupLastBolus` so the in-progress visualizer can show the
|
|
|
+ /// Mirrors `HomeStateModel`'s last-bolus controller so the in-progress visualizer can show the
|
|
|
/// running pump-bolus's amount as the denominator (not the user's pending entry).
|
|
|
/// Filters out external boluses via `NSPredicate.lastPumpBolus`.
|
|
|
- func setupLastBolus() {
|
|
|
- Task {
|
|
|
- do {
|
|
|
- guard let id = try await fetchLastBolus() else { return }
|
|
|
- await updateLastBolus(with: id)
|
|
|
- } catch {
|
|
|
- debug(.default, "\(DebuggingIdentifiers.failed) Error setting up last bolus: \(error)")
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private func fetchLastBolus() async throws -> NSManagedObjectID? {
|
|
|
- let context = CoreDataStack.shared.newTaskContext()
|
|
|
- context.name = "fetchLastBolus"
|
|
|
-
|
|
|
- let results = try await CoreDataStack.shared.fetchEntitiesAsync(
|
|
|
- ofType: PumpEventStored.self,
|
|
|
- onContext: context,
|
|
|
- predicate: NSPredicate.lastPumpBolus,
|
|
|
- key: "timestamp",
|
|
|
- ascending: false,
|
|
|
- fetchLimit: 1
|
|
|
- )
|
|
|
-
|
|
|
- return try await context.perform {
|
|
|
- guard let fetched = results as? [PumpEventStored] else {
|
|
|
- throw CoreDataError.fetchError(function: #function, file: #file)
|
|
|
+ @MainActor func setupLastBolusController() {
|
|
|
+ lastBolusControllerDelegate.onContentChange = { [weak self] in
|
|
|
+ Task { @MainActor in
|
|
|
+ self?.updateLastBolusFromController()
|
|
|
}
|
|
|
- return fetched.map(\.objectID).first
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- @MainActor private func updateLastBolus(with id: NSManagedObjectID) {
|
|
|
do {
|
|
|
- lastPumpBolus = try viewContext.existingObject(with: id) as? PumpEventStored
|
|
|
+ try lastBolusController.performFetch()
|
|
|
+ updateLastBolusFromController()
|
|
|
} catch {
|
|
|
- debug(.default, "\(DebuggingIdentifiers.failed) updateLastBolus: \(error)")
|
|
|
+ debug(.default, "\(DebuggingIdentifiers.failed) Failed to perform last bolus fetch: \(error)")
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ @MainActor private func updateLastBolusFromController() {
|
|
|
+ lastPumpBolus = lastBolusController.fetchedObjects?.first
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private extension Set where Element == Forecast {
|
|
|
+ /// Extracts the sorted forecast values for a given prediction type (iob/zt/cob/uam).
|
|
|
+ func extractValues(for type: String) -> [Int]? {
|
|
|
+ let values = first { $0.type == type }?
|
|
|
+ .forecastValuesArray
|
|
|
+ .map { Int($0.value) }
|
|
|
+ return (values?.isEmpty ?? true) ? nil : values
|
|
|
+ }
|
|
|
}
|