Skip to content

Duration.seconds, .milliseconds, and .microseconds should preserve exact values #66137

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions stdlib/public/core/Duration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,23 @@ extension Duration {
return Duration(_attoseconds:
_Int128(seconds).multiplied(by: 1_000_000_000_000_000_000 as UInt64))
}

/// Construct a `Duration` given a duration and scale, taking care so that
/// exact integer durations are preserved exactly.
internal init(_ duration: Double, scale: UInt64) {
// Split the duration into integral and fractional parts, as we need to
// handle them slightly differently to ensure that integer values are
// never rounded if `scale` is representable as Double.
let integralPart = duration.rounded(.towardZero)
let fractionalPart = integralPart - duration
self.init(_attoseconds:
// This term may trap due to overflow, but it cannot round, so if the
// input `seconds` is an exact integer, we get an exact integer result.
_Int128(integralPart).multiplied(by: scale) +
// This term may round, but cannot overflow.
_Int128((fractionalPart * Double(scale)).rounded())
)
}

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

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

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

/// Construct a `Duration` given a number of nanoseconds represented as a
Expand Down
46 changes: 46 additions & 0 deletions test/stdlib/Duration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// RUN: %target-run-simple-swift
// REQUIRES: executable_test

import StdlibUnittest

var suite = TestSuite("DurationTests")
defer { runAllTests() }

if #available(SwiftStdlib 5.7, *) {
suite.test("seconds from Double") {
for _ in 0 ..< 100 {
let integerValue = Double(Int64.random(in: 0 ... 0x7fff_ffff_ffff_fc00))
let (sec, attosec) = Duration.seconds(integerValue).components
expectEqual(sec, Int64(integerValue))
expectEqual(attosec, 0)
}
// Value that overflows conversion from Double -> Int64, but should be
// representable as a number of seconds:
let huge: Double = 1.7e20
let duration = Duration.seconds(huge)
// Divide by 1000 to get back to a duration with representable components:
let smallerDuration = duration / 1000
expectEqual(smallerDuration.components, (170_000_000_000_000_000, 0))
// Now check that the components of the original value trap:
expectCrashLater()
let _ = duration.components
}

suite.test("milliseconds from Double") {
for _ in 0 ..< 100 {
let integerValue = Double(Int64.random(in: 0 ... 0x7fff_ffff_ffff_fc00))
let (sec, attosec) = Duration.milliseconds(integerValue).components
expectEqual(sec, Int64(integerValue) / 1000)
expectEqual(attosec, Int64(integerValue) % 1000 * 1_000_000_000_000_000)
}
}

suite.test("microseconds from Double") {
for _ in 0 ..< 100 {
let integerValue = Double(Int64.random(in: 0 ... 0x7fff_ffff_ffff_fc00))
let (sec, attosec) = Duration.microseconds(integerValue).components
expectEqual(sec, Int64(integerValue) / 1_000_000)
expectEqual(attosec, Int64(integerValue) % 1_000_000 * 1_000_000_000_000)
}
}
}