import Foundation /// Fills gaps between snapshots for chart display only — it never creates or /// mutates Core Data objects. A source that has a snapshot in February and the /// next in June leaves March/April/May empty; without filling, per-source and /// category lines dip or break. This computes a value for every month in the /// grid so the graphs read continuously. /// /// - Internal gaps (between two known snapshots) are linearly interpolated when /// `interpolate` is true, or carried forward (stepped) when false. /// - After a source's last snapshot the value is carried forward (there is no /// future point to interpolate towards) — matches the portfolio total. /// - Before a source's first snapshot the value is `nil` (the source did not /// exist yet, so it must not contribute 0 and drag the total down). enum ChartGapFill { /// The user-facing toggle (Settings). Defaults to ON so charts look right out /// of the box; reading via `object(forKey:)` so an unset key means enabled. static let defaultsKey = "smoothChartGaps" static var isEnabled: Bool { (UserDefaults.standard.object(forKey: defaultsKey) as? Bool) ?? true } /// For a single source: `known` maps a month index (position in the ordered /// month grid) to that month's value. Returns a value per month index, with /// internal gaps filled and a trailing carry-forward. `nil` means "absent". static func denseValues( known: [Int: Decimal], monthCount: Int, interpolate: Bool ) -> [Decimal?] { var result = [Decimal?](repeating: nil, count: monthCount) let indices = known.keys.sorted() guard let first = indices.first, let last = indices.last else { return result } for i in indices { result[i] = known[i] } if interpolate { // Linear interpolation across each internal gap. for k in 0..<(indices.count - 1) { let a = indices[k] let b = indices[k + 1] guard b - a > 1, let va = known[a], let vb = known[b] else { continue } for i in (a + 1)..