2024-08-23 14:13:17 -07:00

526 lines
19 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2020 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#if canImport(FoundationEssentials)
import FoundationEssentials
#endif
internal import _FoundationICU
typealias ICUNumberFormatterSkeleton = String
/// For testing purposes, remove all caches from below formatters.
internal func resetAllNumberFormatterCaches() {
ICUNumberFormatter.cache.removeAllObjects()
ICUCurrencyNumberFormatter.cache.removeAllObjects()
ICUPercentNumberFormatter.cache.removeAllObjects()
ICUMeasurementNumberFormatter.cache.removeAllObjects()
}
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
internal class ICUNumberFormatterBase : @unchecked Sendable {
/// `Sendable` notes: ICU's `UNumberFormatter` itself is thread safe. The result type is not, but we create that each time we format.
internal let uformatter: OpaquePointer
/// Stored for testing purposes only
internal let skeleton: String
init?(skeleton: String, localeIdentifier: String) {
self.skeleton = skeleton
let ustr = Array(skeleton.utf16)
var status = U_ZERO_ERROR
let formatter = unumf_openForSkeletonAndLocale(ustr, Int32(ustr.count), localeIdentifier, &status)
guard let formatter else {
return nil
}
guard status.isSuccess else {
unumf_close(formatter)
return nil
}
uformatter = formatter
}
deinit {
unumf_close(uformatter)
}
struct AttributePosition {
let field: UNumberFormatFields
let begin: Int
let end: Int
}
enum Value {
case integer(Int64)
case floatingPoint(Double)
case decimal(Decimal)
case numericStringRepresentation(String)
var fallbackDescription: String {
switch self {
case .integer(let i): return String(i)
case .floatingPoint(let d): return String(d)
case .decimal(let d): return d.description
case .numericStringRepresentation(let i): return i
}
}
}
func attributedStringFromPositions(_ positions: [ICUNumberFormatter.AttributePosition], string: String) -> AttributedString {
typealias NumberPartAttribute = AttributeScopes.FoundationAttributes.NumberFormatAttributes.NumberPartAttribute.NumberPart
typealias NumberSymbolAttribute = AttributeScopes.FoundationAttributes.NumberFormatAttributes.SymbolAttribute.Symbol
var attrstr = AttributedString(string)
for attr in positions {
let strRange = String.Index(utf16Offset: attr.begin, in: string) ..<
String.Index(utf16Offset: attr.end, in: string)
let range = Range<AttributedString.Index>(strRange, in: attrstr)!
let field = attr.field
var container = AttributeContainer()
if let part = NumberPartAttribute(unumberFormatField: field) {
container.numberPart = part
}
if let symbol = NumberSymbolAttribute(unumberFormatField: field) {
container.numberSymbol = symbol
}
attrstr[range].mergeAttributes(container)
}
return attrstr
}
func attributedFormatPositions(_ v: Value) -> (String, [AttributePosition])? {
var result: FormatResult?
switch v {
case .integer(let v):
result = try? FormatResult(formatter: uformatter, value: v)
case .floatingPoint(let v):
result = try? FormatResult(formatter: uformatter, value: v)
case .decimal(let v):
result = try? FormatResult(formatter: uformatter, value: v)
case .numericStringRepresentation(let v):
result = try? FormatResult(formatter: uformatter, value: v)
}
guard let result, let str = result.string else {
return nil
}
do {
let positer = try ICU.FieldPositer()
var status = U_ZERO_ERROR
unumf_resultGetAllFieldPositions(result.result, positer.positer, &status)
try status.checkSuccess()
let attributePositions = positer.fields.compactMap { next -> AttributePosition? in
return AttributePosition(field: UNumberFormatFields(CInt(next.field)), begin: next.begin, end: next.end)
}
return (str, attributePositions)
} catch {
return nil
}
}
func format(_ v: Int64) -> String? {
try? FormatResult(formatter: uformatter, value: v).string
}
func format(_ v: Double) -> String? {
try? FormatResult(formatter: uformatter, value: v).string
}
func format(_ v: Decimal) -> String? {
try? FormatResult(formatter: uformatter, value: v).string
}
func format(_ v: String) -> String? {
try? FormatResult(formatter: uformatter, value: v).string
}
// MARK: -
class FormatResult {
var result: OpaquePointer
init(formatter: OpaquePointer, value: Int64) throws {
var status = U_ZERO_ERROR
result = unumf_openResult(&status)
try status.checkSuccess()
unumf_formatInt(formatter, value, result, &status)
try status.checkSuccess()
}
init(formatter: OpaquePointer, value: Double) throws {
var status = U_ZERO_ERROR
result = unumf_openResult(&status)
try status.checkSuccess()
unumf_formatDouble(formatter, value, result, &status)
try status.checkSuccess()
}
init(formatter: OpaquePointer, value: Decimal) throws {
var status = U_ZERO_ERROR
result = unumf_openResult(&status)
try status.checkSuccess()
#if FOUNDATION_FRAMEWORK // TODO: Remove this when Decimal is moved
var v = value
var str = NSDecimalString(&v, nil)
#else
var str = value.description
#endif // FOUNDATION_FRAMEWORK
str.withUTF8 {
unumf_formatDecimal(formatter, $0.baseAddress, Int32($0.count), result, &status)
}
try status.checkSuccess()
}
init(formatter: OpaquePointer, value: String) throws {
var status = U_ZERO_ERROR
result = unumf_openResult(&status)
try status.checkSuccess()
var value = value
value.withUTF8 {
unumf_formatDecimal(formatter, $0.baseAddress, Int32($0.count), result, &status)
}
try status.checkSuccess()
}
deinit {
unumf_closeResult(result)
}
var string: String? {
return _withResizingUCharBuffer { buffer, size, status in
unumf_resultToString(result, buffer, size, &status)
}
}
}
}
// MARK: - Integer
final class ICUNumberFormatter : ICUNumberFormatterBase, @unchecked Sendable {
fileprivate struct Signature : Hashable {
let skeleton: String
let localeIdentifier: String
}
fileprivate static let cache = FormatterCache<Signature, ICUNumberFormatter?>()
private static func _create(with signature: Signature) -> ICUNumberFormatter? {
Self.cache.formatter(for: signature) {
.init(skeleton: signature.skeleton, localeIdentifier: signature.localeIdentifier)
}
}
static func create<T: BinaryInteger>(for style: IntegerFormatStyle<T>) -> ICUNumberFormatter? {
_create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences))
}
static func create(for style: Decimal.FormatStyle) -> ICUNumberFormatter? {
_create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences))
}
static func create<T: BinaryFloatingPoint>(for style: FloatingPointFormatStyle<T>) -> ICUNumberFormatter? {
_create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences))
}
func attributedFormat(_ v: Value) -> AttributedString {
guard let (str, attributes) = attributedFormatPositions(v) else {
return AttributedString(v.fallbackDescription)
}
return attributedStringFromPositions(attributes, string: str)
}
}
// MARK: - Currency
final class ICUCurrencyNumberFormatter : ICUNumberFormatterBase, @unchecked Sendable {
fileprivate struct Signature : Hashable {
let skeleton: String
let currencyCode: String
let localeIdentifier: String
}
private static func skeleton(for signature: Signature) -> String {
var s = "currency/\(signature.currencyCode)"
let stem = signature.skeleton
if stem.count > 0 {
s += " " + stem
}
return s
}
fileprivate static let cache = FormatterCache<Signature, ICUCurrencyNumberFormatter?>()
static private func _create(with signature: Signature) -> ICUCurrencyNumberFormatter? {
return Self.cache.formatter(for: signature) {
.init(skeleton: Self.skeleton(for: signature), localeIdentifier: signature.localeIdentifier)
}
}
static func create<T: BinaryInteger>(for style: IntegerFormatStyle<T>.Currency) -> ICUCurrencyNumberFormatter? {
_create(with: .init(skeleton: style.collection.skeleton, currencyCode: style.currencyCode, localeIdentifier: style.locale.identifierCapturingPreferences))
}
static func create(for style: Decimal.FormatStyle.Currency) -> ICUCurrencyNumberFormatter? {
_create(with: .init(skeleton: style.collection.skeleton, currencyCode: style.currencyCode, localeIdentifier: style.locale.identifierCapturingPreferences))
}
static func create<T: BinaryFloatingPoint>(for style: FloatingPointFormatStyle<T>.Currency) -> ICUCurrencyNumberFormatter? {
_create(with: .init(skeleton: style.collection.skeleton, currencyCode: style.currencyCode, localeIdentifier: style.locale.identifierCapturingPreferences))
}
func attributedFormat(_ v: Value) -> AttributedString {
guard let (str, attributes) = attributedFormatPositions(v) else {
return AttributedString(v.fallbackDescription)
}
return attributedStringFromPositions(attributes, string: str)
}
}
// MARK: - Integer Percent
final class ICUPercentNumberFormatter : ICUNumberFormatterBase, @unchecked Sendable {
fileprivate struct Signature : Hashable {
let skeleton: String
let localeIdentifier: String
}
private static func skeleton(for signature: Signature) -> String {
var s = "percent"
let stem = signature.skeleton
if stem.count > 0 {
s += " " + stem
}
return s
}
fileprivate static let cache = FormatterCache<Signature, ICUPercentNumberFormatter?>()
private static func _create(with signature: Signature) -> ICUPercentNumberFormatter? {
return Self.cache.formatter(for: signature) {
.init(skeleton: Self.skeleton(for: signature), localeIdentifier: signature.localeIdentifier)
}
}
static func create<T: BinaryInteger>(for style: IntegerFormatStyle<T>.Percent) -> ICUPercentNumberFormatter? {
_create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences))
}
static func create(for style: Decimal.FormatStyle.Percent) -> ICUPercentNumberFormatter? {
_create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences))
}
static func create<T: BinaryFloatingPoint>(for style: FloatingPointFormatStyle<T>.Percent) -> ICUPercentNumberFormatter? {
_create(with: .init(skeleton: style.collection.skeleton, localeIdentifier: style.locale.identifierCapturingPreferences))
}
func attributedFormat(_ v: Value) -> AttributedString {
guard let (str, attributes) = attributedFormatPositions(v) else {
return AttributedString(v.fallbackDescription)
}
return attributedStringFromPositions(attributes, string: str)
}
}
// MARK: - Byte Count
final class ICUByteCountNumberFormatter : ICUNumberFormatterBase, @unchecked Sendable {
fileprivate struct Signature : Hashable {
let skeleton: String
let localeIdentifier: String
}
fileprivate static let cache = FormatterCache<Signature, ICUByteCountNumberFormatter?>()
static func create(for skeleton: String, locale: Locale) -> ICUByteCountNumberFormatter? {
let signature = Signature(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences)
return Self.cache.formatter(for: signature) {
.init(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences)
}
}
func attributedFormat(_ v: Value, unit: ByteCountFormatStyle.Unit) -> AttributedString {
guard let (str, attributes) = attributedFormatPositions(v) else {
return AttributedString(v.fallbackDescription)
}
return attributedStringFromPositions(attributes, string: str, unit: unit)
}
private func attributedStringFromPositions(_ positions: [ICUNumberFormatter.AttributePosition], string: String, unit: ByteCountFormatStyle.Unit) -> AttributedString {
typealias NumberPartAttribute = AttributeScopes.FoundationAttributes.NumberFormatAttributes.NumberPartAttribute.NumberPart
typealias NumberSymbolAttribute = AttributeScopes.FoundationAttributes.NumberFormatAttributes.SymbolAttribute.Symbol
typealias ByteCountAttribute = AttributeScopes.FoundationAttributes.ByteCountAttribute.Component
var attrstr = AttributedString(string)
for attr in positions {
let strRange = String.Index(utf16Offset: attr.begin, in: string) ..<
String.Index(utf16Offset: attr.end, in: string)
let range = Range<AttributedString.Index>(strRange, in: attrstr)!
let field = attr.field
var container = AttributeContainer()
if let part = NumberPartAttribute(unumberFormatField: field) {
container.numberPart = part
}
if let symbol = NumberSymbolAttribute(unumberFormatField: field) {
container.numberSymbol = symbol
}
if let comp = ByteCountAttribute(unumberFormatField: field, unit: unit) {
container.byteCount = comp
}
attrstr[range].mergeAttributes(container)
}
return attrstr
}
}
// MARK: - Measurement
final class ICUMeasurementNumberFormatter : ICUNumberFormatterBase, @unchecked Sendable {
fileprivate struct Signature : Hashable {
let skeleton: String
let localeIdentifier: String
}
fileprivate static let cache = FormatterCache<Signature, ICUMeasurementNumberFormatter?>()
static func create(for skeleton: String, locale: Locale) -> ICUMeasurementNumberFormatter? {
let signature = Signature(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences)
return Self.cache.formatter(for: signature) {
.init(skeleton: skeleton, localeIdentifier: locale.identifierCapturingPreferences)
}
}
func attributedFormat(_ v: Value) -> AttributedString {
guard let (str, attributes) = attributedFormatPositions(v) else {
return AttributedString(v.fallbackDescription)
}
return attributedStringFromPositions(attributes, string: str)
}
/// Overrides superclass implementation to add the `MeasurementAttribute` property.
override func attributedStringFromPositions(_ positions: [ICUNumberFormatter.AttributePosition], string: String) -> AttributedString {
typealias NumberPartAttribute = AttributeScopes.FoundationAttributes.NumberFormatAttributes.NumberPartAttribute.NumberPart
typealias NumberSymbolAttribute = AttributeScopes.FoundationAttributes.NumberFormatAttributes.SymbolAttribute.Symbol
typealias MeasurementAttribute = AttributeScopes.FoundationAttributes.MeasurementAttribute.Component
var attrstr = AttributedString(string)
for attr in positions {
let strRange = String.Index(utf16Offset: attr.begin, in: string) ..<
String.Index(utf16Offset: attr.end, in: string)
let range = Range<AttributedString.Index>(strRange, in: attrstr)!
let field = attr.field
var container = AttributeContainer()
if let part = NumberPartAttribute(unumberFormatField: field) {
container.numberPart = part
}
if let symbol = NumberSymbolAttribute(unumberFormatField: field) {
container.numberSymbol = symbol
}
if let comp = MeasurementAttribute(unumberFormatField: field) {
container.measurement = comp
}
attrstr[range].mergeAttributes(container)
}
return attrstr
}
// The raw values are for use with ICU's API. They should match CLDR's declaration at https://github.com/unicode-org/cldr/blob/master/common/supplemental/units.xml
internal enum Usage: String {
// common
case general = "default"
case person
// energy
case food
// length
case personHeight = "person-height"
case road
case focalLength = "focal-length"
case rainfall
case snowfall
case visibility = "visiblty"
// pressure
case barometric = "baromtrc"
// speed
case wind
// temperature
case weather
// volume
case fluid
// Foundation's flag: Do not convert to preferred unit
case asProvided
}
enum UnitWidth: String, Codable {
case wide = "unit-width-full-name"
case abbreviated = "unit-width-short"
case narrow = "unit-width-narrow"
init(_ width: Duration.UnitsFormatStyle.UnitWidth) {
switch width.width.option {
case .wide:
self = .wide
case .abbreviated:
self = .abbreviated
case .narrow:
self = .narrow
}
}
}
static func skeleton(_ unitSkeleton: String?, width: UnitWidth, usage: Usage?, numberFormatStyle: FloatingPointFormatStyle<Double>?) -> String {
var stem = ""
if let unitSkeleton = unitSkeleton {
stem += unitSkeleton + " " + width.rawValue
if let usage {
// ICU handles the conversion when using the `usage` skeleton.
stem += " usage/" + usage.rawValue
}
}
if let numberFormatSkeleton = numberFormatStyle?.collection.skeleton {
if stem.count > 0 {
stem += " "
}
stem += numberFormatSkeleton
}
return stem
}
}