Files
2026-05-08 12:00:05 +06:00

215 lines
6.6 KiB
Swift

import Foundation
enum ValueFormatter {
private static let rawFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.roundingMode = .halfUp
formatter.minimumFractionDigits = 0
return formatter
}()
private static let currencyFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.maximumFractionDigits = 0
formatter.minimumFractionDigits = 0
return formatter
}()
private static func fractionZeroCount(value: Decimal, maxCount: Int) -> Int {
guard value > 0, value < 1 else {
return 0
}
for count in 0 ..< maxCount {
if value * pow(10, count + 1) >= 1 {
return count
}
}
return maxCount
}
private static func digitsAndValue(value: Decimal, basePow: Int) -> (Int, Decimal) {
let digits: Int
switch value {
case pow(10, basePow) ..< (2 * pow(10, basePow + 1)): digits = 2
case (2 * pow(10, basePow + 1)) ..< (2 * pow(10, basePow + 2)): digits = 1
default: digits = 0
}
return (digits, value / pow(10, basePow))
}
private static func transformedFull(value: Decimal, maxDigits: Int, minDigits: Int = 0) -> (value: Decimal, digits: Int) {
let value = abs(value)
let digits: Int
switch value {
case 0:
digits = 0
case 0 ..< 1:
let zeroCount = fractionZeroCount(value: value, maxCount: maxDigits - 1)
digits = min(maxDigits, zeroCount + 4)
case 1 ..< 1.01:
digits = 4
case 1.01 ..< 1.1:
digits = 3
case 1.1 ..< 20:
digits = 2
case 20 ..< 200:
digits = 1
default:
digits = 0
}
return (value: value, digits: max(digits, minDigits))
}
private static func transformedShort(value: Decimal, maxDigits: Int = Int.max) -> (value: Decimal, digits: Int, suffix: ((String) -> String)?, tooSmall: Bool) {
var value = abs(value)
var suffix: ((String) -> String)?
let digits: Int
var tooSmall = false
switch value {
case 0:
digits = 0
case 0 ..< 0.0000_0001:
digits = 8
value = 0.0000_0001
tooSmall = true
case 0.0000_0001 ..< 1:
let zeroCount = fractionZeroCount(value: value, maxCount: 8)
digits = min(maxDigits, zeroCount + 4, 8)
case 1 ..< 1.01:
digits = 4
case 1.01 ..< 1.1:
digits = 3
case 1.1 ..< 20:
digits = 2
case 20 ..< 200:
digits = 1
case 200 ..< 19999.5:
digits = 0
case 19999.5 ..< edge(6):
(digits, value) = digitsAndValue(value: value, basePow: 3)
suffix = { String(localized: "\($0)number.thousand") }
case edge(6) ..< edge(9):
(digits, value) = digitsAndValue(value: value, basePow: 6)
suffix = { String(localized: "\($0)number.million") }
case edge(9) ..< edge(12):
(digits, value) = digitsAndValue(value: value, basePow: 9)
suffix = { String(localized: "\($0)number.billion") }
case edge(12) ..< edge(15):
(digits, value) = digitsAndValue(value: value, basePow: 12)
suffix = { String(localized: "\($0)number.trillion") }
default:
(digits, value) = digitsAndValue(value: value, basePow: 15)
suffix = { String(localized: "\($0)number.quadrillion") }
}
return (value: value, digits: digits, suffix: suffix, tooSmall: tooSmall)
}
private static func edge(_ power: Int) -> Decimal {
pow(10, power) - (pow(10, power - 3) / 2)
}
private static func decorated(string: String, suffix: ((String) -> String)? = nil, symbol: String? = nil, signValue: Decimal? = nil, tooSmall: Bool = false) -> String {
var string = string
if let suffix {
string = suffix(string)
}
if let symbol {
string = "\(string) \(symbol)"
}
if let signValue {
var sign = ""
if !signValue.isZero {
sign = signValue.isSignMinus ? "-" : "+"
}
string = "\(sign)\(string)"
}
if tooSmall {
string = "< \(string)"
}
return string
}
private static func formattedCurrency(value: Decimal, digits: Int, code: String, symbol: String, suffix: ((String) -> String)? = nil) -> String? {
currencyFormatter.currencyCode = code
currencyFormatter.currencySymbol = symbol
currencyFormatter.internationalCurrencySymbol = symbol
guard let pattern = currencyFormatter.string(from: 1 as NSDecimalNumber) else {
return nil
}
rawFormatter.maximumFractionDigits = digits
guard let string = rawFormatter.string(from: value as NSDecimalNumber) else {
return nil
}
return pattern.replacingOccurrences(of: "1", with: decorated(string: string, suffix: suffix))
}
static func format(percentValue: Decimal, showSign: Bool = true) -> String? {
let (transformedValue, digits) = transformedFull(value: percentValue, maxDigits: 2)
rawFormatter.maximumFractionDigits = digits
guard let string = rawFormatter.string(from: transformedValue as NSDecimalNumber) else {
return nil
}
return decorated(string: string, signValue: showSign ? percentValue : nil) + "%"
}
static func format(currency: Currency, value: Decimal, showSign: Bool = false) -> String? {
let (transformedValue, digits) = transformedFull(value: value, maxDigits: 18)
guard let string = formattedCurrency(value: transformedValue, digits: digits, code: currency.code, symbol: currency.symbol) else {
return nil
}
return decorated(string: string, signValue: showSign ? value : nil)
}
static func formatShort(currency: Currency, value: Decimal, showSign: Bool = false) -> String? {
let (transformedValue, digits, suffix, tooSmall) = transformedShort(value: value)
guard let string = formattedCurrency(value: transformedValue, digits: digits, code: currency.code, symbol: currency.symbol, suffix: suffix) else {
return nil
}
return decorated(string: string, signValue: showSign ? value : nil, tooSmall: tooSmall)
}
}