diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index 79435c105..174cefd8d 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -28,6 +28,7 @@ add_library(FoundationEssentials OutputBuffer.swift Platform.swift SortComparator.swift + UTCClock.swift UUID_Wrappers.swift UUID.swift WASILibc+Extensions.swift diff --git a/Sources/FoundationEssentials/UTCClock.swift b/Sources/FoundationEssentials/UTCClock.swift new file mode 100644 index 000000000..1b1020a15 --- /dev/null +++ b/Sources/FoundationEssentials/UTCClock.swift @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +extension TimeInterval { + fileprivate init(_ duration: Duration) { + self = Double(duration.components.seconds) + 1e-18 * Double(duration.components.attoseconds) + } +} + +@available(FoundationPreview 6.2, *) +extension Date: InstantProtocol { + + @available(FoundationPreview 6.2, *) + public func advanced(by duration: Duration) -> Date { + addingTimeInterval(TimeInterval(duration)) + } + + @available(FoundationPreview 6.2, *) + public func duration(to other: Date) -> Duration { + .seconds(other.timeIntervalSince(self)) + } + + @available(FoundationPreview 6.2, *) + public static func leapSeconds(from: Date, to: Date) -> Duration { + /* + These can be generated by using the calendar APIs and the following: + let leaps = [ + DateComponents(timeZone: .gmt, year: 1972, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1972, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1973, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1974, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1975, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1976, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1977, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1978, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1979, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1981, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1982, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1983, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1985, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1987, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1989, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1990, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1992, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1993, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1994, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1995, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1997, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 1998, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 2005, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 2008, month: 12, day: 31, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 2012, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 2015, month: 6, day: 30, hour: 23, minute: 59, second: 59), + DateComponents(timeZone: .gmt, year: 2016, month: 12, day: 31, hour: 23, minute: 59, second: 59), + ] + for leap in leaps { + let date = Calendar.current.date(from: leap)! + let t = date.timeIntervalSinceReferenceDate + print("if start <= \(t) && \(t) < end { adjustment += .seconds(1) }") + } + */ + var adjustment = Duration.seconds(0) + let start = min(from, to).timeIntervalSinceReferenceDate + let end = max(from, to).timeIntervalSinceReferenceDate + if start <= -899510401.0 && -899510401.0 < end { adjustment += .seconds(1) } + if start <= -883612801.0 && -883612801.0 < end { adjustment += .seconds(1) } + if start <= -852076801.0 && -852076801.0 < end { adjustment += .seconds(1) } + if start <= -820540801.0 && -820540801.0 < end { adjustment += .seconds(1) } + if start <= -789004801.0 && -789004801.0 < end { adjustment += .seconds(1) } + if start <= -757382401.0 && -757382401.0 < end { adjustment += .seconds(1) } + if start <= -725846401.0 && -725846401.0 < end { adjustment += .seconds(1) } + if start <= -694310401.0 && -694310401.0 < end { adjustment += .seconds(1) } + if start <= -662774401.0 && -662774401.0 < end { adjustment += .seconds(1) } + if start <= -615513601.0 && -615513601.0 < end { adjustment += .seconds(1) } + if start <= -583977601.0 && -583977601.0 < end { adjustment += .seconds(1) } + if start <= -552441601.0 && -552441601.0 < end { adjustment += .seconds(1) } + if start <= -489283201.0 && -489283201.0 < end { adjustment += .seconds(1) } + if start <= -410313601.0 && -410313601.0 < end { adjustment += .seconds(1) } + if start <= -347155201.0 && -347155201.0 < end { adjustment += .seconds(1) } + if start <= -315619201.0 && -315619201.0 < end { adjustment += .seconds(1) } + if start <= -268358401.0 && -268358401.0 < end { adjustment += .seconds(1) } + if start <= -236822401.0 && -236822401.0 < end { adjustment += .seconds(1) } + if start <= -205286401.0 && -205286401.0 < end { adjustment += .seconds(1) } + if start <= -157852801.0 && -157852801.0 < end { adjustment += .seconds(1) } + if start <= -110592001.0 && -110592001.0 < end { adjustment += .seconds(1) } + if start <= -63158401.0 && -63158401.0 < end { adjustment += .seconds(1) } + if start <= 157766399.0 && 157766399.0 < end { adjustment += .seconds(1) } + if start <= 252460799.0 && 252460799.0 < end { adjustment += .seconds(1) } + if start <= 362793599.0 && 362793599.0 < end { adjustment += .seconds(1) } + if start <= 457401599.0 && 457401599.0 < end { adjustment += .seconds(1) } + if start <= 504921599.0 && 504921599.0 < end { adjustment += .seconds(1) } + if from < to { + return adjustment + } else { + return .seconds(0) - adjustment + } + } + + @available(FoundationPreview 6.2, *) + public typealias Duration = Swift.Duration +} + +@available(FoundationPreview 6.2, *) +public struct UTCClock: Sendable { + public typealias Instant = Date + public init() { } +} + +@available(FoundationPreview 6.2, *) +extension UTCClock: Clock { + @available(FoundationPreview 6.2, *) + public func sleep(until deadline: Date, tolerance: Duration? = nil) async throws { + // this needs a runtime function adjustment (for now it is "good enough" until we integrate the wall clock sleep function in the concurrency runtime) + try await ContinuousClock().sleep(until: .now.advanced(by: .seconds(deadline.timeIntervalSinceNow)), tolerance: tolerance) + } + + @available(FoundationPreview 6.2, *) + public var now: Date { + Date() + } + + @available(FoundationPreview 6.2, *) + public var minimumResolution: Duration { + .nanoseconds(1) + } + + @available(FoundationPreview 6.2, *) + public static var systemEpoch: Date { + Date(timeIntervalSinceReferenceDate: 0) + } +} diff --git a/Tests/FoundationEssentialsTests/UTCClockTests.swift b/Tests/FoundationEssentialsTests/UTCClockTests.swift new file mode 100644 index 000000000..cf568dd6a --- /dev/null +++ b/Tests/FoundationEssentialsTests/UTCClockTests.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(TestSupport) +import TestSupport +#endif + +#if canImport(FoundationEssentials) +@testable import FoundationEssentials +#endif + +@available(FoundationPreview 6.2, *) +final class UTCClockTests : XCTestCase { + + func testAdvancingDate() { + let date = Date(timeIntervalSince1970: 1000000000) + + // Test advancing by a positive duration + let duration = Duration.seconds(3600) + let advancedDate = date.advanced(by: duration) + XCTAssertEqual(advancedDate.timeIntervalSince1970, 1000003600) + + // Test advancing by a negative duration + let negativeDuration = Duration.seconds(-3600) + let reverseAdvancedDate = date.advanced(by: negativeDuration) + XCTAssertEqual(reverseAdvancedDate.timeIntervalSince1970, 999996400) + + // Test advancing with fractional seconds + let fractionalDuration = Duration.seconds(1.5) + let fractionalAdvancedDate = date.advanced(by: fractionalDuration) + XCTAssertEqual(fractionalAdvancedDate.timeIntervalSince1970, 1000000001.5) + } + + func testDateDurationTo() { + let start = Date(timeIntervalSince1970: 1000000000) + let end = Date(timeIntervalSince1970: 1000003600) + + // Test positive duration + let duration = start.duration(to: end) + XCTAssertEqual(Duration.seconds(3600), duration) + + // Test negative duration + let reverseDuration = end.duration(to: start) + XCTAssertEqual(Duration.seconds(-3600), reverseDuration) + + // Test with fractional seconds + let fractionalEnd = Date(timeIntervalSince1970: 1000000001.5) + let fractionalDuration = start.duration(to: fractionalEnd) + XCTAssertEqual(Duration.seconds(1.5), fractionalDuration) + } + + func testLeapSeconds() { + // Test a period from 1971 to 2017 that includes 27 leap seconds + let start = Calendar(identifier: .gregorian).date(from: DateComponents(timeZone: .gmt, year: 1971, month: 1, day: 1))! + let end = Calendar(identifier: .gregorian).date(from: DateComponents(timeZone: .gmt, year: 2017, month: 1, day: 1))! + + let leapSeconds = Date.leapSeconds(from: start, to: end) + XCTAssertEqual(leapSeconds, .seconds(27)) + + // Test that leap seconds in the reverse direction have the opposite sign + let reverseLeapSeconds = Date.leapSeconds(from: end, to: start) + XCTAssertEqual(reverseLeapSeconds, .seconds(-27)) + + // Test a period with no leap seconds + let noLeapStart = Calendar(identifier: .gregorian).date(from: DateComponents(timeZone: .gmt, year: 2020, month: 1, day: 1))! + let noLeapEnd = Calendar(identifier: .gregorian).date(from: DateComponents(timeZone: .gmt, year: 2023, month: 1, day: 1))! + + let noLeapSeconds = Date.leapSeconds(from: noLeapStart, to: noLeapEnd) + XCTAssertEqual(noLeapSeconds, .seconds(0)) + } + + func testUTCClock() { + let clock = UTCClock() + + // Test that now returns the current date + let now = clock.now + let currentDate = Date() + XCTAssertEqual(now.timeIntervalSince(currentDate).magnitude < 1, true) + + // Test epoch is January 1, 2001 + XCTAssertEqual(UTCClock.systemEpoch, Date(timeIntervalSinceReferenceDate: 0)) + + // Test minimum resolution + XCTAssertEqual(clock.minimumResolution, .nanoseconds(1)) + } +}