82 lines
3.0 KiB
Swift
82 lines
3.0 KiB
Swift
import Foundation
|
|
|
|
/// Owns shared daily rates and calculation state for one launcher lifetime.
|
|
@MainActor
|
|
public final class CurrencyCalculation {
|
|
public private(set) var result: CalculatorResult?
|
|
public private(set) var isCurrency = false
|
|
public var changed: (() -> Void)?
|
|
|
|
private let fetch: @MainActor () async throws -> CurrencyRates
|
|
private let now: () -> Date
|
|
private let calendar: Calendar
|
|
private var query = ""
|
|
private var rates: CurrencyRates?
|
|
private var fetchedAt: Date?
|
|
private var task: Task<Void, Never>?
|
|
private var error: String?
|
|
private var failureQuery: String?
|
|
|
|
public init(
|
|
now: @escaping () -> Date = Date.init,
|
|
calendar: Calendar = .current,
|
|
fetch: @escaping @MainActor () async throws -> CurrencyRates = { try await CurrencyAPI.fetch() }
|
|
) {
|
|
self.now = now
|
|
self.calendar = calendar
|
|
self.fetch = fetch
|
|
}
|
|
|
|
deinit { task?.cancel() }
|
|
|
|
public func update(_ query: String) {
|
|
self.query = query
|
|
if failureQuery != query {
|
|
error = nil
|
|
failureQuery = nil
|
|
}
|
|
evaluate(allowingFetch: true)
|
|
}
|
|
|
|
private func evaluate(allowingFetch: Bool) {
|
|
isCurrency = Calculator.isCurrencyQuery(query, currencyRates: rates)
|
|
let stale = fetchedAt.map { !calendar.isDate($0, inSameDayAs: now()) } ?? true
|
|
if allowingFetch, isCurrency, stale, error == nil, task == nil { refresh() }
|
|
result = Calculator.evaluate(query, currencyRates: rates)
|
|
if isCurrency, let error {
|
|
let cached = rates.map { " Cached snapshot \($0.date) is not used." } ?? ""
|
|
result = CalculatorResult(displayValue: "Currency rates unavailable", copyValue: nil,
|
|
detail: "Daily rates: \(error). Change the query to retry.\(cached)")
|
|
} else if isCurrency, rates != nil, let answer = result {
|
|
let cacheStatus = stale
|
|
? (task == nil ? "Stale cached daily rates" : "Stale cache; refreshing daily rates…")
|
|
: "Cached daily rates"
|
|
result = CalculatorResult(displayValue: answer.displayValue, copyValue: answer.copyValue,
|
|
detail: "\(answer.detail) · \(cacheStatus)")
|
|
}
|
|
}
|
|
|
|
private func refresh() {
|
|
let fetch = self.fetch
|
|
task = Task { [weak self] in
|
|
let response: Result<CurrencyRates, any Error>
|
|
do { response = .success(try await fetch()) }
|
|
catch { response = .failure(error) }
|
|
guard !Task.isCancelled, let self else { return }
|
|
self.task = nil
|
|
switch response {
|
|
case .success(let rates):
|
|
self.rates = rates
|
|
self.fetchedAt = self.now()
|
|
self.error = nil
|
|
self.failureQuery = nil
|
|
case .failure(let error):
|
|
self.error = error.localizedDescription
|
|
self.failureQuery = self.query
|
|
}
|
|
self.evaluate(allowingFetch: false)
|
|
self.changed?()
|
|
}
|
|
}
|
|
}
|