Public edition prepared 2026-09-12. Personal/account/contact details are visibly redacted where present. The original SHA256 identifies the captured unredacted full file, not this redacted extract. PUBLIC-SHA256SUMS.txt identifies downloadable public files. This material is not a signed declaration or proof of an Apple-received Build.

# S11  ECB data request XML parsing and cache fallback

Project key: calc
Original relative path: Packages/PrecisionCalcKit/Sources/CalcSystemIntegration/SystemIntegration.swift
Captured: 2026-09-12T18:57:15+08:00
Original full-file SHA256: 1fb932a35430ec0bec8fc612bbbb251283e749c30dad8df59702221fc951a623
Evidence level: local source excerpt
Build correspondence: not yet established with an Apple-received binary.
Line numbers refer to the captured original file. Gaps are explicitly marked. No source code was changed.

## Original lines 76 to 150

```text
0076      public let source: String
0077      public let baseCurrency: String
0078      public let rates: [String: BigDecimal]
0079      public let retrievedAt: Date
0080  
0081      public init(date: Date, source: String, baseCurrency: String = "EUR", rates: [String: BigDecimal], retrievedAt: Date = Date()) {
0082          self.date = date; self.source = source; self.baseCurrency = baseCurrency; self.rates = rates; self.retrievedAt = retrievedAt
0083      }
0084  }
0085  
0086  public protocol ExchangeRateProvider: Sendable { func latestRates() async throws -> ExchangeRateSnapshot }
0087  
0088  public enum ExchangeRateError: Error, Sendable, Hashable, LocalizedError {
0089      case invalidResponse, missingCurrency(String), invalidXML, noCache
0090      public var errorDescription: String? {
0091          switch self { case .invalidResponse: "汇率服务响应无效"; case .missingCurrency(let code): "参考源没有提供 \(code) 汇率"; case .invalidXML: "无法解析参考汇率数据"; case .noCache: "没有可用的本地汇率快照" }
0092      }
0093  }
0094  
0095  public struct ECBExchangeRateProvider: ExchangeRateProvider {
0096      public let endpoint: URL
0097      public let session: URLSession
0098      public init(endpoint: URL = URL(string: "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml") ?? URL(fileURLWithPath: "/invalid-ecb-endpoint"), session: URLSession = .shared) { self.endpoint = endpoint; self.session = session }
0099  
0100      public func latestRates() async throws -> ExchangeRateSnapshot {
0101          var request = URLRequest(url: endpoint, timeoutInterval: 10)
0102          request.setValue("PrecisionCalc/1.0", forHTTPHeaderField: "User-Agent")
0103          let (data, response) = try await session.data(for: request)
0104          guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { throw ExchangeRateError.invalidResponse }
0105          return try Self.parse(data)
0106      }
0107  
0108      public static func parse(_ data: Data) throws -> ExchangeRateSnapshot {
0109          let delegate = ECBParserDelegate()
0110          let parser = XMLParser(data: data)
0111          parser.delegate = delegate
0112          guard parser.parse(), !delegate.rates.isEmpty else { throw ExchangeRateError.invalidXML }
0113          var rates = delegate.rates
0114          rates["EUR"] = .one
0115          return ExchangeRateSnapshot(date: delegate.date ?? Date(), source: "European Central Bank", rates: rates)
0116      }
0117  }
0118  
0119  private final class ECBParserDelegate: NSObject, XMLParserDelegate, @unchecked Sendable {
0120      var rates: [String: BigDecimal] = [:]
0121      var date: Date?
0122      func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String: String] = [:]) {
0123          if let value = attributeDict["time"] { date = ISO8601DateFormatter.precisionCalcDay.date(from: value) }
0124          if let currency = attributeDict["currency"], let rate = attributeDict["rate"], let decimal = try? BigDecimal(parsing: rate) { rates[currency] = decimal }
0125      }
0126  }
0127  
0128  public actor CachedExchangeRateProvider: ExchangeRateProvider {
0129      private let upstream: any ExchangeRateProvider
0130      private let defaults: UserDefaults
0131      private let key = "precisioncalc.exchange-rate.snapshot"
0132      public init(upstream: any ExchangeRateProvider, suiteName: String = "group.com.calculatorx.shared") {
0133          self.upstream = upstream
0134          self.defaults = UserDefaults(suiteName: suiteName) ?? .standard
0135      }
0136      public func latestRates() async throws -> ExchangeRateSnapshot {
0137          do {
0138              let snapshot = try await upstream.latestRates()
0139              if let data = try? JSONEncoder().encode(snapshot) { defaults.set(data, forKey: key) }
0140              return snapshot
0141          } catch {
0142              guard let data = defaults.data(forKey: key), let snapshot = try? JSONDecoder().decode(ExchangeRateSnapshot.self, from: data) else { throw error }
0143              return snapshot
0144          }
0145      }
0146  }
0147  
0148  public struct BundledSnapshotProvider: ExchangeRateProvider {
0149      public init() {}
0150      public func latestRates() async throws -> ExchangeRateSnapshot {
```
