Signal-iOS/SignalServiceKit/Util/OWSFormat.swift
2026-05-06 15:11:55 -05:00

182 lines
5.8 KiB
Swift

//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public enum OWSFormat {
// We evacuate this cache in the background in case the
// user changes a system setting that would affect
// formatting behavior.
private static let shortNameComponentsCache = LRUCache<String, String>(
maxSize: 512,
nseMaxSize: 64,
shouldEvacuateInBackground: true,
)
public static func formatNameComponents(_ nameComponents: PersonNameComponents) -> String {
formatNameComponents(nameComponents, style: .default)
}
public static func formatNameComponentsShort(_ nameComponents: PersonNameComponents) -> String {
formatNameComponents(nameComponents, style: .short)
}
public static func formatNameComponents(
_ nameComponents: PersonNameComponents,
style: PersonNameComponentsFormatter.Style,
) -> String {
let cacheKey = String(describing: nameComponents) + ".\(style.rawValue)"
if let value = shortNameComponentsCache.get(key: cacheKey) {
return value
}
let value = PersonNameComponentsFormatter.localizedString(
from: nameComponents,
style: style,
options: [],
)
shortNameComponentsCache.set(key: cacheKey, value: value)
return value
}
}
// MARK: -
public extension OWSFormat {
private static let defaultNumberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .none
return formatter
}()
static func formatDurationSeconds(_ timeSeconds: Int) -> String {
let timeSeconds = max(0, timeSeconds)
let seconds = timeSeconds % 60
let minutes = (timeSeconds / 60) % 60
let hours = timeSeconds / 3600
if hours > 0 {
return String(format: "%llu:%02llu:%02llu", hours, minutes, seconds)
} else {
return String(format: "%llu:%02llu", minutes, seconds)
}
}
static func formatNSInt(_ value: NSNumber) -> String {
guard let value = defaultNumberFormatter.string(from: value) else {
owsFailDebug("Could not format value.")
return ""
}
return value
}
static func formatInt(_ value: Int) -> String {
return formatNSInt(NSNumber(value: value))
}
static func formatUInt(_ value: UInt) -> String {
return formatNSInt(NSNumber(value: value))
}
static func formatUInt32(_ value: UInt32) -> String {
return formatNSInt(NSNumber(value: value))
}
static func formatUInt64(_ value: UInt64) -> String {
return formatNSInt(NSNumber(value: value))
}
}
// MARK: - Numbers
public extension OWSFormat {
private static let decimalNumberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
private static let byteCountFormatter: ByteCountFormatter = {
let formatter = ByteCountFormatter()
formatter.formattingContext = .standalone
formatter.countStyle = .file
return formatter
}()
static func localizedDecimalString(from number: Int) -> String {
let result = decimalNumberFormatter.string(for: number)
owsAssertDebug(result != nil, "Formatted string is nil. number=[\(number)]")
return result ?? ""
}
static func localizedFileSizeString(from fileSize: UInt64) -> String {
return byteCountFormatter.string(fromByteCount: Int64(clamping: fileSize))
}
}
// MARK: - Time
public extension OWSFormat {
private static let durationFormatterS: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
// `dropTrailing` produces a single leading zero: "0:ss".
formatter.zeroFormattingBehavior = .dropTrailing
formatter.formattingContext = .standalone
formatter.allowedUnits = [.minute, .second]
return formatter
}()
private static let durationFormatterMS: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.formattingContext = .standalone
formatter.allowedUnits = [.minute, .second]
return formatter
}()
private static let durationFormatterHMS: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.formattingContext = .standalone
formatter.allowedUnits = [.hour, .minute, .second]
return formatter
}()
/**
* There's no DateComponentsFormatter configuration that produces "0:00".
* As a workaround, we make the full "00:00" string and take last N characters from it,
* where N is the length of "0:01".
*/
private static var zeroDurationString: String? = {
let formatter = DateComponentsFormatter()
formatter.formattingContext = .standalone
formatter.zeroFormattingBehavior = .pad
formatter.allowedUnits = [.minute, .second]
guard let longString = formatter.string(from: 0) else {
return nil
}
let resultStringLength = localizedDurationString(from: 1).count
return String(longString.suffix(resultStringLength))
}()
static func localizedDurationString(from timeInterval: TimeInterval) -> String {
var result: String?
switch timeInterval {
case 0..<1:
result = zeroDurationString
case 1..<60:
result = durationFormatterS.string(from: timeInterval)
case 3600...:
result = durationFormatterHMS.string(from: timeInterval)
default:
result = durationFormatterMS.string(from: timeInterval)
}
owsAssertDebug(result != nil, "Formatted string is nil. ti=[\(timeInterval)]")
return result ?? ""
}
}