Skip to content

Commit db0a728

Browse files
Rework the .seconds, .milliseconds, and .microseconds constructors to preserve exact values (#66111)
When constructing a Duration from Double, we should do it in such a way that exact integer inputs are preserved exactly, so long as they are represented as a Duration. This was not previously the case. Now it is.
1 parent 22c987e commit db0a728

File tree

2 files changed

+66
-5
lines changed

2 files changed

+66
-5
lines changed

stdlib/public/core/Duration.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,23 @@ extension Duration {
114114
return Duration(_attoseconds:
115115
_Int128(seconds).multiplied(by: 1_000_000_000_000_000_000 as UInt64))
116116
}
117+
118+
/// Construct a `Duration` given a duration and scale, taking care so that
119+
/// exact integer durations are preserved exactly.
120+
internal init(_ duration: Double, scale: UInt64) {
121+
// Split the duration into integral and fractional parts, as we need to
122+
// handle them slightly differently to ensure that integer values are
123+
// never rounded if `scale` is representable as Double.
124+
let integralPart = duration.rounded(.towardZero)
125+
let fractionalPart = integralPart - duration
126+
self.init(_attoseconds:
127+
// This term may trap due to overflow, but it cannot round, so if the
128+
// input `seconds` is an exact integer, we get an exact integer result.
129+
_Int128(integralPart).multiplied(by: scale) +
130+
// This term may round, but cannot overflow.
131+
_Int128((fractionalPart * Double(scale)).rounded())
132+
)
133+
}
117134

118135
/// Construct a `Duration` given a number of seconds represented as a
119136
/// `Double` by converting the value into the closest attosecond scale value.
@@ -123,7 +140,7 @@ extension Duration {
123140
/// - Returns: A `Duration` representing a given number of seconds.
124141
@available(SwiftStdlib 5.7, *)
125142
public static func seconds(_ seconds: Double) -> Duration {
126-
return Duration(_attoseconds: _Int128(seconds * 1_000_000_000_000_000_000))
143+
Duration(seconds, scale: 1_000_000_000_000_000_000)
127144
}
128145

129146
/// Construct a `Duration` given a number of milliseconds represented as a
@@ -148,8 +165,7 @@ extension Duration {
148165
/// - Returns: A `Duration` representing a given number of milliseconds.
149166
@available(SwiftStdlib 5.7, *)
150167
public static func milliseconds(_ milliseconds: Double) -> Duration {
151-
return Duration(_attoseconds:
152-
_Int128(milliseconds * 1_000_000_000_000_000))
168+
Duration(milliseconds, scale: 1_000_000_000_000_000)
153169
}
154170

155171
/// Construct a `Duration` given a number of microseconds represented as a
@@ -174,8 +190,7 @@ extension Duration {
174190
/// - Returns: A `Duration` representing a given number of microseconds.
175191
@available(SwiftStdlib 5.7, *)
176192
public static func microseconds(_ microseconds: Double) -> Duration {
177-
return Duration(_attoseconds:
178-
_Int128(microseconds * 1_000_000_000_000))
193+
Duration(microseconds, scale: 1_000_000_000_000)
179194
}
180195

181196
/// Construct a `Duration` given a number of nanoseconds represented as a

test/stdlib/Duration.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// RUN: %target-run-simple-swift
2+
// REQUIRES: executable_test
3+
4+
import StdlibUnittest
5+
6+
var suite = TestSuite("StringIndexTests")
7+
defer { runAllTests() }
8+
9+
if #available(SwiftStdlib 5.7, *) {
10+
suite.test("seconds from Double") {
11+
for _ in 0 ..< 100 {
12+
let integerValue = Double(Int64.random(in: 0 ... 0x7fff_ffff_ffff_fc00))
13+
let (sec, attosec) = Duration.seconds(integerValue).components
14+
expectEqual(sec, Int64(integerValue))
15+
expectEqual(attosec, 0)
16+
}
17+
// Value that overflows conversion from Double -> Int64, but should be
18+
// representable as a number of seconds:
19+
let huge: Double = 1.7e20
20+
let duration = Duration.seconds(huge)
21+
// Divide by 1000 to get back to a duration with representable components:
22+
let smallerDuration = duration / 1000
23+
expectEqual(smallerDuration.components, (170_000_000_000_000_000, 0))
24+
// Now check that the components of the original value trap:
25+
expectCrashLater()
26+
let _ = duration.components
27+
}
28+
29+
suite.test("milliseconds from Double") {
30+
for _ in 0 ..< 100 {
31+
let integerValue = Double(Int64.random(in: 0 ... 0x7fff_ffff_ffff_fc00))
32+
let (sec, attosec) = Duration.milliseconds(integerValue).components
33+
expectEqual(sec, Int64(integerValue) / 1000)
34+
expectEqual(attosec, Int64(integerValue) % 1000 * 1_000_000_000_000_000)
35+
}
36+
}
37+
38+
suite.test("microseconds from Double") {
39+
for _ in 0 ..< 100 {
40+
let integerValue = Double(Int64.random(in: 0 ... 0x7fff_ffff_ffff_fc00))
41+
let (sec, attosec) = Duration.microseconds(integerValue).components
42+
expectEqual(sec, Int64(integerValue) / 1_000_000)
43+
expectEqual(attosec, Int64(integerValue) % 1_000_000 * 1_000_000_000_000)
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)