mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-22 21:40:04 +08:00
519 lines
23 KiB
Swift
519 lines
23 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
|
|
|
|
let hourSymbol: Character = "h"
|
|
let minuteSymbol: Character = "m"
|
|
let secondSymbol: Character = "s"
|
|
let quoteSymbol: Character = "'"
|
|
|
|
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
|
|
extension Duration {
|
|
|
|
/// Format style to format a `Duration` in a localized positional format.
|
|
/// For example, one hour and ten minutes is displayed as “1:10:00” in
|
|
/// the U.S. English locale, or “1.10.00” in the Finnish locale.
|
|
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
|
|
public struct TimeFormatStyle : FormatStyle, Sendable {
|
|
|
|
/// The units to display a Duration with and configurations for the units.
|
|
public struct Pattern : Hashable, Codable, Sendable {
|
|
enum Fields: Hashable, Codable {
|
|
case hourMinute(roundSeconds: FloatingPointRoundingRule)
|
|
case hourMinuteSecond(fractionalSecondsLength: Int, roundFractionalSeconds: FloatingPointRoundingRule)
|
|
case minuteSecond(fractionalSecondsLength: Int, roundFractionalSeconds: FloatingPointRoundingRule)
|
|
}
|
|
var fields: Fields
|
|
|
|
var paddingForLargestField: Int?
|
|
|
|
var roundingRule: FloatingPointRoundingRule {
|
|
switch fields {
|
|
case .hourMinute(roundSeconds: let rule),
|
|
.hourMinuteSecond(fractionalSecondsLength: _, roundFractionalSeconds: let rule),
|
|
.minuteSecond(fractionalSecondsLength: _, roundFractionalSeconds: let rule):
|
|
return rule
|
|
}
|
|
}
|
|
|
|
init(fields: Fields, paddingForLargestField: Int? = nil) {
|
|
self.fields = fields
|
|
self.paddingForLargestField = paddingForLargestField
|
|
}
|
|
|
|
/// Displays a duration in hours and minutes.
|
|
public static var hourMinute: Pattern {
|
|
.init(fields: .hourMinute(roundSeconds: .toNearestOrEven))
|
|
}
|
|
/// Displays a duration in terms of hours and minutes with the specified configurations.
|
|
/// - Parameters:
|
|
/// - padHourToLength: Padding for the hour field. For example, one hour is formatted as "01:00" in en_US locale when this value is set to 2.
|
|
/// - roundSeconds: Rounding rule for the remaining second values.
|
|
/// - Returns: A pattern to format a duration with.
|
|
public static func hourMinute(padHourToLength: Int, roundSeconds: FloatingPointRoundingRule = .toNearestOrEven) -> Pattern {
|
|
.init(fields: .hourMinute(roundSeconds: roundSeconds), paddingForLargestField: padHourToLength)
|
|
}
|
|
|
|
/// Displays a duration in hours, minutes, and seconds.
|
|
public static var hourMinuteSecond: Pattern {
|
|
.init(fields: .hourMinuteSecond(fractionalSecondsLength: 0, roundFractionalSeconds: .toNearestOrEven))
|
|
}
|
|
|
|
/// Displays a duration in terms of hours, minutes, and seconds with the specified configurations.
|
|
/// - Parameters:
|
|
/// - padHourToLength: Padding for the hour field. For example, one hour is formatted as "01:00:00" in en_US locale when this value is set to 2.
|
|
/// - fractionalSecondsLength: The length of the fractional seconds. For example, one hour is formatted as "1:00:00.00" in en_US locale when this value is set to 2.
|
|
/// - roundFractionalSeconds: Rounding rule for the fractional second values.
|
|
/// - Returns: A pattern to format a duration with.
|
|
public static func hourMinuteSecond(padHourToLength: Int, fractionalSecondsLength: Int = 0, roundFractionalSeconds: FloatingPointRoundingRule = .toNearestOrEven) -> Pattern {
|
|
.init(fields: .hourMinuteSecond(fractionalSecondsLength: fractionalSecondsLength, roundFractionalSeconds: roundFractionalSeconds), paddingForLargestField: padHourToLength)
|
|
}
|
|
|
|
/// Displays a duration in minutes and seconds. For example, one hour is formatted as "60:00" in en_US locale.
|
|
public static var minuteSecond: Pattern {
|
|
.init(fields: .minuteSecond(fractionalSecondsLength: 0, roundFractionalSeconds: .toNearestOrEven))
|
|
}
|
|
/// Displays a duration in minutes and seconds with the specified configurations.
|
|
/// - Parameters:
|
|
/// - padMinuteToLength: Padding for the minute field. For example, five minutes is formatted as "05:00" in en_US locale when this value is set to 2.
|
|
/// - fractionalSecondsLength: The length of the fractional seconds. For example, one hour is formatted as "1:00:00.00" in en_US locale when this value is set to 2.
|
|
/// - roundFractionalSeconds: Rounding rule for the fractional second values.
|
|
/// - Returns: A pattern to format a duration with.
|
|
public static func minuteSecond(padMinuteToLength: Int, fractionalSecondsLength: Int = 0, roundFractionalSeconds: FloatingPointRoundingRule = .toNearestOrEven) -> Pattern {
|
|
.init(fields: .minuteSecond(fractionalSecondsLength: fractionalSecondsLength, roundFractionalSeconds: roundFractionalSeconds), paddingForLargestField: padMinuteToLength)
|
|
}
|
|
}
|
|
|
|
var _attributed: Attributed
|
|
/// The locale to use when formatting the duration.
|
|
public var locale: Locale {
|
|
get { _attributed.locale }
|
|
set { _attributed.locale = newValue }
|
|
}
|
|
|
|
/// The pattern to display a Duration with.
|
|
public var pattern: Pattern {
|
|
get { _attributed.pattern }
|
|
set { _attributed.pattern = newValue }
|
|
}
|
|
|
|
/// The attributed format style corresponding to this style.
|
|
public var attributed: Attributed {
|
|
return _attributed
|
|
}
|
|
|
|
/// Creates an instance using the provided pattern and locale.
|
|
/// - Parameters:
|
|
/// - pattern: A `Pattern` to specify the units to include in the displayed string and the behavior of the units.
|
|
/// - locale: The `Locale` used to create the string representation of the duration.
|
|
public init(pattern: Pattern, locale: Locale = .autoupdatingCurrent) {
|
|
self._attributed = Attributed(pattern: pattern, locale: locale)
|
|
}
|
|
|
|
fileprivate init(_ attributedStyle: Attributed) {
|
|
self._attributed = attributedStyle
|
|
}
|
|
|
|
// `FormatStyle` conformance
|
|
|
|
/// Creates a locale-aware string representation from a duration value.
|
|
/// - Parameter value: The value to format.
|
|
/// - Returns: A string representation of the duration.
|
|
public func format(_ value: Duration) -> String {
|
|
String(_attributed.format(value).characters[...])
|
|
}
|
|
|
|
/// Modifies the format style to use the specified locale.
|
|
/// - Parameter locale: The locale to use when formatting a duration.
|
|
/// - Returns: A format style with the provided locale.
|
|
public func locale(_ locale: Locale) -> Self {
|
|
var new = self
|
|
new.locale = locale
|
|
return new
|
|
}
|
|
|
|
}
|
|
|
|
// For testing purpose. See notes about String._Encoding
|
|
internal typealias _TimeFormatStyle = TimeFormatStyle
|
|
}
|
|
|
|
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
|
|
extension FormatStyle where Self == Duration.TimeFormatStyle {
|
|
/// A factory variable to create a time format style to format a duration.
|
|
/// - Parameter pattern: A `Pattern` to specify the units to include in the displayed string and the behavior of the units.
|
|
/// - Returns: A format style to format a duration.
|
|
public static func time(pattern: Duration.TimeFormatStyle.Pattern) -> Self {
|
|
.init(pattern: pattern)
|
|
}
|
|
}
|
|
|
|
// MARK: - Attributed style
|
|
|
|
|
|
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
|
|
extension Duration.TimeFormatStyle {
|
|
|
|
/// Formats a duration as an attributed string with the `durationField` attribute key and `FoundationAttributes.DurationFieldAttribute` attribute.
|
|
///
|
|
/// For example, two hour, 43 minute and 26.25 seconds can be formatted as an attributed string, "2:43:26.25" with the following run text and attributes:
|
|
/// ```
|
|
/// 2 { durationField: .hours }
|
|
/// : { nil }
|
|
/// 43 { durationField: .minutes }
|
|
/// : { nil }
|
|
/// 26.25 { durationField: .seconds }
|
|
/// ```
|
|
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
|
|
@dynamicMemberLookup
|
|
public struct Attributed : FormatStyle, Sendable {
|
|
|
|
typealias Pattern = Duration.TimeFormatStyle.Pattern
|
|
|
|
var pattern: Pattern
|
|
|
|
var grouping: NumberFormatStyleConfiguration.Grouping = .automatic
|
|
|
|
var locale: Locale
|
|
|
|
internal init(pattern: Pattern, locale: Locale) {
|
|
self.pattern = pattern
|
|
self.locale = locale
|
|
}
|
|
|
|
/// Modifies the format style to use the specified locale.
|
|
/// - Parameter locale: The locale to use when formatting a duration.
|
|
/// - Returns: A format style with the provided locale.
|
|
public func locale(_ locale: Locale) -> Self {
|
|
var new = self
|
|
new.locale = locale
|
|
return new
|
|
}
|
|
|
|
/// Formats a duration as an attributed string with `DurationFieldAttribute`.
|
|
/// - Parameter value: The value to format.
|
|
/// - Returns: An attributed string to represent the duration.
|
|
public func format(_ value: Duration) -> AttributedString {
|
|
var uPattern: UATimeUnitTimePattern
|
|
var fallbackPattern: String
|
|
switch pattern.fields {
|
|
case .hourMinute:
|
|
uPattern = .hourMinute
|
|
fallbackPattern = "h':'mm"
|
|
break
|
|
case .hourMinuteSecond:
|
|
uPattern = .hourMinuteSecond
|
|
fallbackPattern = "h':'mm':'ss"
|
|
break
|
|
case .minuteSecond:
|
|
uPattern = .minuteSecond
|
|
fallbackPattern = "m':'ss"
|
|
break
|
|
}
|
|
|
|
var patternString: String!
|
|
|
|
let capacity = 128
|
|
withUnsafeTemporaryAllocation(of: UChar.self, capacity: capacity) { ptr in
|
|
var status = U_ZERO_ERROR
|
|
let len = uatmufmt_getTimePattern(locale.identifier, uPattern, ptr.baseAddress!, Int32(capacity), &status)
|
|
guard status.isSuccess, let bufferStart = ptr.baseAddress, let str = String(_utf16: bufferStart, count: Int(len)) else {
|
|
patternString = fallbackPattern
|
|
return
|
|
}
|
|
|
|
patternString = str.lowercased()
|
|
}
|
|
|
|
let units: [Duration.UnitsFormatStyle.Unit]
|
|
let rounding: FloatingPointRoundingRule
|
|
let lastUnitFractionalLen: Int
|
|
switch pattern.fields {
|
|
case .hourMinute(let roundSeconds):
|
|
units = [ .hours, .minutes ]
|
|
rounding = roundSeconds
|
|
lastUnitFractionalLen = 0
|
|
case .hourMinuteSecond(let fractionalSecondsLength, let roundFractionalSeconds):
|
|
units = [ .hours, .minutes, .seconds ]
|
|
rounding = roundFractionalSeconds
|
|
lastUnitFractionalLen = fractionalSecondsLength
|
|
case .minuteSecond(let fractionalSecondsLength, let roundFractionalSeconds):
|
|
units = [ .minutes, .seconds ]
|
|
rounding = roundFractionalSeconds
|
|
lastUnitFractionalLen = fractionalSecondsLength
|
|
}
|
|
|
|
let values = value.valuesForUnits(units, trailingFractionalLength: lastUnitFractionalLen, smallestUnitRounding: rounding, roundingIncrement: nil)
|
|
assert(units.count == values.count)
|
|
let unitValues = Dictionary(uniqueKeysWithValues: zip(units, values))
|
|
|
|
let patternComponents = Self.componentsFromPatternString(patternString, patternSet: [ hourSymbol, minuteSymbol, secondSymbol ])
|
|
|
|
return formatWithPatternComponents(patternComponents, hour: unitValues[.hours] ?? 0, minute: unitValues[.minutes] ?? 0, second: unitValues[.seconds] ?? 0)
|
|
}
|
|
|
|
internal struct PatternComponent {
|
|
// Consecutive characters in a pattern that represents a single symbol or a literal string.
|
|
// For example, in the "h':'mm" pattern, this could be [":"] or ["m", "m"].
|
|
let symbols: [Character]
|
|
|
|
// True if this component is one of the pre-defined symbols, or false if it's a literal string surrounded by single quotation marks.
|
|
let isField: Bool
|
|
|
|
// The start and end index of the component
|
|
let start: Int
|
|
let end: Int
|
|
}
|
|
|
|
// Parses a pattern string and returns a list of components the pattern specifies.
|
|
// The pattern string should consist symbols (represented by a character) and literals (enclosed with single quotation marks).
|
|
static func componentsFromPatternString(_ pattern: String, patternSet: [Character]) -> [PatternComponent] {
|
|
var inQuote: Bool = false // inside a quoted literal
|
|
var runBegin: Int = 0
|
|
var runSymbol: Character?
|
|
var runIsField: Bool = true
|
|
|
|
var result = [PatternComponent]()
|
|
var token = [Character]()
|
|
|
|
for (idx, c) in pattern.enumerated() {
|
|
// Record the previous token up until the current position if we see
|
|
// 1) a different symbol while we're in the middle of parsing a field. This would happen if the pattern consists of multiple consecutive symbols that are not separated by literals, such as "hmmss".
|
|
// or
|
|
// 2) a literal while parsing a field, such as the first quotation mark of "h':'mm"
|
|
let isField = !inQuote && patternSet.contains(c)
|
|
if idx > runBegin, (isField && c != runSymbol) || (!isField && runIsField) {
|
|
result.append(PatternComponent(symbols: token, isField: runIsField, start: runBegin, end: idx))
|
|
|
|
token = []
|
|
runBegin = idx
|
|
}
|
|
|
|
if c == quoteSymbol {
|
|
if runSymbol == quoteSymbol {
|
|
token.append(quoteSymbol)
|
|
} else {
|
|
inQuote = !inQuote
|
|
}
|
|
} else {
|
|
token.append(c)
|
|
}
|
|
|
|
runIsField = isField
|
|
runSymbol = c
|
|
}
|
|
|
|
if !token.isEmpty {
|
|
result.append(PatternComponent(symbols: token, isField: runIsField, start: runBegin, end: pattern.count))
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func formatWithPatternComponents(_ components: [PatternComponent], hour: Double, minute: Double, second: Double) -> AttributedString {
|
|
// The number format does not contain rounding settings because it's handled on the value itself
|
|
var numberFormatStyle = FloatingPointFormatStyle<Double>(locale: locale).grouping(grouping)
|
|
var result = AttributedString()
|
|
|
|
let isNegative = hour < 0 || minute < 0 || second < 0
|
|
|
|
for component in components {
|
|
var attr: AttributeScopes.FoundationAttributes.DurationFieldAttribute.Field?
|
|
var substring: String
|
|
if component.isField {
|
|
let patternSymbols = component.symbols
|
|
guard let symbol = patternSymbols.first else { continue }
|
|
var value: Double?
|
|
let isMostSignificantField: Bool
|
|
|
|
var minIntLength = patternSymbols.count
|
|
switch symbol {
|
|
case hourSymbol:
|
|
value = hour
|
|
if let padding = pattern.paddingForLargestField {
|
|
minIntLength = max(padding, patternSymbols.count)
|
|
}
|
|
numberFormatStyle = numberFormatStyle.precision(.integerAndFractionLength(integerLimits: minIntLength..., fractionLimits: 0...0))
|
|
attr = .hours
|
|
isMostSignificantField = true
|
|
case minuteSymbol:
|
|
value = minute
|
|
switch pattern.fields {
|
|
case .hourMinute:
|
|
numberFormatStyle = numberFormatStyle.precision(.integerAndFractionLength(integerLimits: minIntLength..., fractionLimits: 0...0))
|
|
isMostSignificantField = false
|
|
case .hourMinuteSecond:
|
|
numberFormatStyle = numberFormatStyle.precision(.integerAndFractionLength(integerLimits: minIntLength..., fractionLimits: 0...0))
|
|
isMostSignificantField = false
|
|
case .minuteSecond:
|
|
if let padding = pattern.paddingForLargestField {
|
|
minIntLength = max(padding, patternSymbols.count)
|
|
}
|
|
numberFormatStyle = numberFormatStyle.precision(.integerAndFractionLength(integerLimits: minIntLength..., fractionLimits: 0...0))
|
|
isMostSignificantField = true
|
|
}
|
|
|
|
attr = .minutes
|
|
case secondSymbol:
|
|
value = second
|
|
switch pattern.fields {
|
|
case .hourMinute:
|
|
break
|
|
case .hourMinuteSecond(let fractionalSecondsLength, _):
|
|
numberFormatStyle = numberFormatStyle.precision(.integerAndFractionLength(integerLimits: minIntLength..., fractionLimits: fractionalSecondsLength...fractionalSecondsLength))
|
|
case .minuteSecond(let fractionalSecondsLength, _):
|
|
numberFormatStyle = numberFormatStyle.precision(.integerAndFractionLength(integerLimits: minIntLength..., fractionLimits: fractionalSecondsLength...fractionalSecondsLength))
|
|
}
|
|
|
|
attr = .seconds
|
|
isMostSignificantField = false
|
|
default:
|
|
isMostSignificantField = false
|
|
}
|
|
|
|
if var value = value {
|
|
// we only want the sign to show for the first component
|
|
// and only if the overall value is negative
|
|
let showNegativeSign = isNegative && isMostSignificantField
|
|
|
|
// if the first component is zero, we normally wouldn't get
|
|
// a negative sign, so we make the value a small negative
|
|
// value that still rounds to zero
|
|
if showNegativeSign && value == 0 {
|
|
value = -0.1
|
|
}
|
|
|
|
substring = numberFormatStyle
|
|
.sign(strategy: showNegativeSign
|
|
? .always(includingZero: true)
|
|
: .never)
|
|
.format(value)
|
|
} else {
|
|
substring = String(component.symbols)
|
|
}
|
|
} else {
|
|
substring = String(component.symbols)
|
|
}
|
|
|
|
let attrSubstring: AttributedString
|
|
if let attr = attr {
|
|
attrSubstring = AttributedString(substring, attributes: .init().durationField(attr))
|
|
} else {
|
|
attrSubstring = AttributedString(substring)
|
|
}
|
|
result += attrSubstring
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(FoundationPreview 0.4, *)
|
|
extension Duration.TimeFormatStyle {
|
|
/// Returns a modified style that applies the given `grouping` rule to the highest field in the
|
|
/// pattern.
|
|
public func grouping(_ grouping: NumberFormatStyleConfiguration.Grouping) -> Self {
|
|
var copy = self
|
|
copy._attributed.grouping = grouping
|
|
return copy
|
|
}
|
|
|
|
/// The `grouping` rule applied to high number values on the largest field in the pattern.
|
|
public var grouping: NumberFormatStyleConfiguration.Grouping {
|
|
get { _attributed.grouping }
|
|
set { _attributed.grouping = newValue }
|
|
}
|
|
}
|
|
|
|
@available(FoundationPreview 0.4, *)
|
|
extension Duration.TimeFormatStyle.Attributed {
|
|
/// Returns a modified style that applies the given `grouping` rule to the highest field in the
|
|
/// pattern.
|
|
public func grouping(_ grouping: NumberFormatStyleConfiguration.Grouping) -> Self {
|
|
var copy = self
|
|
copy.grouping = grouping
|
|
return copy
|
|
}
|
|
}
|
|
|
|
// MARK: Dynamic Member Lookup
|
|
|
|
@available(FoundationPreview 0.4, *)
|
|
extension Duration.TimeFormatStyle.Attributed {
|
|
private var innerStyle: Duration.TimeFormatStyle {
|
|
get {
|
|
.init(self)
|
|
}
|
|
set {
|
|
self = newValue._attributed
|
|
}
|
|
}
|
|
|
|
public subscript<T>(dynamicMember key: KeyPath<Duration.TimeFormatStyle, T>) -> T {
|
|
innerStyle[keyPath: key]
|
|
}
|
|
|
|
public subscript<T>(dynamicMember key: WritableKeyPath<Duration.TimeFormatStyle, T>) -> T {
|
|
get {
|
|
innerStyle[keyPath: key]
|
|
}
|
|
set {
|
|
innerStyle[keyPath: key] = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: DiscreteFormatStyle Conformance
|
|
|
|
@available(FoundationPreview 0.4, *)
|
|
extension Duration.TimeFormatStyle.Attributed : DiscreteFormatStyle {
|
|
public func discreteInput(before input: Duration) -> Duration? {
|
|
Duration.TimeFormatStyle(pattern: pattern, locale: locale).discreteInput(before: input)
|
|
}
|
|
|
|
public func discreteInput(after input: Duration) -> Duration? {
|
|
Duration.TimeFormatStyle(pattern: pattern, locale: locale).discreteInput(after: input)
|
|
}
|
|
}
|
|
|
|
@available(FoundationPreview 0.4, *)
|
|
extension Duration.TimeFormatStyle : DiscreteFormatStyle {
|
|
public func discreteInput(before input: Duration) -> Duration? {
|
|
let (bound, isIncluded) = Duration.bound(for: input, in: interval(for: input), countingDown: true, roundingRule: self.pattern.roundingRule)
|
|
|
|
return isIncluded ? bound.nextDown : bound
|
|
}
|
|
|
|
public func discreteInput(after input: Duration) -> Duration? {
|
|
let (bound, isIncluded) = Duration.bound(for: input, in: interval(for: input), countingDown: false, roundingRule: self.pattern.roundingRule)
|
|
|
|
return isIncluded ? bound.nextUp : bound
|
|
}
|
|
|
|
private func interval(for input: Duration) -> Duration {
|
|
switch pattern.fields {
|
|
case .hourMinute:
|
|
return .seconds(60)
|
|
case .hourMinuteSecond(fractionalSecondsLength: let length, _):
|
|
return Duration.interval(for: .seconds, fractionalDigits: length)
|
|
case .minuteSecond(fractionalSecondsLength: let length, _):
|
|
return Duration.interval(for: .seconds, fractionalDigits: length)
|
|
}
|
|
}
|
|
}
|