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 }}
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
}
}