117 lines · 3.8 KB · Swift
RawDownload
import Foundation public struct UsagePace: Sendable {    public enum Stage: Sendable {        case onTrack        case slightlyAhead        case ahead        case farAhead        case slightlyBehind        case behind        case farBehind    }     public let stage: Stage    public let deltaPercent: Double    public let expectedUsedPercent: Double    public let actualUsedPercent: Double    public let etaSeconds: TimeInterval?    public let willLastToReset: Bool    public let runOutProbability: Double?     public init(        stage: Stage,        deltaPercent: Double,        expectedUsedPercent: Double,        actualUsedPercent: Double,        etaSeconds: TimeInterval?,        willLastToReset: Bool,        runOutProbability: Double? = nil)    {        self.stage = stage        self.deltaPercent = deltaPercent        self.expectedUsedPercent = expectedUsedPercent        self.actualUsedPercent = actualUsedPercent        self.etaSeconds = etaSeconds        self.willLastToReset = willLastToReset        self.runOutProbability = runOutProbability    }     public static func weekly(        window: RateWindow,        now: Date = .init(),        defaultWindowMinutes: Int = 10080) -> UsagePace?    {        guard let resetsAt = window.resetsAt else { return nil }        let minutes = window.windowMinutes ?? defaultWindowMinutes        guard minutes > 0 else { return nil }         let duration = TimeInterval(minutes) * 60        let timeUntilReset = resetsAt.timeIntervalSince(now)        guard timeUntilReset > 0 else { return nil }        guard timeUntilReset <= duration else { return nil }        let elapsed = (duration - timeUntilReset).clamped(to: 0...duration)        let expected = ((elapsed / duration) * 100).clamped(to: 0...100)        let actual = window.usedPercent.clamped(to: 0...100)        if elapsed == 0, actual > 0 {            return nil        }        let delta = actual - expected        let stage = Self.stage(for: delta)         var etaSeconds: TimeInterval?        var willLastToReset = false         if elapsed > 0, actual > 0 {            let rate = actual / elapsed            if rate > 0 {                let remaining = max(0, 100 - actual)                let candidate = remaining / rate                if candidate >= timeUntilReset {                    willLastToReset = true                } else {                    etaSeconds = candidate                }            }        } else if elapsed > 0, actual == 0 {            willLastToReset = true        }         return UsagePace(            stage: stage,            deltaPercent: delta,            expectedUsedPercent: expected,            actualUsedPercent: actual,            etaSeconds: etaSeconds,            willLastToReset: willLastToReset,            runOutProbability: nil)    }     public static func historical(        expectedUsedPercent: Double,        actualUsedPercent: Double,        etaSeconds: TimeInterval?,        willLastToReset: Bool,        runOutProbability: Double?) -> UsagePace    {        let expected = expectedUsedPercent.clamped(to: 0...100)        let actual = actualUsedPercent.clamped(to: 0...100)        let delta = actual - expected        return UsagePace(            stage: Self.stage(for: delta),            deltaPercent: delta,            expectedUsedPercent: expected,            actualUsedPercent: actual,            etaSeconds: etaSeconds,            willLastToReset: willLastToReset,            runOutProbability: runOutProbability)    }     private static func stage(for delta: Double) -> Stage {        let absDelta = abs(delta)        if absDelta <= 2 { return .onTrack }        if absDelta <= 6 { return delta >= 0 ? .slightlyAhead : .slightlyBehind }        if absDelta <= 12 { return delta >= 0 ? .ahead : .behind }        return delta >= 0 ? .farAhead : .farBehind    }}