import Foundation extension Decimal { // MARK: - Performance: Shared formatters (avoid creating on every call) private static let percentFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .percent formatter.maximumFractionDigits = 2 formatter.multiplier = 1 return formatter }() private static let decimalFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 2 return formatter }() // MARK: - Performance: Cached currency symbol private static var _cachedCurrencySymbol: String? private static var currencySymbolCacheTime: Date? private static var cachedCurrencySymbol: String { // Refresh cache every 60 seconds to pick up settings changes let now = Date() if let cached = _cachedCurrencySymbol, let cacheTime = currencySymbolCacheTime, now.timeIntervalSince(cacheTime) < 60 { return cached } let symbol = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currencySymbol _cachedCurrencySymbol = symbol currencySymbolCacheTime = now return symbol } /// Call this when currency settings change to invalidate the cache static func invalidateCurrencyCache() { _cachedCurrencySymbol = nil currencySymbolCacheTime = nil } // MARK: - Formatting var currencyString: String { CurrencyFormatter.format(self, style: .currency, maximumFractionDigits: 2) } var compactCurrencyString: String { CurrencyFormatter.format(self, style: .currency, maximumFractionDigits: 0) } var shortCurrencyString: String { let value = NSDecimalNumber(decimal: self).doubleValue let symbol = Self.cachedCurrencySymbol switch Swift.abs(value) { case 1_000_000...: return String(format: "%@%.1fM", symbol, value / 1_000_000) case 1_000...: return String(format: "%@%.1fK", symbol, value / 1_000) default: return compactCurrencyString } } var percentageString: String { Self.percentFormatter.string(from: self as NSDecimalNumber) ?? "0%" } var signedPercentageString: String { let prefix = self >= 0 ? "+" : "" return prefix + percentageString } var decimalString: String { Self.decimalFormatter.string(from: self as NSDecimalNumber) ?? "0" } // MARK: - Conversions var doubleValue: Double { NSDecimalNumber(decimal: self).doubleValue } var intValue: Int { NSDecimalNumber(decimal: self).intValue } // MARK: - Math Operations var abs: Decimal { self < 0 ? -self : self } func rounded(scale: Int = 2) -> Decimal { var result = Decimal() var mutableSelf = self NSDecimalRound(&result, &mutableSelf, scale, .plain) return result } // MARK: - Comparisons var isPositive: Bool { self > 0 } var isNegative: Bool { self < 0 } var isZero: Bool { self == 0 } // MARK: - Static Helpers static func from(_ double: Double) -> Decimal { Decimal(double) } static func from(_ string: String) -> Decimal? { let formatter = NumberFormatter() formatter.numberStyle = .decimal return formatter.number(from: string)?.decimalValue } } // MARK: - NSDecimalNumber Extension extension NSDecimalNumber { var currencyString: String { decimalValue.currencyString } var compactCurrencyString: String { decimalValue.compactCurrencyString } } // MARK: - Optional Decimal extension Optional where Wrapped == Decimal { var orZero: Decimal { self ?? Decimal.zero } var currencyString: String { (self ?? Decimal.zero).currencyString } }