73 lines
2.1 KiB
Swift
73 lines
2.1 KiB
Swift
import Foundation
|
|
|
|
public enum WatchBaselineMaturity: String, Codable, Sendable {
|
|
case unavailable
|
|
case provisional
|
|
case personalized
|
|
}
|
|
|
|
public enum WatchBaselineComparison: String, Codable, Sendable {
|
|
case unavailable
|
|
case usual
|
|
case slightlyDifferent
|
|
case clearlyDifferent
|
|
}
|
|
|
|
public struct WatchBaselineSnapshot: Codable, Equatable, Sendable {
|
|
public let maturity: WatchBaselineMaturity
|
|
public let comparableMeasurements: Int
|
|
public let medianBPM: Double?
|
|
public let medianAbsoluteDeviation: Double?
|
|
|
|
public init(measurements: [WatchMeasurementSummaryV1]) {
|
|
let values = measurements
|
|
.filter { $0.source == .appleWatchOptical && $0.isValid && $0.quality >= 0.65 && $0.context.isBaselineComparable }
|
|
.compactMap(\.averageBPM)
|
|
.filter(\.isFinite)
|
|
|
|
comparableMeasurements = values.count
|
|
medianBPM = Self.median(values)
|
|
medianAbsoluteDeviation = medianBPM.map { median in
|
|
Self.median(values.map { abs($0 - median) }) ?? 0
|
|
}
|
|
if values.count >= 10 {
|
|
maturity = .personalized
|
|
} else if values.count >= 5 {
|
|
maturity = .provisional
|
|
} else {
|
|
maturity = .unavailable
|
|
}
|
|
}
|
|
|
|
public func compare(bpm: Double?) -> WatchBaselineComparison {
|
|
guard maturity != .unavailable, let bpm, bpm.isFinite, let medianBPM else {
|
|
return .unavailable
|
|
}
|
|
let scale = max(medianAbsoluteDeviation ?? 0, 2)
|
|
let robustDistance = abs(bpm - medianBPM) / scale
|
|
if robustDistance < 2 { return .usual }
|
|
if robustDistance < 4 { return .slightlyDifferent }
|
|
return .clearlyDifferent
|
|
}
|
|
|
|
private static func median(_ values: [Double]) -> Double? {
|
|
guard !values.isEmpty else { return nil }
|
|
let sorted = values.sorted()
|
|
let middle = sorted.count / 2
|
|
return sorted.count.isMultiple(of: 2)
|
|
? (sorted[middle - 1] + sorted[middle]) / 2
|
|
: sorted[middle]
|
|
}
|
|
}
|
|
|
|
public extension WatchBaselineComparison {
|
|
var frenchLabel: String {
|
|
switch self {
|
|
case .unavailable: "Baseline en construction"
|
|
case .usual: "Habituel"
|
|
case .slightlyDifferent: "Légèrement différent"
|
|
case .clearlyDifferent: "Clairement différent"
|
|
}
|
|
}
|
|
}
|