diff --git a/Sources/SwiftProtobuf/Google_Protobuf_Duration+Extensions.swift b/Sources/SwiftProtobuf/Google_Protobuf_Duration+Extensions.swift index 9b5b3cf52..203d91eff 100644 --- a/Sources/SwiftProtobuf/Google_Protobuf_Duration+Extensions.swift +++ b/Sources/SwiftProtobuf/Google_Protobuf_Duration+Extensions.swift @@ -169,6 +169,46 @@ extension Google_Protobuf_Duration { } } +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Google_Protobuf_Duration { + /// Creates a new `Google_Protobuf_Duration` by rounding a `Duration` to + /// the nearest nanosecond according to the given rounding rule. + /// + /// - Parameters: + /// - duration: The `Duration`. + /// - rule: The rounding rule to use. + public init( + rounding duration: Duration, + rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero + ) { + let secs = duration.components.seconds + let attos = duration.components.attoseconds + let fracNanos = + (Double(attos % attosPerNanosecond) / Double(attosPerNanosecond)).rounded(rule) + let nanos = Int32(attos / attosPerNanosecond) + Int32(fracNanos) + let (s, n) = normalizeForDuration(seconds: secs, nanos: nanos) + self.init(seconds: s, nanos: n) + } +} + +@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) +extension Duration { + /// Creates a new `Duration` that is equal to the given duration. + /// + /// Swift `Duration` has a strictly higher precision than `Google_Protobuf_Duration` + /// (attoseconds vs. nanoseconds, respectively), so this conversion is always + /// value-preserving. + /// + /// - Parameters: + /// - duration: The `Google_Protobuf_Duration`. + public init(_ duration: Google_Protobuf_Duration) { + self.init( + secondsComponent: duration.seconds, + attosecondsComponent: Int64(duration.nanos) * attosPerNanosecond + ) + } +} + private func normalizeForDuration( seconds: Int64, nanos: Int32 diff --git a/Sources/SwiftProtobuf/TimeUtils.swift b/Sources/SwiftProtobuf/TimeUtils.swift index e21d31c3b..bccd13fb9 100644 --- a/Sources/SwiftProtobuf/TimeUtils.swift +++ b/Sources/SwiftProtobuf/TimeUtils.swift @@ -18,6 +18,7 @@ let secondsPerDay: Int32 = 86400 let secondsPerHour: Int32 = 3600 let secondsPerMinute: Int32 = 60 let nanosPerSecond: Int32 = 1_000_000_000 +let attosPerNanosecond: Int64 = 1_000_000_000 internal func timeOfDayFromSecondsSince1970(seconds: Int64) -> (hh: Int32, mm: Int32, ss: Int32) { let secondsSinceMidnight = Int32(mod(seconds, Int64(secondsPerDay))) diff --git a/Tests/SwiftProtobufTests/Test_Duration.swift b/Tests/SwiftProtobufTests/Test_Duration.swift index 07b6b6478..803424db4 100644 --- a/Tests/SwiftProtobufTests/Test_Duration.swift +++ b/Tests/SwiftProtobufTests/Test_Duration.swift @@ -312,4 +312,76 @@ final class Test_Duration: XCTestCase, PBTestHelpers { let t2 = Google_Protobuf_Duration(seconds: 123, nanos: 123_456_789) XCTAssertEqual(t2.timeInterval, 123.123456789) } + + func testConvertFromStdlibDuration() throws { + // Full precision + do { + let sd = Duration.seconds(123) + .nanoseconds(123_456_789) + let pd = Google_Protobuf_Duration(rounding: sd) + XCTAssertEqual(pd.seconds, 123) + XCTAssertEqual(pd.nanos, 123_456_789) + } + + // Default rounding (toNearestAwayFromZero) + do { + let sd = Duration(secondsComponent: 123, attosecondsComponent: 123_456_789_499_999_999) + let pd = Google_Protobuf_Duration(rounding: sd) + XCTAssertEqual(pd.seconds, 123) + XCTAssertEqual(pd.nanos, 123_456_789) + } + do { + let sd = Duration(secondsComponent: 123, attosecondsComponent: 123_456_789_500_000_000) + let pd = Google_Protobuf_Duration(rounding: sd) + XCTAssertEqual(pd.seconds, 123) + XCTAssertEqual(pd.nanos, 123_456_790) + } + + // Other rounding rules + do { + let sd = Duration(secondsComponent: 123, attosecondsComponent: 123_456_789_499_999_999) + let pd = Google_Protobuf_Duration(rounding: sd, rule: .awayFromZero) + XCTAssertEqual(pd.seconds, 123) + XCTAssertEqual(pd.nanos, 123_456_790) + } + do { + let sd = Duration(secondsComponent: 123, attosecondsComponent: 123_456_789_999_999_999) + let pd = Google_Protobuf_Duration(rounding: sd, rule: .towardZero) + XCTAssertEqual(pd.seconds, 123) + XCTAssertEqual(pd.nanos, 123_456_789) + } + + // Negative duration + do { + let sd = Duration.zero - .seconds(123) - .nanoseconds(123_456_789) + let pd = Google_Protobuf_Duration(rounding: sd) + XCTAssertEqual(pd.seconds, -123) + XCTAssertEqual(pd.nanos, -123_456_789) + } + do { + let sd = .zero - Duration(secondsComponent: 123, attosecondsComponent: 123_456_789_000_000_001) + let pd = Google_Protobuf_Duration(rounding: sd, rule: .towardZero) + XCTAssertEqual(pd.seconds, -123) + XCTAssertEqual(pd.nanos, -123_456_789) + } + do { + let sd = .zero - Duration(secondsComponent: 123, attosecondsComponent: 123_456_789_000_000_001) + let pd = Google_Protobuf_Duration(rounding: sd, rule: .awayFromZero) + XCTAssertEqual(pd.seconds, -123) + XCTAssertEqual(pd.nanos, -123_456_790) + } + } + + func testConvertToStdlibDuration() throws { + do { + let pd = Google_Protobuf_Duration(seconds: 123, nanos: 123_456_789) + let sd = Duration(pd) + XCTAssertEqual(sd, .seconds(123) + .nanoseconds(123_456_789)) + } + // Negative duration + do { + let pd = Google_Protobuf_Duration(seconds: -123, nanos: -123_456_789) + let sd = Duration(pd) + XCTAssertEqual(sd, .zero - (.seconds(123) + .nanoseconds(123_456_789))) + } + } }