Make implicit units on TimeInterval explicit

Hi everybody,

I'd like to improve the TimeInterval type in the Swift Standard Library since I think it doesn't fit Swifts goals.

The Problem

In our projects we are often seeing code like this:

// property example
let pollingInterval: TimeInterval = 5

// method example
func animate(duration: TimeInterval = 20, animations: () -> Void) {
    // do something
}

// method usage example
animate(duration: 0.5, animations: { /* do something */ })

As most of you will know, the TimeInterval type is basically a typealias for Double.
The documentation also states:

A TimeInterval value is always specified in seconds; (...).

What I don't like about the TimeInterval type regarding Swifts goal for being an expressive language is that the time unit is hidden to the reader of Swift code. This not only makes code harder to understand for Swift newbies but it's also more error prone since people are doing unit conversion calculation manually:

let intervalThreshold: TimeInterval = 6 * 60 * 60 // 6 hours

Also often times (as you can see in the above real world example) the actual time which was meant here is documented via a comment so the reader knows the intention. This again is not only unnecessary extra work, but again error prone when the code is being changed.

Proposed Solution

In the type DispatchTimeInterval which is very similar to the TimeInterval type in usage we have a solution for this. Replacing TimeInterval in the above example code with DispatchTimeInterval would look something like this:

// property example
let pollingInterval: DispatchTimeInterval = .seconds(5)

// method example
func animate(duration: DispatchTimeInterval = .seconds(20), animations: () -> Void) {
    // do something
}

// method usage example
animate(duration: .milliseconds(500), animations: { /* do something */ })

As you can see, the units are now explicit. We could implement the same behavior for TimeInterval by extending it like this:

extension TimeInterval {
    // MARK: - Computed Type Properties
    internal static var secondsPerDay: Double { return 24 * 60 * 60 }
    internal static var secondsPerHour: Double { return 60 * 60 }
    internal static var secondsPerMinute: Double { return 60 }
    internal static var millisecondsPerSecond: Double { return 1_000 }
    internal static var microsecondsPerSecond: Double { return 1_000 * 1_000 }
    internal static var nanosecondsPerSecond: Double { return 1_000 * 1_000 * 1_000 }

    // MARK: - Type Methods
    /// - Returns: The time in days using the `TimeInterval` type.
    public static func days(_ value: Double) -> TimeInterval {
        return value * secondsPerDay
    }

    /// - Returns: The time in hours using the `TimeInterval` type.
    public static func hours(_ value: Double) -> TimeInterval {
        return value * secondsPerHour
    }

    /// - Returns: The time in minutes using the `TimeInterval` type.
    public static func minutes(_ value: Double) -> TimeInterval {
        return value * secondsPerMinute
    }

    /// - Returns: The time in seconds using the `TimeInterval` type.
    public static func seconds(_ value: Double) -> TimeInterval {
        return value
    }

    /// - Returns: The time in milliseconds using the `TimeInterval` type.
    public static func milliseconds(_ value: Double) -> TimeInterval {
        return value / millisecondsPerSecond
    }

    /// - Returns: The time in microseconds using the `TimeInterval` type.
    public static func microseconds(_ value: Double) -> TimeInterval {
        return value / microsecondsPerSecond
    }

    /// - Returns: The time in nanoseconds using the `TimeInterval` type.
    public static func nanoseconds(_ value: Double) -> TimeInterval {
        return value / nanosecondsPerSecond
    }
}

Then the example code would read exactly like with DispatchTimeInterval:

// property example
let pollingInterval: TimeInterval = .seconds(5)

// method example
func animate(duration: TimeInterval = .seconds(20), animations: () -> Void) {
    // do something
}

// method usage example
animate(duration: .milliseconds(500), animations: { /* do something */ })

Much more readable and less error prone in cases conversions are needed:

let intervalThreshold: TimeInterval = .hours(6)

One more thing ...

While my main pitch here is to make the creation of TimeInterval objects more expressive, we could also improve usage of TimeInterval variables in case they need to be converted to other unit scales. For this we could add the following content to the TimeInterval extension above:

extension TimeInterval {
    // MARK: - Computed Type Properties
    // ...

    // MARK: - Computed Instance Properties
    /// - Returns: The `TimeInterval` in days.
    public var days: Double {
        return self / TimeInterval.secondsPerDay
    }

    /// - Returns: The `TimeInterval` in hours.
    public var hours: Double {
        return self / TimeInterval.secondsPerHour
    }

    /// - Returns: The `TimeInterval` in minutes.
    public var minutes: Double {
        return self / TimeInterval.secondsPerMinute
    }

    /// - Returns: The `TimeInterval` in seconds.
    public var seconds: Double {
        return self
    }

    /// - Returns: The `TimeInterval` in milliseconds.
    public var milliseconds: Double {
        return self * TimeInterval.millisecondsPerSecond
    }

    /// - Returns: The `TimeInterval` in microseconds.
    public var microseconds: Double {
        return self * TimeInterval.microsecondsPerSecond
    }

    /// - Returns: The `TimeInterval` in nanoseconds.
    public var nanoseconds: Double {
        return self * TimeInterval.nanosecondsPerSecond
    }

    // MARK: - Type Methods
    // ...
}

Then something like the following would be possible:

let timeInterval: TimeInterval = .hours(6)

timeInterval.days // => 0.25
timeInterval.hours // => 6
timeInterval.minutes // => 360
timeInterval.seconds // => 21600
timeInterval.milliseconds // => 21600000
timeInterval.microseconds // => 21600000000
timeInterval.nanoseconds // => 21600000000000

Framework available

The above suggested extension is actually taken from my open source library HandySwift on GitHub, the extension code can be found here, tests for it are written there. Feel free to include HandySwift into one of your projects to try out how this change would feel in the real world. We are using it since quite a while now and feel like it should be part of Swift – which is why I'm posting this thread.

What do you think?

9 Likes

I've just also put the above examples into a Playground and posted it as a gist on GitHub:
https://gist.github.com/Dschee/4185b17199a7e769e447d1a3beeb6c42

In theory I think this would be good, but as the Foundation team will attest, days and times and hours are not as standard as we’d like to think they are, and extensions like these, especially used with days, are triggers for error-prone code.

Take for example if I have an appointment at 5pm this Saturday and 5pm this Sunday. Just add the time interval for .days(1)... nope. This Sunday in eastern Australia where I live is actually a 25 hour day due to daylight savings changes.

Generally anyone playing with dates and reminders etc in apps should be aware of this, and generally they won’t be doing these naive implementations, but adding calendrical conveniences that imply assumptions that don’t hold seems a bad idea considering the amount of time bugs that have come around, even from companies like Apple.

I think maybe something at the seconds level would be cool, but I’m not sure then of the value...

In general I would love for this to be a good idea, but I worry about the implications of these changes beyond the second scope, as even some hours have “leap seconds” etc...

10 Likes

Unless I'm missing something, the original poster didn't mention anything about dates, only about intervals. So timezones, leap seconds etc are not relevant here.

However the problem here is that the pitch is about Foundation, which is owned by Apple and not by Swift.

3 Likes

;-) if only our solar system was designed by a good developer...

But I guess it would encourage people to write problematic (yet easy to implement) code.
So, I wouldn't say the idea is bad, but it's something better placed in personal extensions.

1 Like

Well, I see the point about calendars and dates. To be honest I didn't think of them, nor did anyone of my fellows who used this API. It was merely there to replace time intervals which were alternatively calculated manually. So, while I see the point, I don't think the argument holds true for intervals, like @nick.keets said already.

Regarding the fact that I'm pitching this for Foundation: What I meant is the Swift Standard Library (which I always think of as the successor of Foundation). I'm correcting the text of my original post.

And just for reference, I found this line in the GitHub Swift project. So I'm guessing it is part of what we can change here.

@Rod_Brown The value when days and hours wouldn't be included would be expressiveness since it would be .seconds(5.0) or .milliseconds(750) instead of the implicit unit-based 5.0 and 0.75.

Swift has 5 "Standard Library" like things. See here: Good PDF with the Swift 4 Standard Library - #16 by xwu

4 out of 5 are managed by Apple, so any change would have to go through Apple's pipeline with implementation times measured in years.

I think this is the main issue with your proposal. Otherwise I think it is an obvious improvement and I don't see any issues with definitions up to hours. The fact that Dispatch has re-implemented this is further indication of its usefulness.

1 Like

My reference was to issues specifically related to “length of day,” “length of hour” etc where they are variable, not constants.

@Jeehut: as @Tino says, these limitations of time intervals being both calendrical and contextual are precisely why Foundation doesn’t have these types of extensions and conveniences: because they are extremely difficult and error prone to reason about, and while “generally” an hour is 60 minutes, which are each “generally” 60 seconds, these assumptions quickly lead a developer into trouble. I wouldn’t encourage an API to deliberately assert that these things are constant even if “generally” they are.

3 Likes

I'm no time expert, but how can the duration of hour be variable? Sure, 4:00am today may be 1 hour and 1 second after 3:00, but an 1 hour interval should always be 3600 seconds.

Go and Python seem to have no problem defining interval types:

As I said in my initial post, I don’t have an issue with groups of seconds etc :blush: my issue was directly with extensions such as secondsPerHour, secondsPerDay etc which are explicitly shown in your pitch.

At the second level, however, I think it’s all fine. It’s when you state “a day is 24 x 60 x 60 seconds long” which is an incorrect statement and leads to risky coding around days (and why we have the Calendar class). Anything above the second precision ends up with errors because of the issues with leap seconds etc. what you’re meaning is “standard day”, “standard hour” etc but these things are dangerous because people won’t think about the fact they aren’t appropriate when calculating days and times, that’s all.

As @Tino said, I’d say these things belong in 3rd party libraries precisely because they have a high risk of leading to developers misusing these values.

What about Leap Seconds? You end up with some hours actually being 3601 seconds, and some minutes being 61 seconds, for example.

While the general interval holds, which I assume is why they exist in Python and Go, but beyond “in general”, the specifics breaks in practice often enough that giving such APIs is not a good idea because people use it without considering all the wider implications of days, day lengths, etc, which can be mind-bogglingly complex! :smile:

Apple have done a lot of Foundation talks about why days, times, etc can be really hard and particularly error prone. I recommend anyone interested look them up. :slight_smile:

1 Like

Just a clear example where this is an issue:

let saturday1PM = Date(...)
let sunday1PM = saturday1PM.addingTimeInterval(.days(1)) // :open_mouth:

In this week, where you are, this code will probably work.
Where I am, this code won’t work. It will make a Date instance which is actually Sunday at 12pm, not 1pm. Next week, it will work fine here.

Time zones, leap seconds, and dates and time intervals are all linked and conveniences like this give the illusion of correct code when it may or may not actually be correct. Perhaps the programmer really did mean exactly 24 standard hours, 60 standard minutes an hour. Or perhaps (more likely) he meant 1 calendrical day later which does rely on time zones etc. Adding conveniences for days and hours makes it very easy for someone to bypass using Calendar and make this assumption without actually making an assessment whether they really do mean a second count or whether calendar computations are appropriate.

1 Like

Resident Time Lord chiming in...

I think the idea has merit. I agree that it would be great to have a nicer API to deal with explicit intervals.

However, I absolutely do not think this should be on TimeInterval. TimeInterval is problematic because it's tied very strongly to the Date API. If we go down this route, we need to do it in such a way that the API is explicitly clear that this has nothing to do with calendrical values. However, even then, you're still going to have people abusing this, because of the underlying "rawValue" that is literally "a number of SI seconds". I predict you'd end up seeing things like

let oneDay = AnimationInterval.days(1)
let tomorrow = Date().addingTimeInterval(TimeInterval(oneDay))

Because the proposed API in its current form would be so easily abused, I am an emphatic -1 on the concept, but a +½ on the concept. I think it's worth thinking about, but I'm skeptical that we'd arrive at something that isn't trivially abusable.

4 Likes

Maybe something like

extension Date {
	func adding(days: Int, calendar: Calendar =	Calendar.current) -> Date {
		return calendar.date(byAdding: .day, value:	days, to: self)!
	}
}

  let tomorrow = today.adding(days: 1)

could offer some protection from abuse (but I'm already scared because the Calendar-method returns an Optional ;-)

I'm actively researching ways to make the Date API better in Swift. As currently vended by Foundation, it's not very salvageable; you can add more and more convenience methods, but those don't do anything to fix the underlying issues. On the other hand, you can wrap the Foundation API using a new API and come up with something nicer: GitHub - davedelong/time: Building a better date/time library for Swift

2 Likes

It seems most appropriate for TimeInterval to be more closely related to UnitDuration.

However, I don't think there is a clear exposed dependency model of Foundation and Dispatch to make this work from an API standpoint (say, with Foundation extending Dispatch methods to take or expose UnitDuration instances, or a TimeDuration.init(UnitDuration) initializer.

Generally, duration types are limited in units beyond hours to prevent developer confusion between "one standard length day in seconds" vs "this time tomorrow" - the second of which can be affected by DST, time zone changes in mobile devices, clock drift adjustments, leap seconds, etc.

Nice!

In addition to the libraries listed, you may also be able to take inspiration from C++'s std::chrono. While I can't vouch for its capabilities, the Rust chrono library might also be a useful source of inspiration due to the feature overlap of Rust and Swift.

While Chronology looks really amazing, I think it needs a much wider overhaul of the existing APIs. What I am pitching here in the first place is to fix the implicitness of the existing TimeInterval type. My main suggestion is to add .seconds(Double), .milliseconds(Double) etc. – that's just fixing a small thing.

And if I understand it correctly, the general reaction on this suggestion is very positive, except when we would also add .days (and maybe .hours). Having helpers up to .seconds doesn't seem to be something anybody found bad. Some people seem to think, this doesn't solve the underlying problem (like @davedelong) but that's not what I'm suggesting here. That should be part of a different thread IMHO.

So, the next step for me would be to write a proposal, no? Or is this too early?
I'm still new to the Swift Forums ...

2 Likes

One downside of doing this even for seconds and smaller intervals is that the API gives the appearance of an API contract regarding exactness, but doesn't actually make the contract.

For example, .milliseconds(700) + .milliseconds(300) looks like it adds up to one second, but it probably doesn't.

Using a raw number doesn't give any hint of such a promise. Furthermore, a unit-less number, even when restricted to seconds, doesn't give any hint of a connection to Calendar units.

What I'm saying is, the defects of using a raw number (lack of expressiveness was what you said) may actually be an advantage in this computationally treacherous area. :slight_smile:

We already have Measurement in Foundation, e.g. Measurement(value: 5, unit: UnitDuration.seconds). Why add yet another duration type?