import Foundation enum CurrencyFormatter { static func currentCurrencyCode() -> String { let context = CoreDataStack.shared.viewContext return AppSettings.getOrCreate(in: context).currency } static func locale(for currencyCode: String?) -> Locale { guard let currencyCode, !currencyCode.isEmpty else { return Locale.current } if let match = Locale.availableIdentifiers.first(where: { Locale(identifier: $0).currency?.identifier == currencyCode }) { return Locale(identifier: match) } return Locale.current } static func format(_ decimal: Decimal, style: NumberFormatter.Style = .currency, maximumFractionDigits: Int = 2) -> String { let formatter = NumberFormatter() formatter.numberStyle = style formatter.currencyCode = currentCurrencyCode() formatter.maximumFractionDigits = maximumFractionDigits return formatter.string(from: decimal as NSDecimalNumber) ?? "\(decimal)" } static func format( _ decimal: Decimal, currencyCode: String?, style: NumberFormatter.Style = .currency, maximumFractionDigits: Int = 2, preferredLocale: Locale? = nil ) -> String { let formatter = NumberFormatter() formatter.numberStyle = style formatter.currencyCode = currencyCode ?? currentCurrencyCode() formatter.maximumFractionDigits = maximumFractionDigits formatter.locale = preferredLocale ?? locale(for: currencyCode) return formatter.string(from: decimal as NSDecimalNumber) ?? "\(decimal)" } static func symbol(for code: String) -> String { let formatter = NumberFormatter() formatter.numberStyle = .currency formatter.currencyCode = code return formatter.currencySymbol ?? code } /// Parses a user-typed numeric string using digit-position-based separator detection. /// Locale-independent: a separator followed by 1-2 digits at the end is decimal, /// otherwise it is a thousands separator. static func parseUserInput(_ string: String, currencySymbol: String = "") -> Decimal? { let stripped = string .replacingOccurrences(of: currencySymbol, with: "") .trimmingCharacters(in: .whitespaces) .filter { $0.isNumber || $0 == "." || $0 == "," } guard !stripped.isEmpty else { return nil } let hasDot = stripped.contains(".") let hasComma = stripped.contains(",") let normalized: String if hasDot && hasComma { // Both separators: the one appearing last is the decimal let lastDot = stripped.lastIndex(of: ".")! let lastComma = stripped.lastIndex(of: ",")! if lastComma > lastDot { // e.g. 1.234,56 → 1234.56 normalized = stripped .replacingOccurrences(of: ".", with: "") .replacingOccurrences(of: ",", with: ".") } else { // e.g. 1,234.56 → 1234.56 normalized = stripped.replacingOccurrences(of: ",", with: "") } } else if hasComma { // Only comma: decimal if exactly 2 parts and last has ≤2 digits let parts = stripped.components(separatedBy: ",") if parts.count == 2 && (parts.last?.count ?? 0) <= 2 { // e.g. 408857,62 → 408857.62 normalized = stripped.replacingOccurrences(of: ",", with: ".") } else { // e.g. 408,857 or 1,234,567 → thousands normalized = stripped.replacingOccurrences(of: ",", with: "") } } else if hasDot { // Only dot: decimal if exactly 2 parts and last has ≤2 digits let parts = stripped.components(separatedBy: ".") if parts.count == 2 && (parts.last?.count ?? 0) <= 2 { // e.g. 408857.62 → already correct normalized = stripped } else { // e.g. 408.857 or 1.234.567 → thousands normalized = stripped.replacingOccurrences(of: ".", with: "") } } else { normalized = stripped } let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.locale = Locale(identifier: "en_US_POSIX") return formatter.number(from: normalized)?.decimalValue } /// Formats a decimal for display in an input field (no grouping separator). static func formatForInput(_ decimal: Decimal, currencyCode: String) -> String { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.locale = locale(for: currencyCode) formatter.minimumFractionDigits = 2 formatter.maximumFractionDigits = 2 formatter.groupingSeparator = "" return formatter.string(from: decimal as NSDecimalNumber) ?? "" } }