154 lines
3.9 KiB
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
|
|
}
|
|
}
|