mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-23 14:00:14 +08:00
451 lines
16 KiB
Swift
451 lines
16 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
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal {
|
|
public struct FormatStyle: Sendable {
|
|
public var locale: Locale
|
|
public init(locale: Locale = .autoupdatingCurrent) {
|
|
self.locale = locale
|
|
}
|
|
|
|
public var attributed: Attributed {
|
|
return Attributed(style: self)
|
|
}
|
|
|
|
public typealias Configuration = NumberFormatStyleConfiguration
|
|
internal var collection: Configuration.Collection = Configuration.Collection()
|
|
|
|
public func grouping(_ group: Configuration.Grouping) -> Self {
|
|
var new = self
|
|
new.collection.group = group
|
|
return new
|
|
}
|
|
|
|
public func precision(_ p: Configuration.Precision) -> Self {
|
|
var new = self
|
|
new.collection.precision = p
|
|
return new
|
|
}
|
|
|
|
public func sign(strategy: Configuration.SignDisplayStrategy) -> Self {
|
|
var new = self
|
|
new.collection.signDisplayStrategy = strategy
|
|
return new
|
|
}
|
|
|
|
public func decimalSeparator(strategy: Configuration.DecimalSeparatorDisplayStrategy) -> Self {
|
|
var new = self
|
|
new.collection.decimalSeparatorStrategy = strategy
|
|
return new
|
|
}
|
|
|
|
public func rounded(rule: Configuration.RoundingRule = .toNearestOrEven, increment: Int? = nil) -> Self {
|
|
var new = self
|
|
new.collection.rounding = rule
|
|
if let increment = increment {
|
|
new.collection.roundingIncrement = .integer(value: increment)
|
|
}
|
|
return new
|
|
}
|
|
|
|
public func scale(_ multiplicand: Double) -> Self {
|
|
var new = self
|
|
new.collection.scale = multiplicand
|
|
return new
|
|
}
|
|
|
|
public func notation(_ notation: Configuration.Notation) -> Self {
|
|
var new = self
|
|
new.collection.notation = notation
|
|
return new
|
|
}
|
|
|
|
// FormatStyle
|
|
public func format(_ value: Decimal) -> String {
|
|
if let f = ICUNumberFormatter.create(for: self), let res = f.format(value) {
|
|
return res
|
|
}
|
|
|
|
return value.description
|
|
}
|
|
|
|
public func locale(_ locale: Locale) -> Self {
|
|
var new = self
|
|
new.locale = locale
|
|
return new
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal.FormatStyle : FormatStyle {}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal.FormatStyle {
|
|
public struct Percent : Sendable {
|
|
public typealias Configuration = NumberFormatStyleConfiguration
|
|
|
|
public var locale: Locale
|
|
// Set scale to 100 so we format 0.42 as "42%" instead of "0.42%"
|
|
var collection: Configuration.Collection = Configuration.Collection(scale: 100)
|
|
|
|
public init(locale: Locale = .autoupdatingCurrent) {
|
|
self.locale = locale
|
|
}
|
|
|
|
public var attributed: Attributed {
|
|
return Attributed(style: self)
|
|
}
|
|
|
|
public func grouping(_ group: Configuration.Grouping) -> Self {
|
|
var new = self
|
|
new.collection.group = group
|
|
return new
|
|
}
|
|
|
|
public func precision(_ p: Configuration.Precision) -> Self {
|
|
var new = self
|
|
new.collection.precision = p
|
|
return new
|
|
}
|
|
|
|
public func sign(strategy: Configuration.SignDisplayStrategy) -> Self {
|
|
var new = self
|
|
new.collection.signDisplayStrategy = strategy
|
|
return new
|
|
}
|
|
|
|
public func decimalSeparator(strategy: Configuration.DecimalSeparatorDisplayStrategy) -> Self {
|
|
var new = self
|
|
new.collection.decimalSeparatorStrategy = strategy
|
|
return new
|
|
}
|
|
|
|
public func rounded(rule: Configuration.RoundingRule = .toNearestOrEven, increment: Int? = nil) -> Self {
|
|
var new = self
|
|
new.collection.rounding = rule
|
|
if let increment = increment {
|
|
new.collection.roundingIncrement = .integer(value: increment)
|
|
}
|
|
return new
|
|
}
|
|
|
|
public func scale(_ multiplicand: Double) -> Self {
|
|
var new = self
|
|
new.collection.scale = multiplicand
|
|
return new
|
|
}
|
|
|
|
public func notation(_ notation: Configuration.Notation) -> Self {
|
|
var new = self
|
|
new.collection.notation = notation
|
|
return new
|
|
}
|
|
|
|
// FormatStyle
|
|
public func format(_ value: Decimal) -> String {
|
|
if let f = ICUPercentNumberFormatter.create(for: self), let res = f.format(value) {
|
|
return res
|
|
}
|
|
|
|
return value.description
|
|
}
|
|
|
|
public func locale(_ locale: Locale) -> Self {
|
|
var new = self
|
|
new.locale = locale
|
|
return new
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public struct Currency : Sendable {
|
|
public typealias Configuration = CurrencyFormatStyleConfiguration
|
|
|
|
public var locale: Locale
|
|
public var currencyCode: String
|
|
|
|
internal var collection: Configuration.Collection
|
|
public init(code: String, locale: Locale = .autoupdatingCurrent) {
|
|
self.locale = locale
|
|
self.currencyCode = code
|
|
self.collection = Configuration.Collection(presentation: .standard)
|
|
}
|
|
|
|
public var attributed: Attributed {
|
|
return Attributed(style: self)
|
|
}
|
|
|
|
public func grouping(_ group: Configuration.Grouping) -> Self {
|
|
var new = self
|
|
new.collection.group = group
|
|
return new
|
|
}
|
|
|
|
public func precision(_ p: Configuration.Precision) -> Self {
|
|
var new = self
|
|
new.collection.precision = p
|
|
return new
|
|
}
|
|
|
|
public func sign(strategy: Configuration.SignDisplayStrategy) -> Self {
|
|
var new = self
|
|
new.collection.signDisplayStrategy = strategy
|
|
return new
|
|
}
|
|
|
|
public func decimalSeparator(strategy: Configuration.DecimalSeparatorDisplayStrategy) -> Self {
|
|
var new = self
|
|
new.collection.decimalSeparatorStrategy = strategy
|
|
return new
|
|
}
|
|
|
|
public func rounded(rule: Configuration.RoundingRule = .toNearestOrEven, increment: Int? = nil) -> Self {
|
|
var new = self
|
|
new.collection.rounding = rule
|
|
if let increment = increment {
|
|
new.collection.roundingIncrement = .integer(value: increment)
|
|
}
|
|
return new
|
|
}
|
|
|
|
public func scale(_ multiplicand: Double) -> Self {
|
|
var new = self
|
|
new.collection.scale = multiplicand
|
|
return new
|
|
}
|
|
|
|
public func presentation(_ p: Configuration.Presentation) -> Self {
|
|
var new = self
|
|
new.collection.presentation = p
|
|
return new
|
|
}
|
|
|
|
/// Modifies the format style to use the specified notation.
|
|
///
|
|
/// - Parameter notation: The notation to apply to the format style.
|
|
/// - Returns: A decimal currency format style modified to use the specified notation.
|
|
@available(FoundationPreview 0.4, *)
|
|
public func notation(_ notation: Configuration.Notation) -> Self {
|
|
var new = self
|
|
new.collection.notation = notation
|
|
return new
|
|
}
|
|
|
|
// FormatStyle
|
|
public func format(_ value: Decimal) -> String {
|
|
if let f = ICUCurrencyNumberFormatter.create(for: self), let res = f.format(value) {
|
|
return res
|
|
}
|
|
|
|
return value.description
|
|
}
|
|
|
|
public func locale(_ locale: Locale) -> Self {
|
|
var new = self
|
|
new.locale = locale
|
|
return new
|
|
}
|
|
}
|
|
|
|
public struct Attributed : Sendable {
|
|
enum Style : Hashable, Codable, Sendable {
|
|
case decimal(Decimal.FormatStyle)
|
|
case currency(Decimal.FormatStyle.Currency)
|
|
case percent(Decimal.FormatStyle.Percent)
|
|
}
|
|
|
|
var style: Style
|
|
|
|
init(style: Decimal.FormatStyle) {
|
|
self.style = .decimal(style)
|
|
}
|
|
|
|
init(style: Decimal.FormatStyle.Currency) {
|
|
self.style = .currency(style)
|
|
}
|
|
|
|
init(style: Decimal.FormatStyle.Percent) {
|
|
self.style = .percent(style)
|
|
}
|
|
|
|
/// Returns an attributed string with `NumberFormatAttributes.SymbolAttribute` and `NumberFormatAttributes.NumberPartAttribute`.
|
|
public func format(_ value: Decimal) -> AttributedString {
|
|
switch style {
|
|
case .decimal(let formatStyle):
|
|
if let formatter = ICUNumberFormatter.create(for: formatStyle) {
|
|
return formatter.attributedFormat(.decimal(value))
|
|
}
|
|
case .currency(let formatStyle):
|
|
if let formatter = ICUCurrencyNumberFormatter.create(for: formatStyle) {
|
|
return formatter.attributedFormat(.decimal(value))
|
|
|
|
}
|
|
case .percent(let formatStyle):
|
|
if let formatter = ICUPercentNumberFormatter.create(for: formatStyle) {
|
|
return formatter.attributedFormat(.decimal(value))
|
|
|
|
}
|
|
}
|
|
|
|
// Fallback
|
|
return AttributedString(value.description)
|
|
}
|
|
|
|
public func locale(_ locale: Locale) -> Self {
|
|
var new = self
|
|
switch style {
|
|
case .decimal(var s):
|
|
s.locale = locale
|
|
new.style = .decimal(s)
|
|
case .currency(var s):
|
|
s.locale = locale
|
|
new.style = .currency(s)
|
|
case .percent(var s):
|
|
s.locale = locale
|
|
new.style = .percent(s)
|
|
}
|
|
return new
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal.FormatStyle.Percent : FormatStyle {}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal.FormatStyle.Currency : FormatStyle {}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal.FormatStyle.Attributed : FormatStyle {}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal.FormatStyle: ParseableFormatStyle {
|
|
public var parseStrategy: Decimal.ParseStrategy<Self> { .init(formatStyle: self, lenient: true) }
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal.FormatStyle.Currency: ParseableFormatStyle {
|
|
public var parseStrategy: Decimal.ParseStrategy<Self> { .init(formatStyle: self, lenient: true) }
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal.FormatStyle.Percent: ParseableFormatStyle {
|
|
public var parseStrategy: Decimal.ParseStrategy<Self> { .init(formatStyle: self, lenient: true) }
|
|
}
|
|
|
|
// MARK: - FormatStyle protocol membership
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public extension FormatStyle where Self == Decimal.FormatStyle {
|
|
static var number: Self { .init() }
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public extension FormatStyle where Self == Decimal.FormatStyle.Percent {
|
|
static var percent: Self { .init() }
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public extension FormatStyle where Self == Decimal.FormatStyle.Currency {
|
|
static func currency(code: String) -> Self { .init(code: code, locale: .autoupdatingCurrent) }
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public extension ParseableFormatStyle where Self == Decimal.FormatStyle {
|
|
static var number: Self { Decimal.FormatStyle() }
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public extension ParseableFormatStyle where Self == Decimal.FormatStyle.Percent {
|
|
static var percent: Self { Decimal.FormatStyle.Percent() }
|
|
}
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
public extension ParseableFormatStyle where Self == Decimal.FormatStyle.Currency {
|
|
static func currency(code: String) -> Self { Decimal.FormatStyle.Currency(code: code) }
|
|
}
|
|
|
|
// MARK: - Decimal type entry point
|
|
|
|
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
|
|
extension Decimal {
|
|
/// Format `self` using `Decimal.FormatStyle`
|
|
public func formatted() -> String {
|
|
FormatStyle().format(self)
|
|
}
|
|
|
|
#if FOUNDATION_FRAMEWORK
|
|
/// Format `self` with the given format.
|
|
public func formatted<S: Foundation.FormatStyle>(_ format: S) -> S.FormatOutput where Self == S.FormatInput {
|
|
format.format(self)
|
|
}
|
|
#else
|
|
/// Format `self` with the given format.
|
|
public func formatted<S: FoundationEssentials.FormatStyle>(_ format: S) -> S.FormatOutput where Self == S.FormatInput {
|
|
format.format(self)
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Regex
|
|
|
|
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
|
|
extension Decimal.FormatStyle : CustomConsumingRegexComponent {
|
|
public typealias RegexOutput = Decimal
|
|
public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: Decimal)? {
|
|
Decimal.ParseStrategy(formatStyle: self, lenient: false).parse(input, startingAt: index, in: bounds)
|
|
}
|
|
}
|
|
|
|
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
|
|
extension Decimal.FormatStyle.Percent : CustomConsumingRegexComponent {
|
|
public typealias RegexOutput = Decimal
|
|
public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: Decimal)? {
|
|
Decimal.ParseStrategy(formatStyle: self, lenient: false).parse(input, startingAt: index, in: bounds)
|
|
}
|
|
}
|
|
|
|
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
|
|
extension Decimal.FormatStyle.Currency : CustomConsumingRegexComponent {
|
|
public typealias RegexOutput = Decimal
|
|
public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: Decimal)? {
|
|
Decimal.ParseStrategy(formatStyle: self, lenient: false).parse(input, startingAt: index, in: bounds)
|
|
}
|
|
}
|
|
|
|
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
|
|
extension RegexComponent where Self == Decimal.FormatStyle {
|
|
/// Creates a regex component to match a localized number string and capture it as a `Decimal`.
|
|
/// - Parameter locale: The locale with which the string is formatted.
|
|
/// - Returns: A `RegexComponent` to match a localized number string.
|
|
public static func localizedDecimal(locale: Locale) -> Self {
|
|
Decimal.FormatStyle(locale: locale)
|
|
}
|
|
}
|
|
|
|
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
|
|
extension RegexComponent where Self == Decimal.FormatStyle.Currency {
|
|
/// Creates a regex component to match a localized currency string and capture it as a `Decimal`. For example, `localizedIntegerCurrency(code: "USD", locale: Locale(identifier: "en_US"))` matches "$52,249.98" and captures it as 52249.98.
|
|
/// - Parameters:
|
|
/// - code: The currency code of the currency symbol or name in the string.
|
|
/// - locale: The locale with which the string is formatted.
|
|
/// - Returns: A `RegexComponent` to match a localized currency number.
|
|
public static func localizedCurrency(code: Locale.Currency, locale: Locale) -> Self {
|
|
Decimal.FormatStyle.Currency(code: code.identifier, locale: locale)
|
|
}
|
|
}
|