Files
InvestmentTrackerApp/PortfolioJournal/Utilities/SnapshotGapDetector.swift
T
alexandrev-tibco a8a9ad64f3 Xcode 27: compila sin warnings y migra API deprecadas de SwiftUI
- Imports de CoreData que faltaban (MemberImportVisibility) en AccountsView,
  ImportDataView, ChartsContainerView y AddSourceView.
- Aislamiento MainActor: IsolatedDefaultValues en el target de la app,
  LinearGradient @MainActor, WatchSyncMessage nonisolated, closures de
  ReviewPromptService/MonthlyCheckInStore/AppIntents.
- Warnings de valores sin usar (SnapshotGapDetector, DashboardLayoutStore,
  OnboardingView, GoalEditorView, CoreDataStack, SettingsViewModel).
- foregroundColor→foregroundStyle, cornerRadius→clipShape,
  navigationBarLeading/Trailing→topBarLeading/Trailing,
  Task.sleep(nanoseconds:)→sleep(for:), NavigationLink(isActive:)→
  navigationDestination(isPresented:).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
2026-09-16 11:44:49 +02:00

87 lines
3.5 KiB
Swift

import Foundation
import CoreData
/// Detects INTERNAL gaps in a source's snapshot history: two consecutive
/// snapshots more than one calendar month apart, i.e. one or more calendar
/// months between them have no data at all.
///
/// This complements `ChartGapFill`, which only smooths the *display* by
/// interpolating/carrying values across those empty months. The chart looks
/// continuous, but real data is missing this surfaces that so the user can
/// fill it in. Read-only: it never mutates Core Data.
enum SnapshotGapDetector {
/// A single internal gap for one source.
struct Gap: Identifiable, Equatable {
let sourceId: UUID
let sourceName: String
/// The month of the snapshot before the gap (start of that month).
let fromMonth: Date
/// The month of the snapshot after the gap (start of that month).
let toMonth: Date
/// Number of calendar months with no data between `fromMonth` and `toMonth`.
let missingMonths: Int
/// The individual missing months (start-of-month dates), oldest first.
let missingMonthDates: [Date]
var id: String { "\(sourceId.uuidString)-\(fromMonth.timeIntervalSince1970)" }
}
/// Computes all internal gaps across the provided sources' snapshots.
/// Only months strictly between two known snapshots count a source's
/// trailing "not updated in a while" is handled elsewhere (pending updates).
static func detectGaps(sources: [InvestmentSource], snapshots: [Snapshot]) -> [Gap] {
let snapshotsBySource = Dictionary(grouping: snapshots) { $0.source?.id }
var gaps: [Gap] = []
for source in sources {
let sourceId = source.id
guard let sourceSnapshots = snapshotsBySource[sourceId], sourceSnapshots.count >= 2 else {
continue
}
// One value per calendar month (keep the latest in each), sorted ascending.
var monthStarts: [Date] = sourceSnapshots
.map { $0.date.startOfMonth }
monthStarts = Array(Set(monthStarts)).sorted()
guard monthStarts.count >= 2 else { continue }
for i in 0..<(monthStarts.count - 1) {
let from = monthStarts[i]
let to = monthStarts[i + 1]
let monthsApart = from.monthsBetween(to)
guard monthsApart > 1 else { continue }
var missingDates: [Date] = []
var cursor = from.adding(months: 1).startOfMonth
while cursor < to {
missingDates.append(cursor)
cursor = cursor.adding(months: 1).startOfMonth
}
guard !missingDates.isEmpty else { continue }
gaps.append(Gap(
sourceId: sourceId,
sourceName: source.name,
fromMonth: from,
toMonth: to,
missingMonths: missingDates.count,
missingMonthDates: missingDates
))
}
}
// Most recent gaps first (by the month after the gap), then by source name.
return gaps.sorted {
if $0.toMonth != $1.toMonth { return $0.toMonth > $1.toMonth }
return $0.sourceName < $1.sourceName
}
}
/// Total count of missing months across all gaps.
static func totalMissingMonths(_ gaps: [Gap]) -> Int {
gaps.reduce(0) { $0 + $1.missingMonths }
}
}