[Pitch] Clock, Instant, Date, and Duration

no one is saying that the proposed APIs should be calendar aware, just that it would be hugely motivating to have a way to represent leap seconds within a timestamp type. we will have three spare bits anyway, why not use one as a leap-second flag? the leap second flag would work with APIs such as Comparable, but APIs that don’t know about leap-seconds can simply ignore it, or never set it in the first place.

if we exclude leap seconds from the Date representation we ultimately settle on, i anticipate many users will simply end up wrapping it in another custom type which does nothing but add a leap:Bool field.

1 Like

Just to clarify what you are talking about is having an extra accessor of var isLeapSecond: Bool { get set } on Date?

I think the objection that is reasonable to raise is that if there is such a field - developers would expect that field is populated when accessing a Date. Either by .now or construction via advancement etc. The problem with that is that to access that if it is a leap second or not would need to access which calendar that date would be represented in. Date today is merely a call to CFAbsoluteTimeGetCurrent which just is a fancy way of on Darwin and Linux saying gettimeofday and Windows saying GetSystemTime. Neither of those have a way to determine a calendar or if a second is a leap second or not.

Perhaps wrapping is the right move? Because that lets the user know the actual intent of the payload of the Date with that additional info and it removes the expectation that the value is set or not.

Also it is worth noting that any time that value would be sent through the bridge into objc that information would be destroyed. (since NSDate is a tagged pointer on most platforms it only stores as 56 bits of floating point data)

7 Likes

based on my recent experiences writing a great deal of code which deals with a lot of Timestamps (Dates, in your proposal), i have found that the benefits of having a way to represent leap seconds inline in a Date greatly outweigh the drawbacks. i have tried the two-tiered approach, and found that it doesn’t really add anything but needless indirection.

i will also add that “naked” timestamps (no leap second information) are usually used for things like windowing or pagination. in this situation, we are almost always either flooring to the previous second, or rounding up to the next second boundary (or an even coarser time increment), in which case the leap second can be safely ignored. this satisfies:

11:59:59 < 11:59:60 < 12:00:00

i am not concerned about accidentally doing operations (like time-based sorting) that depend on leap seconds on timestamps lacking this information. what i am way more concerned about is passing a timestamp containing leap second information through an API that strips away this information, or losing track of a leap second due to pedantic destructurings and restructurings required by not having a way to represent them inline in a Date.

The problem here is that 11:59:59 is a value of seconds + nanoseconds since 1970 here. So the next second past that is just that value + 1 second and not 11:59:60 or 12:00:00. The only way to get to a hours/minutes/second day/month/year etc is via a Calendar. So in truth it seems like what you really want is DateComponents not Date. Which does account for leap X's. One could envision a custom Clock that does account for this type of thing.

This is just a sketch and not intended for real use w/o some more thinking.

struct CalendarClock: ClockProtocol, UnsafeSendable {
  let calendar: Calendar
  
  var now: Instant {
    Instant(calendar: calendar, date: .now)
  }
  
  func sleep(until deadline: Instant) async throws {
    // omitted for brevity
  }
  
  struct Instant: InstantProtocol, UnsafeSendable {
    let calendar: Calendar
    let date: Date
    
    func advanced(by duration: Duration) -> CalendarClock.Instant {
      Instant(calendar: calendar, date: calendar.date(byAdding: DateComponents(second: Int(duration.seconds), nanosecond: Int(duration.nanoseconds)), to: date)!)
    }
    
    func duration(to other: CalendarClock.Instant) -> Duration {
      let c = calendar.dateComponents([.second, .nanosecond], from: date, to: other.date)
      return .seconds(c.second!) + .nanoseconds(c.nanosecond!)
    }
    
    static func < (lhs: CalendarClock.Instant, rhs: CalendarClock.Instant) -> Bool {
      return lhs.date < rhs.date
    }
  }
}

That would account for leap seconds, leap years, leap months etc.

6 Likes

11:59:59 incremented by 1 is 12:00:00, which produces a 2-second page. which is generally what we want, since many external APIs that accept seconds since 1970 offer no other way to access data binned into the second 11:59:60.

I think the disconnect here is that "Date at the standard library level" proposed here has no notion of anything calendrical; it represents strictly an offset in seconds and nanoseconds from a fixed point in time, and so it has no problem with representing any instant of time in the next however many billion years whether it is a leap second or not. But, unless I misunderstand, the facilities to be vended by the standard library will have no way of converting anything that you'd call a timestamp--or, for that matter, anything you'd call a date--into a Date.

It falls to the calendrical operations of the Foundation layer to make that translation from timestamp to Date possible, just as Foundation provides locale-aware string sorting. (Hence, as I understand it, the desire to mimic that division of labor for String by sinking the non-calendrical facilities of Date into the standard library without creating a distinct type. But maybe the non-calendrical nature of the standard library type simply cannot be reconciled with a name such as Date that strongly implies something on a calendar.)

Nonetheless, "Date at the standard library level" has no notion of a leap second or leap day just as it doesn't have any notion of midnight or February. I can see the argument for Foundation to supply maybe convenience APIs such as isLeapSecond in the same way it'd be convenient to be able to query isLeapDay, isMidnight, or isMonth(2). And there could be an argument to cache the result of isLeapSecond as a stored value (which should more accurately be isKnownLeapSecond, since the same instant in the future might not be a leap second today but could be designated as such six months in advance of the instant in time), but it's unclear why this of all calendrical concepts should be cached, and indeed there would be implementation complexities.

9 Likes

The purpose of a leap second is to disambiguate what would otherwise be a non-monotonic representation of time. Without leap seconds, there really is not much you can do with a raw Unix time... It cannot conform to Comparable, nor can it even conform to Equatable, because the same Unix time can refer to up to two different instants in time.

Expressing time instants in the future, as in your example, is inherently ambiguous, for the exact reasons you mention. This ambiguity is no different from the sort we encounter when expressing time instants in the past. For time instants in the future, the only sensible interpretation is “no leap second”, since you would probably want any condition you have specified using that time instant to trigger the first time that time instant is reached. I do not perceive any value in making this Optional or trying to model this ambiguity.

A quantity of seconds/nanoseconds from Jan 1 1970 is distinctly comparable to another quantity of seconds/nanoseconds from Jan 1 1970.

Any representation of a calendrical instant would need to have the date as well as the calendar; that is beyond the proposal and an implementation left for other layers to solve. The proposal clearly leaves this out for this very reason (and some other layering issues w.r.t. where to source that information etc).

Date has never represented this, nor realistically do I think it should since that would require additional information; e.g. the Calendar.

To recap some definitions here since I think this has gone a bit off track:

Date: A Date value encapsulates a single point in time, independent of any particular calendrical system or time zone. Date values represent a time interval relative to an absolute reference date.

Wall Clock Time: Time like reading from a clock. This may be adjusted forwards or backwards for numerous reasons; in this context, it is time that is not specific to a time zone or locale, but measured from an absolute reference date. Network updates may adjust the drift on the clock either backwards or forwards depending on the relativistic drift, clock skew from inaccuracies with the processor, or from hardware power characteristics.

Given those definitions Date falls into a wall clock time, but does have a concept of now that is non-monotonic; i.e. it may have two sequential calls to now move backwards when the system clock is adjusted via network update time etc. However that does not mean that it has any concept of leap anythings. Because a leap second does not pause the universe and skip over a duration value since the reference point. Leap seconds just adjust the component of the date representation. If the type in question was calendrical then it would be DateComponents.

That being all said; the proposal does not preclude any development of a Calendar aware clock type. As a point of reference it allows that better than what is done today since the generic types of APIs that take InstantProtocol could equally take a Monotonic.Instant as it does Date or as it would SomeCustomClock.Instant.

I think that if there is a need for a calendrical aware clock exists then it is a great spot for development of complementary APIs in packages or in application layers.

To be super clear, this proposal does not aim to capture calendrical calculations; only the basis for time representations as they exist with regards to execution and transmission and refining the existing behavior to a non-lossy system.

13 Likes

I agree with this. Leap seconds should not live at this layer; they belong to the layer including calendars, which is necessarily above the layer @Philippe_Hausler is talking about.

14 Likes

+1 to that. to make it more obvious perhaps better call it Time.

Others have made this point but I’d like to use this example to show there may be multiple clocks providing separate sources of truth for “now” using the same Instant type. NTP and WallClocks may both use Instant = Date but not reflect the same now value. I understand the convenience of Instant.now but this precludes multiple clocks from using the same Instant type, which is pretty inconvenient and doesn’t model reality as well as asking the clock itself for now.

4 Likes

i really feel like i need to push back on this, because this seems like it could open up a lot of potential for misuse. seconds since 1970 should not be Equatable, because it is perfectly possible to record two identical time values from two different instants in time. it follows that seconds since 1970 cannot be Comparable or Hashable either. any such operations ought to go through properties like date1.value == date2.value or something similar.

i should also note that Equatable and friends are special in that they can be implemented through recursive synthetic conformances. this makes it very easy to accidentally mark a struct or enum containing a Date, among other members/associated values, as Equatable/Comparable/Hashable, without accounting for the potential implications of such a conformance. therefore, the bar for conforming to these protocols ought to be higher than for other protocols in the standard library.

I don't understand: how can there be two different instants in time that are both exactly n seconds + m nanoseconds after 1970-01-01T00:00:00Z?

after a (positive) leap second occurs, the number of seconds since 1970-01-01T00:00:00Z repeats in order to ensure that midnight always occurs on a multiple of 86400 seconds.

...in Unix time, which is not a monotonic representation of seconds since 1970-01-01T00:00:00Z. This proposal has Date representing the actual elapsed time since 1970-01-01T00:00:00Z, not Unix time.

4 Likes

what you are describing is a leap second representation. the purpose of a leap second is to keep track of the seconds offset needed to convert Unix time to “actual elapsed time” since 1970-01-01T00:00:00Z.

if your Date representation does not enforce a constant midnight-to-midnight interval, by definition, it contains a leap second representation, and is therefore a “calendrical” API, of the sort you have mentioned.

2 Likes

I don't understand what you are getting at. Date is proposed to represent the number of seconds and milliseconds elapsed since 1970-01-01T00:00:00Z. This is an unambiguous and comparable value that increments monotonically. There is no notion of midnight at all; indeed, there is no notion of any subdivision of time whatsoever other than the relationship of seconds to nanoseconds.

4 Likes

how could you implement such an API without knowledge of calendrical concepts like leap seconds?

2 Likes

For me, this debate among people which probably have been using the type in question back in the days when it was called NSDate, is a very strong indication that deviation from the common meaning of "date" will cause big confusion for newcomers.
This might even be harmful enough to warrant a breaking change, but afaics, it's not even needed to touch this issue at all as part of this pitch: As much as I would prefer a holistic take on the whole topic of time, I think it would be no problem to split the proposal.

1 Like

That depends on what system APIs are available. The purpose of the pitch is to detail the user-facing APIs that will abstract this away. Whether the underlying implementation will have to consult a list of leap seconds or not is immaterial.

You are correct that the end user could not then roll their own conversions from Unix time to Date without the calendrical APIs in Foundation, but that's entirely consistent with the division of labor proposed here between the standard library and Foundation. (Users similarly do not have access to Swift APIs to roll their own Unicode grapheme cluster breaking.)

2 Likes