mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-26 08:01:20 +08:00
948 lines
32 KiB
Swift
948 lines
32 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2020 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
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public struct FormatStyleCapitalizationContext : Codable, Hashable, Sendable {
|
|
|
|
internal enum Option: Int, Codable, Hashable {
|
|
case unknown
|
|
case standalone
|
|
case listItem
|
|
case beginningOfSentence
|
|
case middleOfSentence
|
|
}
|
|
|
|
internal var option: Option
|
|
|
|
private init(_ option: Option) {
|
|
self.option = option
|
|
}
|
|
|
|
var icuContext: UDisplayContext {
|
|
switch self.option {
|
|
case .unknown:
|
|
return .unknown
|
|
case .standalone:
|
|
return .standalone
|
|
case .listItem:
|
|
return .listItem
|
|
case .beginningOfSentence:
|
|
return .beginningOfSentence
|
|
case .middleOfSentence:
|
|
return .middleOfSentence
|
|
}
|
|
}
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
var formatterContext: Formatter.Context {
|
|
switch option {
|
|
case .unknown:
|
|
return .unknown
|
|
case .standalone:
|
|
return .standalone
|
|
case .listItem:
|
|
return .listItem
|
|
case .beginningOfSentence:
|
|
return .beginningOfSentence
|
|
case .middleOfSentence:
|
|
return .middleOfSentence
|
|
}
|
|
}
|
|
#endif // FOUNDATION_FRAMEWORK
|
|
|
|
public static var unknown : FormatStyleCapitalizationContext {
|
|
.init(.unknown)
|
|
}
|
|
|
|
/// For stand-alone usage, such as an isolated name on a calendar page.
|
|
public static var standalone : FormatStyleCapitalizationContext {
|
|
.init(.standalone)
|
|
}
|
|
|
|
/// For use in a UI list or menu item.
|
|
public static var listItem : FormatStyleCapitalizationContext {
|
|
.init(.listItem)
|
|
}
|
|
|
|
public static var beginningOfSentence : FormatStyleCapitalizationContext {
|
|
.init(.beginningOfSentence)
|
|
}
|
|
|
|
public static var middleOfSentence : FormatStyleCapitalizationContext {
|
|
.init(.middleOfSentence)
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public enum NumberFormatStyleConfiguration {
|
|
internal struct Collection : Codable, Hashable, Sendable {
|
|
var scale: Double?
|
|
var precision: Precision?
|
|
var group: Grouping?
|
|
var signDisplayStrategy: SignDisplayStrategy?
|
|
var decimalSeparatorStrategy: DecimalSeparatorDisplayStrategy?
|
|
var rounding: RoundingRule?
|
|
var roundingIncrement: RoundingIncrement?
|
|
var notation: Notation?
|
|
}
|
|
|
|
public typealias RoundingRule = FloatingPointRoundingRule
|
|
|
|
typealias Scale = Double
|
|
|
|
public struct Grouping : Codable, Hashable, CustomStringConvertible, Sendable {
|
|
enum Option : Int, Codable, Hashable {
|
|
case automatic
|
|
case hidden
|
|
|
|
// unused
|
|
// case hiddenBelow10000 // UNUM_GROUPING_MIN2
|
|
// case alwaysGreaterThan1000 // UNUM_GROUPING_ON_ALIGNED
|
|
// case alwaysGroup3 // UNUM_GROUPING_THOUSANDS
|
|
}
|
|
var option: Option
|
|
|
|
public static var automatic: Self { .init(option: .automatic) }
|
|
public static var never: Self { .init(option: .hidden) }
|
|
|
|
public var description: String {
|
|
switch option {
|
|
case .automatic:
|
|
return "automatic"
|
|
case .hidden:
|
|
return "never"
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct Precision : Codable, Hashable, Sendable {
|
|
|
|
enum Option: Hashable {
|
|
case significantDigits(min: Int, max: Int?)
|
|
case integerAndFractionalLength(minInt: Int?, maxInt: Int?, minFraction: Int?, maxFraction: Int?)
|
|
}
|
|
var option: Option
|
|
|
|
// The maximum total length that ICU allows is 999.
|
|
// We take one off to reserve one character for the non-zero digit skeleton (the "0" skeleton in the number format)
|
|
static let validPartLength = 0..<999
|
|
static let validSignificantDigits = 1..<999
|
|
|
|
// min 3, max 3: 12345 -> 12300
|
|
// min 3, max 3: 0.12345 -> 0.123
|
|
// min 2, max 4: 3.14159 -> 3.142
|
|
// min 2, max 4: 1.23004 -> 1.23
|
|
// ^ Trailing zero digits to the right of the decimal separator are suppressed after the minimum number of significant digits have been shown
|
|
|
|
public static func significantDigits<R: RangeExpression>(_ limits: R) -> Self where R.Bound == Int {
|
|
let (lower, upper) = limits.clampedLowerAndUpperBounds(validSignificantDigits)
|
|
return Precision(option: .significantDigits(min: lower ?? validSignificantDigits.lowerBound, max: upper))
|
|
}
|
|
|
|
public static func significantDigits(_ digits: Int) -> Self {
|
|
return Precision(option: .significantDigits(min: digits, max: digits))
|
|
}
|
|
|
|
// maxInt 2 : 1997 -> 97
|
|
// minInt 5: 1997 -> 01997
|
|
// maxFrac 2: 0.125 -> 0.12
|
|
// minFrac 4: 0.125 -> 0.1250
|
|
public static func integerAndFractionLength<R1: RangeExpression, R2: RangeExpression>(integerLimits: R1, fractionLimits: R2) -> Self where R1.Bound == Int, R2.Bound == Int {
|
|
let (minInt, maxInt) = integerLimits.clampedLowerAndUpperBounds(validPartLength)
|
|
let (minFrac, maxFrac) = fractionLimits.clampedLowerAndUpperBounds(validPartLength)
|
|
|
|
return Precision(option: .integerAndFractionalLength(minInt: minInt, maxInt: maxInt, minFraction: minFrac, maxFraction: maxFrac))
|
|
}
|
|
|
|
public static func integerAndFractionLength(integer: Int, fraction: Int) -> Self {
|
|
return Precision(option: .integerAndFractionalLength(minInt: integer, maxInt: integer, minFraction: fraction, maxFraction: fraction))
|
|
}
|
|
|
|
public static func integerLength<R: RangeExpression>(_ limits: R) -> Self {
|
|
let (minInt, maxInt) = limits.clampedLowerAndUpperBounds(validPartLength)
|
|
return Precision(option: .integerAndFractionalLength(minInt: minInt, maxInt: maxInt, minFraction: nil, maxFraction: nil))
|
|
}
|
|
|
|
public static func integerLength(_ length: Int) -> Self {
|
|
return Precision(option: .integerAndFractionalLength(minInt: length, maxInt: length, minFraction: nil, maxFraction: nil))
|
|
}
|
|
|
|
public static func fractionLength<R: RangeExpression>(_ limits: R) -> Self where R.Bound == Int {
|
|
let (minFrac, maxFrac) = limits.clampedLowerAndUpperBounds(validPartLength)
|
|
return Precision(option: .integerAndFractionalLength(minInt: nil, maxInt: nil, minFraction: minFrac, maxFraction: maxFrac))
|
|
}
|
|
|
|
public static func fractionLength(_ length: Int) -> Self {
|
|
return Precision(option: .integerAndFractionalLength(minInt: nil, maxInt: nil, minFraction: length, maxFraction: length))
|
|
}
|
|
}
|
|
|
|
public struct DecimalSeparatorDisplayStrategy : Codable, Hashable, CustomStringConvertible, Sendable {
|
|
enum Option : Int, Codable, Hashable {
|
|
case automatic
|
|
case always
|
|
}
|
|
var option: Option
|
|
|
|
// "1.1", "1"
|
|
public static var automatic: Self {
|
|
.init(option: .automatic)
|
|
}
|
|
|
|
// "1.1", "1."
|
|
public static var always : Self { .init(option: .always) }
|
|
|
|
public var description: String {
|
|
switch option {
|
|
case .automatic:
|
|
return "automatic"
|
|
case .always:
|
|
return "always"
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct SignDisplayStrategy : Codable, Hashable, CustomStringConvertible, Sendable {
|
|
enum Option : Int, Hashable, Codable {
|
|
case always
|
|
case hidden
|
|
}
|
|
|
|
var positive: Option
|
|
var negative: Option
|
|
var zero: Option
|
|
|
|
// Show the minus sign on negative numbers, and do not show the sign on positive numbers or zero
|
|
public static var automatic: Self {
|
|
SignDisplayStrategy(positive: .hidden, negative: .always, zero: .hidden)
|
|
}
|
|
|
|
public static var never: Self {
|
|
SignDisplayStrategy(positive: .hidden, negative: .hidden, zero: .hidden)
|
|
}
|
|
|
|
// Show the minus sign on negative numbers and the plus sign on positive numbers, and zero if specified
|
|
public static func always(includingZero: Bool = true) -> Self {
|
|
SignDisplayStrategy(positive: .always, negative: .always, zero: includingZero ? .always : .hidden)
|
|
}
|
|
|
|
public var description: String {
|
|
switch positive {
|
|
case .always:
|
|
switch zero {
|
|
case .always:
|
|
return "always(includingZero: true)"
|
|
case .hidden:
|
|
return "always(includingZero: false)"
|
|
}
|
|
case .hidden:
|
|
switch negative {
|
|
case .always:
|
|
return "automatic"
|
|
case .hidden:
|
|
return "never"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct Notation : Codable, Hashable, CustomStringConvertible, Sendable {
|
|
enum Option : Int, Codable, Hashable {
|
|
case automatic
|
|
case scientific
|
|
case compactName
|
|
}
|
|
var option: Option
|
|
|
|
public static var scientific: Self { .init(option: .scientific) }
|
|
public static var automatic: Self { .init(option: .automatic) }
|
|
|
|
/// Formats the number with localized prefixes or suffixes corresponding to powers of ten. Rounds to integer while showing at least two significant digits by default.
|
|
/// For example, "42.3K" for 42300 for the "en_US" locale.
|
|
/// - note: We do not support parsing a number string containing localized prefixes or suffixes.
|
|
public static var compactName: Self { .init(option: .compactName) }
|
|
|
|
public var description: String {
|
|
switch option {
|
|
case .scientific:
|
|
return "scientific"
|
|
case .automatic:
|
|
return "automatic"
|
|
case .compactName:
|
|
return "compact name"
|
|
}
|
|
}
|
|
}
|
|
|
|
internal enum RoundingIncrement: Hashable, CustomStringConvertible {
|
|
case integer(value: Int)
|
|
case floatingPoint(value: Double)
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .integer(let value):
|
|
return String(value)
|
|
case .floatingPoint(let value):
|
|
return String(value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS, unavailable, introduced: 12.0)
|
|
@available(iOS, unavailable, introduced: 15.0)
|
|
@available(tvOS, unavailable, introduced: 15.0)
|
|
@available(watchOS, unavailable, introduced: 8.0)
|
|
@available(*, unavailable)
|
|
extension NumberFormatStyleConfiguration : Sendable {}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public enum CurrencyFormatStyleConfiguration {
|
|
public typealias Grouping = NumberFormatStyleConfiguration.Grouping
|
|
public typealias Precision = NumberFormatStyleConfiguration.Precision
|
|
public typealias DecimalSeparatorDisplayStrategy = NumberFormatStyleConfiguration.DecimalSeparatorDisplayStrategy
|
|
public typealias RoundingRule = NumberFormatStyleConfiguration.RoundingRule
|
|
/// The type used to configure notation for currency format styles.
|
|
@available(FoundationPreview 0.4, *)
|
|
public typealias Notation = NumberFormatStyleConfiguration.Notation
|
|
|
|
internal typealias RoundingIncrement = NumberFormatStyleConfiguration.RoundingIncrement
|
|
internal struct Collection : Codable, Hashable {
|
|
var scale: Double?
|
|
var precision: Precision?
|
|
var group: Grouping?
|
|
var signDisplayStrategy: SignDisplayStrategy?
|
|
var decimalSeparatorStrategy: DecimalSeparatorDisplayStrategy?
|
|
var rounding: RoundingRule?
|
|
var roundingIncrement: RoundingIncrement?
|
|
var presentation: Presentation
|
|
var notation: Notation?
|
|
}
|
|
|
|
public struct SignDisplayStrategy: Codable, Hashable, Sendable {
|
|
enum Option : Int, Hashable, Codable {
|
|
case always
|
|
case hidden
|
|
}
|
|
var positive: Option
|
|
var negative: Option
|
|
var zero: Option
|
|
var accounting: Bool = false
|
|
|
|
public static var automatic: Self {
|
|
SignDisplayStrategy(positive: .hidden, negative: .always, zero: .hidden)
|
|
}
|
|
|
|
public static var never: Self {
|
|
SignDisplayStrategy(positive: .hidden, negative: .hidden, zero: .hidden)
|
|
}
|
|
|
|
// Show the minus sign on negative numbers and the plus sign on positive numbers, and zero if specified
|
|
public static func always(showZero: Bool = true) -> Self {
|
|
SignDisplayStrategy(positive: .always, negative: .always, zero: showZero ? .always : .hidden)
|
|
}
|
|
|
|
public static var accounting: Self {
|
|
SignDisplayStrategy(positive: .hidden, negative: .always, zero: .hidden, accounting: true)
|
|
}
|
|
|
|
public static func accountingAlways(showZero: Bool = false) -> Self {
|
|
SignDisplayStrategy(positive: .always, negative: .always, zero: showZero ? .always : .hidden, accounting: true)
|
|
}
|
|
}
|
|
|
|
public struct Presentation: Codable, Hashable, Sendable {
|
|
enum Option : Int, Codable, Hashable {
|
|
case narrow
|
|
case standard
|
|
case isoCode
|
|
case fullName
|
|
}
|
|
internal var option: Option
|
|
|
|
public static var narrow: Self { Presentation(option: .narrow) }
|
|
public static var standard: Self { Presentation(option: .standard) }
|
|
public static var isoCode: Self { Presentation(option: .isoCode) }
|
|
public static var fullName: Self { Presentation(option: .fullName) }
|
|
}
|
|
}
|
|
|
|
@available(macOS, unavailable, introduced: 12.0)
|
|
@available(iOS, unavailable, introduced: 15.0)
|
|
@available(tvOS, unavailable, introduced: 15.0)
|
|
@available(watchOS, unavailable, introduced: 8.0)
|
|
@available(*, unavailable)
|
|
extension CurrencyFormatStyleConfiguration : Sendable {}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public enum DescriptiveNumberFormatConfiguration {
|
|
public typealias CapitalizationContext = FormatStyleCapitalizationContext
|
|
public struct Presentation : Codable, Hashable, Sendable {
|
|
internal enum Option : Int, Codable, Hashable {
|
|
case spellOut = 1
|
|
case ordinal = 2
|
|
case cardinal = 3
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
fileprivate var numberFormatterStyle : NumberFormatter.Style {
|
|
switch self {
|
|
case .spellOut:
|
|
return .spellOut
|
|
case .ordinal:
|
|
return .ordinal
|
|
case .cardinal:
|
|
return .spellOut // cardinal is a special case spellout style
|
|
}
|
|
}
|
|
#endif // FOUNDATION_FRAMEWORK
|
|
}
|
|
internal var option: Option
|
|
|
|
public static var spellOut: Self { Presentation(rawValue: 1) }
|
|
public static var ordinal: Self { Presentation(rawValue: 2) }
|
|
internal static var cardinal: Self { Presentation(rawValue: 3) }
|
|
|
|
internal init(rawValue: Int) {
|
|
option = Option(rawValue: rawValue)!
|
|
}
|
|
}
|
|
|
|
internal struct Collection : Codable, Hashable {
|
|
var presentation: Presentation
|
|
var capitalizationContext: CapitalizationContext?
|
|
|
|
var icuNumberFormatStyle: UNumberFormatStyle {
|
|
switch presentation.option {
|
|
case .spellOut:
|
|
return .spellout
|
|
case .ordinal:
|
|
return .ordinal
|
|
case .cardinal:
|
|
return .spellout // cardinal is a special case spellout stype
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS, unavailable, introduced: 12.0)
|
|
@available(iOS, unavailable, introduced: 15.0)
|
|
@available(tvOS, unavailable, introduced: 15.0)
|
|
@available(watchOS, unavailable, introduced: 8.0)
|
|
@available(*, unavailable)
|
|
extension DescriptiveNumberFormatConfiguration : Sendable {}
|
|
|
|
// MARK: - Codable
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension FloatingPointRoundingRule {
|
|
private enum CodingValue: Int, Codable {
|
|
case toNearestOrAwayFromZero
|
|
case toNearestOrEven
|
|
case up
|
|
case down
|
|
case towardZero
|
|
case awayFromZero
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
let value = try container.decode(CodingValue.self)
|
|
switch value {
|
|
case .toNearestOrAwayFromZero:
|
|
self = .toNearestOrAwayFromZero
|
|
case .toNearestOrEven:
|
|
self = .toNearestOrEven
|
|
case .up:
|
|
self = .up
|
|
case .down:
|
|
self = .down
|
|
case .towardZero:
|
|
self = .towardZero
|
|
case .awayFromZero:
|
|
self = .awayFromZero
|
|
}
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
let codingValue: CodingValue
|
|
switch self {
|
|
case .toNearestOrAwayFromZero:
|
|
codingValue = .toNearestOrAwayFromZero
|
|
case .toNearestOrEven:
|
|
codingValue = .toNearestOrEven
|
|
case .up:
|
|
codingValue = .up
|
|
case .down:
|
|
codingValue = .down
|
|
case .towardZero:
|
|
codingValue = .towardZero
|
|
case .awayFromZero:
|
|
codingValue = .awayFromZero
|
|
@unknown default:
|
|
codingValue = .toNearestOrEven
|
|
}
|
|
var container = encoder.singleValueContainer()
|
|
try container.encode(codingValue)
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension FloatingPointRoundingRule : Codable { }
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.RoundingIncrement: Codable {
|
|
private enum CodingKeys: CodingKey {
|
|
case integer
|
|
case floatingPoint
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
if let value = try container.decodeIfPresent(Int.self, forKey: .integer) {
|
|
self = .integer(value: value)
|
|
} else if let value = try container.decodeIfPresent(Double.self, forKey: .floatingPoint) {
|
|
self = .floatingPoint(value: value)
|
|
} else {
|
|
self = .floatingPoint(value: 0.5)
|
|
}
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
switch self {
|
|
case .integer(let value):
|
|
try container.encode(value, forKey: .integer)
|
|
case .floatingPoint(let value):
|
|
try container.encode(value, forKey: .floatingPoint)
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.Precision.Option : Codable {
|
|
private enum CodingKeys: CodingKey {
|
|
case minSignificantDigits
|
|
case maxSignificantDigits
|
|
case minIntegerLength
|
|
case maxIntegerLength
|
|
case minFractionalLength
|
|
case maxFractionalLength
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
if let minSignificantDigits = try container.decodeIfPresent(Int.self, forKey: .minSignificantDigits), let maxSignificantDigits = try container.decodeIfPresent(Int.self, forKey: .maxSignificantDigits) {
|
|
self = .significantDigits(min: minSignificantDigits, max: maxSignificantDigits)
|
|
} else if let minInt = try container.decodeIfPresent(Int.self, forKey: .minIntegerLength), let maxInt = try container.decodeIfPresent(Int.self, forKey: .maxIntegerLength), let minFrac = try container.decodeIfPresent(Int.self, forKey: .minFractionalLength), let maxFrac = try container.decodeIfPresent(Int.self, forKey: .maxFractionalLength) {
|
|
self = .integerAndFractionalLength(minInt: minInt, maxInt: maxInt, minFraction: minFrac, maxFraction: maxFrac)
|
|
} else {
|
|
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath,
|
|
debugDescription: "Invalid Precision"))
|
|
}
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
switch self {
|
|
case .significantDigits(let min, let max):
|
|
try container.encode(min, forKey: .minSignificantDigits)
|
|
try container.encode(max, forKey: .maxSignificantDigits)
|
|
case .integerAndFractionalLength(let minInt, let maxInt, let minFraction, let maxFraction):
|
|
try container.encode(minInt, forKey: .minIntegerLength)
|
|
try container.encode(maxInt, forKey: .maxIntegerLength)
|
|
try container.encode(minFraction, forKey: .minFractionalLength)
|
|
try container.encode(maxFraction, forKey: .maxFractionalLength)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: - ICU compatibility: NumberFormatStyleConfiguration
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.Collection {
|
|
var skeleton: String {
|
|
var s = ""
|
|
if let scale = scale {
|
|
s += scale.skeleton + " "
|
|
}
|
|
if let precision = precision, let roundingIncrement = roundingIncrement {
|
|
s += precision.skeletonWithRoundingIncrement(stem: roundingIncrement.skeleton) + " "
|
|
} else if let precision = precision {
|
|
s += precision.skeleton + " "
|
|
} else if let roundingIncrement = roundingIncrement {
|
|
s += roundingIncrement.skeleton + " "
|
|
}
|
|
|
|
if let group = group {
|
|
s += group.skeleton + " "
|
|
}
|
|
if let signDisplayStrategy = signDisplayStrategy {
|
|
s += signDisplayStrategy.skeleton + " "
|
|
}
|
|
if let decimalSeparatorStrategy = decimalSeparatorStrategy {
|
|
s += decimalSeparatorStrategy.skeleton + " "
|
|
}
|
|
if let rounding = rounding {
|
|
s += rounding.skeleton + " "
|
|
}
|
|
if let notation = notation {
|
|
s += notation.skeleton + " "
|
|
}
|
|
|
|
return s._trimmingWhitespace()
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.RoundingIncrement {
|
|
var skeleton: String {
|
|
switch self {
|
|
// ICU treats any value <= 0 as invalid. Fallback to the default behavior if that's the case.
|
|
case .integer(let value):
|
|
if value > 0 {
|
|
return "precision-increment/\(Decimal(value))"
|
|
} else {
|
|
return ""
|
|
}
|
|
case .floatingPoint(let value):
|
|
if value > 0 {
|
|
return "precision-increment/\(Decimal(value))"
|
|
} else {
|
|
return ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.Precision {
|
|
|
|
private func significantDigitsSkeleton(min: Int, max: Int?) -> String {
|
|
var stem: String = ""
|
|
stem += String(repeating: "@", count: min)
|
|
|
|
if let max = max {
|
|
stem += String(repeating: "#", count: max - min)
|
|
} else {
|
|
stem += "+"
|
|
}
|
|
return stem
|
|
}
|
|
|
|
private func integerAndFractionalLengthSkeleton(minInt: Int?, maxInt: Int?, minFrac: Int?, maxFrac: Int?) -> String {
|
|
var stem: String = ""
|
|
// Construct skeleton for fractional part
|
|
if minFrac != nil || maxFrac != nil {
|
|
if maxFrac == 0 {
|
|
stem += "precision-integer"
|
|
} else {
|
|
stem += fractionalStem(min: minFrac ?? 0, max: maxFrac)
|
|
}
|
|
}
|
|
|
|
// Construct skeleton for integer part
|
|
if minInt != nil || maxInt != nil {
|
|
if stem.count > 0 {
|
|
stem += " "
|
|
}
|
|
stem += integerStem(min: minInt ?? 0, max: maxInt)
|
|
}
|
|
|
|
return stem
|
|
}
|
|
|
|
func skeletonWithRoundingIncrement(stem: String) -> String {
|
|
guard stem.count > 0 else { return self.skeleton }
|
|
|
|
var incrementStem = stem
|
|
switch self.option {
|
|
case .significantDigits(_, _):
|
|
// Specifying rounding increment hides the effect of significant digits
|
|
break
|
|
case .integerAndFractionalLength(let minInt, let maxInt, let minFrac, _):
|
|
if let minFrac = minFrac {
|
|
if let decimalPoint = incrementStem.lastIndex(of: ".") {
|
|
let frac = incrementStem.suffix(from: incrementStem.index(after: decimalPoint))
|
|
if minFrac > frac.count {
|
|
incrementStem += String(repeating: "0", count: minFrac - frac.count)
|
|
}
|
|
} else {
|
|
incrementStem += "." + String(repeating: "0", count: minFrac)
|
|
}
|
|
}
|
|
if minInt != nil || maxInt != nil {
|
|
incrementStem += " " + integerStem(min: minInt ?? 0, max: maxInt)
|
|
}
|
|
}
|
|
return incrementStem
|
|
}
|
|
|
|
var skeleton : String {
|
|
switch self.option {
|
|
case .significantDigits(let min, let max):
|
|
return significantDigitsSkeleton(min: min, max: max)
|
|
case .integerAndFractionalLength(let minInt, let maxInt, let minFrac, let maxFrac):
|
|
return integerAndFractionalLengthSkeleton(minInt: minInt, maxInt: maxInt, minFrac: minFrac, maxFrac: maxFrac)
|
|
}
|
|
}
|
|
|
|
private func integerStem(min: Int, max: Int?) -> String {
|
|
var s = "integer-width/"
|
|
if max == 0 && min == 0 {
|
|
s += "*" // 75459602
|
|
return s
|
|
}
|
|
|
|
if let max = max {
|
|
guard max >= min else { return "" }
|
|
s += String(repeating: "#", count: max - min)
|
|
} else {
|
|
s += "+"
|
|
}
|
|
|
|
s += String(repeating: "0", count: min)
|
|
|
|
return s
|
|
}
|
|
|
|
private func fractionalStem(min: Int, max: Int?) -> String {
|
|
var s = "."
|
|
s += String(repeating: "0", count: min)
|
|
|
|
if let max = max {
|
|
guard max >= min else { return "" }
|
|
s += String(repeating: "#", count: max - min)
|
|
} else {
|
|
s += "+"
|
|
}
|
|
|
|
return s
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.Grouping {
|
|
var skeleton : String {
|
|
switch self.option {
|
|
case .automatic:
|
|
// This is the default, so no need to set it
|
|
return ""
|
|
case .hidden:
|
|
return "group-off"
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.SignDisplayStrategy {
|
|
var skeleton : String {
|
|
let stem: String
|
|
switch positive {
|
|
case .always:
|
|
switch zero {
|
|
case .always:
|
|
stem = "sign-always"
|
|
case .hidden:
|
|
stem = "sign-except-zero"
|
|
}
|
|
case .hidden:
|
|
switch negative {
|
|
case .always:
|
|
stem = "sign-auto"
|
|
case .hidden:
|
|
stem = "sign-never"
|
|
}
|
|
}
|
|
return stem
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.DecimalSeparatorDisplayStrategy {
|
|
var skeleton : String {
|
|
let stem: String
|
|
switch option {
|
|
case .always:
|
|
stem = "decimal-always"
|
|
case .automatic:
|
|
stem = "decimal-auto"
|
|
}
|
|
return stem
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.RoundingRule {
|
|
var skeleton : String {
|
|
var stem: String
|
|
switch self {
|
|
case .awayFromZero:
|
|
stem = "rounding-mode-up"
|
|
case .toNearestOrAwayFromZero:
|
|
stem = "rounding-mode-half-up"
|
|
case .toNearestOrEven:
|
|
stem = "rounding-mode-half-even"
|
|
case .up:
|
|
stem = "rounding-mode-ceiling"
|
|
case .down:
|
|
stem = "rounding-mode-floor"
|
|
case .towardZero:
|
|
stem = "rounding-mode-down"
|
|
@unknown default:
|
|
stem = ""
|
|
}
|
|
return stem
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.Notation {
|
|
var skeleton : String {
|
|
let stem: String
|
|
switch self.option {
|
|
case .scientific:
|
|
stem = "scientific"
|
|
case .automatic:
|
|
stem = ""
|
|
case .compactName:
|
|
stem = "compact-short"
|
|
}
|
|
return stem
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension NumberFormatStyleConfiguration.Scale {
|
|
var skeleton : String {
|
|
return "scale/\(Decimal(self))"
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: - ICU compatibility: CurrencyFormatStyleConfiguration
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension CurrencyFormatStyleConfiguration.Collection {
|
|
var skeleton: String {
|
|
var s = presentation.skeleton + " "
|
|
|
|
if let scale = scale {
|
|
s += scale.skeleton + " "
|
|
}
|
|
if let precision = precision, let roundingIncrement = roundingIncrement {
|
|
s += precision.skeletonWithRoundingIncrement(stem: roundingIncrement.skeleton)
|
|
} else if let precision = precision {
|
|
s += precision.skeleton + " "
|
|
} else if let roundingIncrement = roundingIncrement {
|
|
s += roundingIncrement.skeleton + " "
|
|
}
|
|
if let group = group {
|
|
s += group.skeleton
|
|
}
|
|
if let signDisplayStrategy = signDisplayStrategy {
|
|
s += signDisplayStrategy.skeleton + " "
|
|
}
|
|
if let decimalSeparatorStrategy = decimalSeparatorStrategy {
|
|
s += decimalSeparatorStrategy.skeleton + " "
|
|
}
|
|
if let rounding = rounding {
|
|
s += rounding.skeleton + " "
|
|
}
|
|
if let notation = notation {
|
|
s += notation.skeleton + " "
|
|
}
|
|
|
|
return s._trimmingWhitespace()
|
|
}
|
|
|
|
var icuNumberFormatStyle: UNumberFormatStyle {
|
|
if signDisplayStrategy?.accounting == true {
|
|
return .currencyAccounting
|
|
}
|
|
switch presentation.option {
|
|
case .narrow:
|
|
return .currencyNarrow
|
|
case .standard:
|
|
return .currencyStandard
|
|
case .isoCode:
|
|
return .currencyISO
|
|
case .fullName:
|
|
return .currencyFullName
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension CurrencyFormatStyleConfiguration.SignDisplayStrategy {
|
|
var skeleton: String {
|
|
var stem: String
|
|
if accounting {
|
|
switch positive {
|
|
case .always:
|
|
switch zero {
|
|
case .always:
|
|
stem = "sign-accounting-always"
|
|
case .hidden:
|
|
stem = "sign-accounting-except-zero"
|
|
}
|
|
case .hidden:
|
|
stem = "sign-accounting"
|
|
}
|
|
} else {
|
|
switch positive {
|
|
case .always:
|
|
switch zero {
|
|
case .always:
|
|
stem = "sign-always"
|
|
case .hidden:
|
|
stem = "sign-except-zero"
|
|
}
|
|
case .hidden:
|
|
switch negative {
|
|
case .always:
|
|
stem = "sign-auto"
|
|
case .hidden:
|
|
stem = "sign-never"
|
|
}
|
|
}
|
|
}
|
|
|
|
return stem
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension CurrencyFormatStyleConfiguration.Presentation {
|
|
var skeleton: String {
|
|
var stem: String
|
|
switch option {
|
|
case .narrow:
|
|
stem = "unit-width-narrow"
|
|
case .standard:
|
|
stem = "unit-width-short"
|
|
case .isoCode:
|
|
stem = "unit-width-iso-code"
|
|
case .fullName:
|
|
stem = "unit-width-full-name"
|
|
}
|
|
return stem
|
|
}
|
|
}
|