HealthProbe / HealthProbe / Services / SnapshotDiffService.swift
1 contributor
76 lines | 2.687kb
import Foundation

struct TypeDiff: Identifiable {
    let id: String
    let typeIdentifier: String
    let displayName: String
    let currentCount: Int
    let previousCount: Int
    let previousTracked: Bool

    var delta: Int { currentCount - previousCount }

    var percentChange: Double? {
        guard previousTracked, previousCount > 0 else { return nil }
        return Double(delta) / Double(previousCount) * 100
    }
}

enum DiffFilter: String, CaseIterable {
    case all        = "All"
    case changed    = "Changed"
    case increased  = "Increased"
    case decreased  = "Decreased"
}

final class SnapshotDiffService {
    static let shared = SnapshotDiffService()

    func diff(current: HealthSnapshot, baseline: HealthSnapshot) -> [TypeDiff] {
        let baselineMap = Dictionary(
            uniqueKeysWithValues: (baseline.typeCounts ?? []).map { ($0.typeIdentifier, $0.count) }
        )
        return (current.typeCounts ?? []).map { tc in
            let prior = baselineMap[tc.typeIdentifier]
            return TypeDiff(
                id: tc.typeIdentifier,
                typeIdentifier: tc.typeIdentifier,
                displayName: tc.displayName,
                currentCount: tc.count,
                previousCount: prior ?? 0,
                previousTracked: prior != nil
            )
        }.sorted { $0.displayName.localizedCompare($1.displayName) == .orderedAscending }
    }

    func totalAbsoluteChange(current: HealthSnapshot, baseline: HealthSnapshot) -> Int {
        let baselineMap = Dictionary(
            uniqueKeysWithValues: (baseline.typeCounts ?? []).map { ($0.typeIdentifier, $0) }
        )

        return (current.typeCounts ?? []).reduce(0) { partial, currentType in
            guard currentType.quality == .complete,
                  let previousType = baselineMap[currentType.typeIdentifier],
                  previousType.quality == .complete else {
                return partial
            }

            return partial + abs(currentType.count - previousType.count)
        }
    }

    func apply(filter: DiffFilter, to diffs: [TypeDiff]) -> [TypeDiff] {
        switch filter {
        case .all:       return diffs
        case .changed:   return diffs.filter { $0.previousTracked && $0.delta != 0 }
        case .increased: return diffs.filter { $0.previousTracked && $0.delta > 0 }
        case .decreased: return diffs.filter { $0.previousTracked && $0.delta < 0 }
        }
    }

    func nearest(to targetDate: Date, in snapshots: [HealthSnapshot]) -> HealthSnapshot? {
        snapshots
            .filter { $0.timestamp <= targetDate }
            .max { $0.timestamp < $1.timestamp }
    }
}