InvestmentTrackerApp/PortfolioJournal/Utilities/Extensions/Decimal+Extensions.swift

154 lines
3.9 KiB
Swift

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
}
}