diff --git a/stdlib/public/core/Duration.swift b/stdlib/public/core/Duration.swift index cd3979a6697b8..737b77971b3e2 100644 --- a/stdlib/public/core/Duration.swift +++ b/stdlib/public/core/Duration.swift @@ -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. @@ -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 @@ -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 @@ -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 diff --git a/test/stdlib/Duration.swift b/test/stdlib/Duration.swift new file mode 100644 index 0000000000000..e6533d52d5254 --- /dev/null +++ b/test/stdlib/Duration.swift @@ -0,0 +1,46 @@ +// RUN: %target-run-simple-swift +// REQUIRES: executable_test + +import StdlibUnittest + +var suite = TestSuite("StringIndexTests") +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) + } + } +}