import Foundation // MARK: - App Group bridge (extension side) // // KEEP IN SYNC with PortfolioJournal/Utilities/SharedQuickUpdate.swift — same // UserDefaults suite, same keys, same JSON shapes. Duplicated here so the // extension stays self-contained (no Core Data, no app-target sources). struct ExtSourceInfo: Codable, Identifiable { let id: UUID let name: String let latestValue: Double let currencyCode: String var updatedThisMonth: Bool } struct ExtPendingUpdate: Codable { let sourceId: UUID let amount: Double let capturedAt: Date } enum ExtSharedStore { static let appGroupIdentifier = "group.com.alexandrevazquez.portfoliojournal" private static let mirrorKey = "sharedSourceMirror" private static let queueKey = "pendingQuickUpdates" private static var defaults: UserDefaults? { UserDefaults(suiteName: appGroupIdentifier) } static func readMirror() -> [ExtSourceInfo] { guard let data = defaults?.data(forKey: mirrorKey), let decoded = try? JSONDecoder().decode([ExtSourceInfo].self, from: data) else { return [] } return decoded } static func appendPending(_ update: ExtPendingUpdate) { var queue: [ExtPendingUpdate] = [] if let data = defaults?.data(forKey: queueKey), let decoded = try? JSONDecoder().decode([ExtPendingUpdate].self, from: data) { queue = decoded } queue.append(update) if let data = try? JSONEncoder().encode(queue) { defaults?.set(data, forKey: queueKey) } // Mark the source as updated so the next share preselects the following one. var mirror = readMirror() if let idx = mirror.firstIndex(where: { $0.id == update.sourceId }) { mirror[idx].updatedThisMonth = true if let data = try? JSONEncoder().encode(mirror) { defaults?.set(data, forKey: mirrorKey) } } } } // MARK: - Amount parsing (locale-tolerant) enum ExtAmountParser { /// Parses strings like "12.345,67 €", "$1,234.56", "1234,5" into a Decimal. static func parse(_ raw: String) -> Decimal? { // Strip everything except digits and separators. let cleaned = raw.filter { $0.isNumber || $0 == "." || $0 == "," } guard !cleaned.isEmpty else { return nil } let lastDot = cleaned.lastIndex(of: ".") let lastComma = cleaned.lastIndex(of: ",") var normalized = cleaned switch (lastDot, lastComma) { case let (dot?, comma?): // Both present: the LAST one is the decimal separator. if dot > comma { normalized = cleaned.replacingOccurrences(of: ",", with: "") } else { normalized = cleaned.replacingOccurrences(of: ".", with: "") .replacingOccurrences(of: ",", with: ".") } case (nil, let comma?): // Only commas: decimal if the trailing group isn't exactly 3 digits. let trailing = cleaned.distance(from: cleaned.index(after: comma), to: cleaned.endIndex) if trailing == 3 && cleaned.filter({ $0 == "," }).count > 0 && cleaned.count > 4 { normalized = cleaned.replacingOccurrences(of: ",", with: "") } else { normalized = cleaned.replacingOccurrences(of: ",", with: ".") } case (let dot?, nil): // Only dots: decimal if the trailing group isn't exactly 3 digits. let trailing = cleaned.distance(from: cleaned.index(after: dot), to: cleaned.endIndex) if trailing == 3 && cleaned.filter({ $0 == "." }).count > 1 { normalized = cleaned.replacingOccurrences(of: ".", with: "") } case (nil, nil): break } guard let value = Decimal(string: normalized, locale: Locale(identifier: "en_US_POSIX")), value > 0 else { return nil } return value } }