[Pitch] UTCClock

Introduction

The proposal for Clock, Instant and Duration was left with a future direction to address feedback for the need of clocks based upon the time measured by interacting with the user displayed clock, otherwise known as the "wall clock".

This proposal introduces a new clock type for interacting with the user displayed clock, transacts in instants that are representations of offsets from an epoch and defined by the advancement from a UTC time.

Motivation

Clocks in general can express many different mechanisms for time. That time can be strictly increasing, increasing but paused when the computer is not active, or in a clock in the traditional non-computing sense that one schedules according to a given time of day. The latter one must have some sort of mechanism to interact with calendarical and localized calculations.

All three of the aforementioned clocks all have a concept of a starting point of reference. This is not a distinct requirement for creating a clock, but all three share this additional convention of a property.

Proposed solution and example

In short, a new clock will be added: UTCClock. This clock will have its Instant type defined as Date. There will also be affordances added for calculations that account for the edge cases of leap seconds (which currently Date on its own does not currently offer any sort of mechanism either on itself or via Calendar). Date has facilities for expressing a starting point from an epoch, however that mechanism is not shared to ContinuousClock.Instant or SuspendingClock.Instant. All three types will have an added new static property for fetching the epoch - and it is suggested that any adopters of InstantProtocol should add a new property to their types to match this convention where it fits.

Usage of this UTCClock can be illustrated by awaiting to perform a task at a given time of day. This has a number of interesting wrinkles that the SuspendingClock and ContinousClock wouldn't be able to handle. Example cases include where the deadline might be beyond a daylight savings time. Since Date directly interacts with Calendar it then ensures appropriate handling of these edges and is able to respect the user's settings and localization.

let calendar = Calendar.current
var when = calendar.dateComponents([.day, .month, .year], from: .now)
when.day = when.day.map { $0 + 1 }
when.hour = 8

if let tomorrowMorning8AM = calendar.date(from: when) {
  try await UTCClock().sleep(until: tomorrowMorning8AM)
  playAlarmSound()
}

This can be used not only for alarms, but also scheduled maintenance routines or other such chronological tasks. The applications for which span from mobile to desktop to server environments and have a wide but specific set of use cases. It is worth noting that this new type should not be viewed as a replacement since those others have key functionality for representing behavior where the concept of time would be inappropriate to be non-monotonic.

Detailed design

These additions can be broken down into three categories; the UTCClock definition, the conformance of Date to InstantProtocol, and the extensions for vending epochs.

The structure of the UTCClock is trivially sendable since it houses no specific state and has the defined typealias of its Instant as Date. The minimum feasible resolution of Date is 1 nanosecond (however that may vary from platform to platform where Foundation is implemented).

public struct UTCClock: Sendable {
  public typealias Instant = Date
  public init()
}

extension UTCClock: Clock {
    public func sleep(until deadline: Date, tolerance: Duration? = nil) async throws
    public var now: Date { get }
    public var minimumResolution: Duration { get }
}

The extension of Date conforms it to InstantProtocol and adds one addition "near miss" of the protocol as an additional function that in practice feels like a default parameter. This duration(to:includingLeapSeconds:) function provides the calculation of the duration from one point in time to another and calculates if the span between the two points includes a leap second or not. This calculation can be used for historical astronomical data since the irregularity of the rotation causes variation in the observed solar time. Those points are historically fixed and are a known series of events at specific dates (in UTC)[^utclist].

extension Date: InstantProtocol {
    public func advanced(by duration: Duration) -> Date
    public func duration(to other: Date) -> Duration
}

extension Date {
    public static func leapSeconds(from start: Date, to end: Date) -> Duration
}

Usage of the duration(to:) and leapSeconds(from:to:) works as follows to calculate the total number of leap seconds:

let start = Calendar.current.date(from: DateComponents(timeZone: .gmt, year: 1971, month: 1, day: 1))!
let end = Calendar.current.date(from: DateComponents(timeZone: .gmt, year: 2017, month: 1, day: 1))!
let leaps = Date.leapSeconds(from: start, to: end)
print(leaps) // prints 27.0 seconds
print(start.duration(to: end) + leaps) // prints 1451692827.0 seconds

It is worth noting that the usages of leap seconds for a given range is not a common use in most every-day computing; this is intended for special cases where data-sets or historical leap second including durations are strictly needed. The general usages should only require the normal duration(to:) api without adding any additional values. Documentation of this function will reflect that more "niche" use case.

An extension to Date will be made in Foundation for exposing an epoch similarly to the properties proposed for the SuspendingClock and ContinousClock. The Date epoch will be defined as the Date(timeIntervalSinceReferenceDate: 0) which is Jan 1 2001.


extension Date {
    public static var epoch: Self { get }
}

Impact on existing code

This is a purely additive set of changes.

Alternatives considered

It was considered to add a protocol joining the epochs as a "EpochInstant" but that offers no real algorithmic advantage worth introducing a new protocol. Specialization of functions should likely just use where clauses to the specific instant or clock types.

It was considered to add a new Instant type instead of depending on Date however this was rejected since it would mean re-implementing a vast swath of the supporting APIs for Calendar and such. The advantage of this is minimal and does not counteract the additional API complexity.

It was considered to add a near-miss overload for duration(to:) that had an additional parameter of includingLeapSeconds this was dismissed because it was considered to be too confusing and may lead to bugs where that data was not intended to be used.

[^utclist] If there are any updates to the list that will be considered a data table update and require a change to Foundation.

8 Likes

I very much like the idea. However it is worth noting that ContinuousClock and SuspendingClock are part of the standard library, whereas Date (and Calendar) are part of Foundation.

I'd rather see a new date type (and calendar stuff; including this proposed clock) in the standard library than adding/restricting to just Foundation.

5 Likes

It was a consideration to home this elsewhere but immediately upon usage you end up needing localization support from Calendar; which squarely makes it belong with the other Foundation APIs.

It is worth noting that this will not require the full Foundation to work - it can be used with just the Foundation essentials; making it rather decent on cost wise.

5 Likes

Sorry, which module will this go in? I don't see "Foundation" mentioned until the second paragraph of the detailed design section, and FoundationEssentials isn't mentioned at all.

4 Likes

Overall +1 on this. This has been a missing feature since we initially pitched the clock types and glad we are tackling this now. Also thanks for clarifying that this is intended to land as part of FoundationEssentials.

Given the recent conversation about issues with the floating-point representation that underpins Date and uncertainty whether this is fixable, is this pitch doubling down on the known problem?

Can there be alternatives considered such that any new APIs are generic over, say, some UTCClockInstantProtocol so that Date could be drop-in replaced?

Otherwise, by introducing additional API here that takes only a concrete Date, it is a self-fulfilling rationale that it results in an even vaster swath of APIs that would need replacing if Date needs to be replaced.

15 Likes

What about renaming this Date.UTCClock? Then a future replacement for Date could take the top-level UTCClock name and use itself as that type’s Instant.

What is the "epoch" value that will be added to ContinuousClock and SuspendingClock? Is it just .zero, or be "platform defined but unspecified"?

So that proposal is pitching effectively adding those as a zero point in practice. Date.epoch obviously not zero (it is outlined in the pitch as the reference date).

Im not sure any platform implements its system clocks as anything but zero. If there is a platform that does implement that reference point differently then that will be the implementation.

To be clear; this proposal is not suggesting any sort of change in the representation or presentation of Date itself. It is possible that Swift itself grows a mechanism to change backing implementations to types then perhaps at that future point it could be considered; but that is a strong pre-requisite. The cost currently of totally discarding Date and going down the path of re-implementing the supporting parts would mean one of two things: a) a huge breaking change to the ABI (which is 100% a non-starter for supporting folks deploying apps, operating systems, stable frameworks etc) or b) introducing a totally parallel implementation of the calendrical system (which is both non-trivial of a consideration but also disruptive and potentially breaking).

I think the path forward for the suggestion is coming up with a design for how to re-core out the backing storage of Date; this proposal implementation wise would be orthogonal to that. Those efforts are not blocking the implementation or discussion of the merits or use of a UTCClock.