Skip to content

Add an implementation of the UTCClock and associated tests #1344

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions Sources/FoundationEssentials/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ add_library(FoundationEssentials
OutputBuffer.swift
Platform.swift
SortComparator.swift
UTCClock.swift
UUID_Wrappers.swift
UUID.swift
WASILibc+Extensions.swift
Expand Down
140 changes: 140 additions & 0 deletions Sources/FoundationEssentials/UTCClock.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
95 changes: 95 additions & 0 deletions Tests/FoundationEssentialsTests/UTCClockTests.swift
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this test need a @testable import? If not, I think you can remove this. If it does, I think you'll want an #else branch that has @testable import Foundation to make sure this works for FOUNDATION_FRAMEWORK where FoundationEssentials doesn't exist

#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))
}
}
Loading