Skip to content

Commit 25014fc

Browse files
authored
Add Duration FormatStyle(s) to swift-foundation (#278)
* Add Duration formatting and utility files to FoundationInternationalization. This commit moves existing files as-is to FoundationInternationalization. We'll address compatibility fixes in the following commits. * Add partial implementation for Measurement.FormatStyle `Duration.FormatStyle` uses `Measurement.FormatStyle.UnitWidth` internally. This PR adds a partial implementation for `Measurement.FormatStyle` to maintain source compatibility for `Duration.FormatStyle`. `Measurement.FormatStyle` is built on top of `struct Measurement<Dimension>`, which isn't available in the package yet. To unblock ourselves, add stubs for relevant types if they're not available for now. * Addressing compatibility issues post file move - Add functions to generate ICU skeleton to `ICUMeasurementNumberFormatter`: The function was originally declared as a `static func` in `Measurement.FormatStyle`. Re-work this into `ICUMeasurementNumberFormatter` since this function is only relevant when used with this class. - Use Glibc for `pow` when Darwin isn't available * Move test files * Move test files * Compatibility fix: typealias Duration.TimeFormatStyle and Duration.UnitsFormatStyle XCTest explicitly exports Foundation, so referring to `Duration.UnitsFormatStyle` and `Duration.TimeFormatStyle` raises amibiguity errors on Darwin since the compiler doesn't know if they refer to the ones defined in the package or the one in Foundation. We've been working around this issue by referring common types with prefix of package name (see TestSupport.swift). However we cannot do that for `Duration.UnitsFormatStyle` since this type is declared as `extension Duration`, and there's no way to disambiguate or typealias extensions on stdlib types. Workaround this by declaring `Duration._UnitsFormatStyle` as a typealias of `Duration.FormatStyle`, and use the former in the test. This way, when Darwin is imported, `_UnitsFormatStyle` would refer to the `Duration.UnitsFormatStyle` declared in Foundation, and the one in the package otherwise.
1 parent e973a96 commit 25014fc

File tree

8 files changed

+3035
-0
lines changed

8 files changed

+3035
-0
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if canImport(FoundationEssentials)
14+
import FoundationEssentials
15+
#endif
16+
17+
#if canImport(Darwin)
18+
import Darwin
19+
#elseif canImport(Glibc)
20+
import Glibc
21+
#endif
22+
23+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
24+
extension Duration {
25+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
26+
public func formatted<S: FormatStyle>(_ v: S) -> S.FormatOutput where S.FormatInput == Self {
27+
v.format(self)
28+
}
29+
30+
/// Formats `self` using the hour-minute-second time pattern
31+
/// - Returns: A formatted string to describe the duration, such as "1:30:56" for a duration of 1 hour, 30 minutes, and 56 seconds
32+
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
33+
public func formatted() -> String {
34+
return Self.TimeFormatStyle(pattern: .hourMinuteSecond).format(self)
35+
}
36+
}
37+
38+
extension Duration {
39+
// Returns an array of values corresponding to each unit in `units`
40+
func valuesForUnits(
41+
_ units: [UnitsFormatStyle.Unit],
42+
trailingFractionalLength: Int,
43+
smallestUnitRounding: FloatingPointRoundingRule,
44+
roundingIncrement: Double?
45+
) -> [Double] {
46+
guard let smallestUnit = units.last?.unit else {
47+
return []
48+
}
49+
50+
let increment = Self.interval(
51+
for: .init(unit: smallestUnit),
52+
fractionalDigits: trailingFractionalLength,
53+
roundingIncrement: roundingIncrement
54+
)
55+
56+
let rounded: Duration
57+
if increment != .zero {
58+
rounded = self.rounded(increment: increment, rule: smallestUnitRounding)
59+
} else {
60+
rounded = self
61+
}
62+
63+
var (values, remainder) = rounded.factor(intoUnits: units)
64+
65+
values[values.count-1] += TimeInterval(remainder) / Self.secondCoefficient(for: smallestUnit)
66+
67+
return values
68+
}
69+
70+
static func interval(
71+
for unit: Duration.UnitsFormatStyle.Unit,
72+
fractionalDigits: Int = 0,
73+
roundingIncrement: Double? = nil
74+
) -> Duration {
75+
let fractionalLengthBasedIncrement: Duration
76+
if unit.unit >= .seconds {
77+
fractionalLengthBasedIncrement = Self.interval(fractionalSecondsLength: fractionalDigits) * Self.secondCoefficient(for: unit.unit)!
78+
} else {
79+
let offset = Self.fractionalDigitOffsetToSecond(from: unit.unit)!
80+
fractionalLengthBasedIncrement = Self.interval(fractionalSecondsLength: offset + Swift.min(fractionalDigits, Int.max - offset))
81+
}
82+
83+
if let roundingIncrement {
84+
let roundingIncrementBasedIncrement: Duration
85+
if unit.unit >= .seconds {
86+
roundingIncrementBasedIncrement = .seconds(Self.secondCoefficient(for: unit.unit)!) * roundingIncrement
87+
} else {
88+
roundingIncrementBasedIncrement = .nanoseconds(Self.nanosecondCoefficientsForSubsecondUnits(unit.unit)!) * roundingIncrement
89+
}
90+
91+
return Swift.max(fractionalLengthBasedIncrement, roundingIncrementBasedIncrement)
92+
} else {
93+
return fractionalLengthBasedIncrement
94+
}
95+
}
96+
97+
private static func interval(fractionalSecondsLength: Int) -> Duration {
98+
let intervalMod3: Int64
99+
switch fractionalSecondsLength % 3 {
100+
case 0:
101+
intervalMod3 = 1
102+
case 1:
103+
intervalMod3 = 100
104+
case 2:
105+
intervalMod3 = 10
106+
default:
107+
fatalError("Int % 3 >= 3")
108+
}
109+
switch fractionalSecondsLength {
110+
case ...0:
111+
return .seconds(1)
112+
case ...3:
113+
return .milliseconds(intervalMod3)
114+
case ...6:
115+
return .microseconds(intervalMod3)
116+
case ...9:
117+
return .nanoseconds(intervalMod3)
118+
default:
119+
return .seconds(pow(0.1, Double(fractionalSecondsLength)))
120+
}
121+
}
122+
123+
func factor(intoUnits units: [UnitsFormatStyle.Unit]) -> (values: [Double], remainder: Duration) {
124+
var value = self
125+
var values = [Double]()
126+
for unit in units {
127+
if unit.unit >= .seconds {
128+
guard let coefficient = Self.secondCoefficient(for: unit.unit) else {
129+
values.append(0)
130+
continue
131+
}
132+
133+
let (quotient, remainder) = value.components.seconds.quotientAndRemainder(dividingBy: coefficient)
134+
135+
values.append(Double(quotient))
136+
value = .init(secondsComponent: remainder, attosecondsComponent: value.components.attoseconds)
137+
} else {
138+
guard let coefficient = Self.attosecondCoefficient(for: unit.unit) else {
139+
values.append(0)
140+
continue
141+
}
142+
143+
let (quotient, remainder) = value.components.attoseconds.quotientAndRemainder(dividingBy: coefficient)
144+
145+
var unitValue = Double(quotient)
146+
147+
if value.components.seconds != .zero {
148+
unitValue += Double(value.components.seconds) * pow(10, Double(Self.fractionalDigitOffsetToSecond(from: unit.unit)!))
149+
}
150+
151+
values.append(unitValue)
152+
value = .init(secondsComponent: 0, attosecondsComponent: remainder)
153+
}
154+
}
155+
return (values, value)
156+
}
157+
158+
159+
private static func secondCoefficient(for unit: UnitsFormatStyle.Unit._Unit) -> Double {
160+
if let c: Int64 = secondCoefficient(for: unit) {
161+
return Double(c)
162+
} else {
163+
return pow(0.1, Double(fractionalDigitOffsetToSecond(from: unit)!))
164+
}
165+
}
166+
167+
private static func secondCoefficient(for unit: UnitsFormatStyle.Unit._Unit) -> Int64? {
168+
switch unit {
169+
case .weeks:
170+
return 604800
171+
case .days:
172+
return 86400
173+
case .hours:
174+
return 3600
175+
case .minutes:
176+
return 60
177+
case .seconds:
178+
return 1
179+
default:
180+
return nil
181+
}
182+
}
183+
184+
private static func fractionalDigitOffsetToSecond(from unit: UnitsFormatStyle.Unit._Unit) -> Int? {
185+
switch unit {
186+
case .seconds:
187+
return 0
188+
case .milliseconds:
189+
return 3
190+
case .microseconds:
191+
return 6
192+
case .nanoseconds:
193+
return 9
194+
default:
195+
return nil
196+
}
197+
}
198+
199+
private static func attosecondCoefficient(for unit: UnitsFormatStyle.Unit._Unit) -> Int64? {
200+
switch unit {
201+
case .seconds:
202+
return 1_000_000_000_000_000_000
203+
case .milliseconds:
204+
return 1_000_000_000_000_000
205+
case .microseconds:
206+
return 1_000_000_000_000
207+
case .nanoseconds:
208+
return 1_000_000_000
209+
default:
210+
return nil
211+
}
212+
}
213+
214+
private static func nanosecondCoefficientsForSubsecondUnits(_ unit: UnitsFormatStyle.Unit._Unit) -> Int64? {
215+
switch unit {
216+
case .seconds:
217+
return 1_000_000_000
218+
case .milliseconds:
219+
return 1_000_000
220+
case .microseconds:
221+
return 1_000
222+
case .nanoseconds:
223+
return 1
224+
default:
225+
return nil
226+
}
227+
}
228+
}

0 commit comments

Comments
 (0)