Date.now() and other calendar thoughts

It came up in @armcknight's excellent "Surveying how Swift evolves" thread that "Date.now()" might be a reasonable addition to the date/time APIs of Foundation.

I am vehemently opposed to this idea, but I think we should have this discussion in a new thread.

Date() is one of the great hidden dependencies we inject in to our code. It relies on a host of external things, including the system's connection to the network time servers. Using Date() to retrieve the current time means that it's almost impossible to accurately test how your app will behave at other times. Usually when you want to test that, you create your own static func now() → Date extension on Date that you can then have reach in to other global state to know if you should fake the time to be something else instead.

As a concrete example of this, I used to work on an app that was only really used a few days a year (the WWDC app). In order to accurately test how the app would behave at key moments (pre-keynote, post-keynote, post-lunch, etc), we created our own static func now() → Date method and tried to used that everywhere. It worked, but only in a roundabout way. It was extremely difficult to see progression through the week, because manually advancing the time meant stopping and recompiling and re-running.

The proposed Date.now() method suffers from all of these same problems. It only slightly de-obfuscates how to get the current time, but it does nothing to break the hidden dependency on global state, nor does it make it easy to affect the current time.

Instead of a Date.now() method, I propose a whole new addition to Foundation: Clock.

In the real world, when we want to know what time it is, we look at a clock. The clock tells us the date and time of day. If we want to pretend it's a different time, we alter the clock.

Foundation, IMO, should follow this same pattern, and our code should be updated to use Clock instead of Date.

An example of what the API might look like is this:

public struct Clock {
    public static let system: Clock
    public func now() → Date
}

You could then take this further by making clocks that start at different points in time, or even tick faster or slower than the system clock:

extension Clock {
    init(startingAt: Date, flowRate: Double)
}

If you'd like to see an implementation of this, please check out the date-and-time library I'm working on, called Chronology. In your code, you'd pass around a Clock instance and ask it for the value of now() in order to get the current time.

Doing it this way means you can create a clock that runs 100x faster than real time, or that starts in the past, or the future, or even only runs fast during certain intervals (so you can speed through the boring bits and focus on the good bits)... Just about the only thing you couldn't do is make time flow backwards. :wink:

This idea doesn't begin to address some of the other fundamental problems of Foundation's date and time API, but I don't think we should encourage more hidden dependencies by introducing Date.now().

20 Likes

Clocks also represent the time in a particular time zone. I'm curious: where do time zones fit in this model? For example, I'm imagining the spy agency's wall of clocks with "now" in different time zones.

1 Like

The current foundation model has Time Zones only being relevant when it comes to formatting. “Date()” represents the current instant in time, regardless of timezone.

Under the proposed model, Clock.now() represents the current instant of the clock, independent of time zone. If you want the components of that instant in a particular timezone or calendar, you’d need to dig in to the Calendar and formatting APIs for that.

1 Like

Core Location can simulate other locations, either in Xcode via the scheme's Run options, or in the iOS simulator's Debug menu.

There could be similar options for date-time APIs, to simulate NSSystemClockDidChange and UIApplicationSignificantTimeChange notifications, etc.

I'm not sure how this would be made available to Linux and other platforms.

1 Like

Today, code that uses Date() internally cannot be unit-tested. Instead, the developer would have to write a wrapper or factory class to provide the date. While the production cod would simply call down to Date(), unit tests would be able to use a mock that returns a specific date for testing boundary conditions and the like.

To the extent that we would like to encourage better coding practices, I agree that providing a class such as Clock, which is inherently useful in both production code and in testing code, is a good idea.

2 Likes

That’s a great point, and is one I hadn’t considered.

I think though that I would prefer less magic and more straightforwardness. Environment variables or command line arguments aren’t obvious places to look when you want to customize things. They’re also not dynamically configurable at runtime. It’s not difficult to imagine a scenario where you might want multiple distinct clocks producing values at the same time.

This is where I think the use of the term Clock breaks down. A clock is a mechanism of reading time in a specific timezone, it's inherently tied to a specific timezone for the instant of time that it is measuring. I think having something called Clock that isn't tied to a timezone will cause as many bad assumptions about it at Date currently has.

I would love to offer an alternative, but as of right now I'm not sure. I thought Time would be better since it's independent of timezone and an always progressing unit of measurement... but thats basically what Date already is.

4 Likes

About the Clock API, I understand that the main utility would be for testing purposes. But, changing Date() with myTestClock.now() in the code does not seem like not a good idea. I would prefer using environment variables or adding a delegate to the Date API that would be asked what Date to return when initializing it.

I fundamentally disagree with some of the points of the Chronology: Foundation's API manifesto. In particular I like how Date instances always represents an unique point in time. I have used other implementations in other languages like Python where datetime objects are backed by years, months, days, hour, minutes and seconds and timezone conversions are a lot more cumbersome.

Thinking that Date instances simply represents a universal point in time is great for simplicity.

Why would you oppose to it? The idea would be to use Date.now instead of Date(), maybe even deprecating the last. This would enforce clarity and allow easy discoverability. Also, this would align with other languages's datetime APIs.

2 Likes

All the terms in this domain are overloaded with many incompatible meanings, so I think that trying to come up with a bulletproof API that would rule off any bad assumptions is futile. Time would be wrong because it’s the quantity being measured, not the measuring device. (You can have a clock that runs faster or keeps still, but trying to say the same about time is a much bigger stretch.)

I would like to stress what Dave already said about time zones – they only come to play when parsing or formatting times, ie. in String -> TimeType and TimeType -> String situations. So a Clock -> TimeType operation is perfectly fine without any regard to time zones.

The main intent here is splitting time representation from the time source. And I think that’s reasonable, I guess many of us already have var clock: () -> Date or something like that in code to improve testability. Sticking a time source (.now) on a time representation type (Date) is very convenient, but hides an important dependency.

5 Likes

Hi Pedro, thanks for taking the time to respond.

My main issue with Date() and Date.now() is the implied global state. Using environment variables or a delegate for the date constructor singleton doesn't eliminate the fact that we're still using global state. It would also mean that I can't have two clocks, for whatever reason.

I do too, and if you poke through Chronology you'll see they're still there, but they're called Instants.

The problem with Date is 1) it's the wrong name and 2) it can't represent a range, which all calendar values are.

At the heart of the problems with the date & time API is the idea that "points in time are for computers, but calendars are for humans", and trying to deal with crossing that boundary is extremely error-prone.

I completely agree. Having a notion of an instantaneous point in time is nice. But it's also a separate concept from "the range of instants that make up a year/day/nanosecond".

I'm opposed to it for all the reasons I've outlined. I don't want a global state that I can't easily mock or alter for my app's own purposes.

Changing Date() to Date.now() is simply code churn with a minor win in readability, but with no change in utility. If we're going to make changes to the Date & Time API, I we should make useful changes. Renaming a method isn't really "useful" (ie, it does nothing to change how a thing is used); it's just a name change.

6 Likes

Yeah, that's a great point. In Chronology, all calendar values (including Clocks), have a "region" value, which is a triple of the calendar, locale, and timezone. Clocks still produce instantaneous values (Instant, largely equivalent to Foundation.Date), but they can also produce region-specific values (See this file for that), which end up being the far more useful values. In my opinion, using Instant would be rare for clients of Chronology; you'd want the far more palatable calendar values instead.

If I were Supreme Swift Potentate, I would actually deprecate all of Foundation's API and replace it with Chronology instead. That's unlikely to happen, so I'm developing my library separately to do things "properly". But if there's a chance to make Foundation's API better, I'm all for it.

It is not hard to write code in a way that makes it easy to control the current time for testing, etc. It does require discipline though. This is true for all code that interacts with the real world.

I suspect that one reason you are calling out Date() is that Date is a type where most of the operations have value semantics, but the default initializer is impure. This is indeed somewhat awkward and I have seen a quite a few people use Date() in code that they intend to be pure. I think a strong argument can be made that Date() should removed. I also agree that this is a good example of the problems that can happen when IO operations (reading a clock) are exposed by a type that generally has value semantics.

5 Likes

Thanks for bringing up this topic, Dave. Some thoughts on this:

As with all new API we might introduce, we would need strong motivation to bring it forward — especially in an area where there is so much existing API and convention, it would need to really carry its own weight.

One of your main concerns here is that Date.now(), however spelled, is an implicit dependency; my question to you is: in what ways would a new Clock API solve this concern? Presumably, the idea is to pass a Clock around between function calls to mock out the underlying datetime calculations and make it easier to test:

// Old
func myDateThing() -> Result {
    // ...
    let date = Date.now()
    // calculate based on date
}

// New
func myDateThing(_ clock: Clock) -> Result {
    // ...
    let date = clock.now()
    // calculate based on date
}

Passing in the clock could allow you to set up a different clock in your tests, letting you inject the dependency.

However, this is the solution to all dependency injection issues: making your functions referentially transparent by passing in your dependencies lets you mock and test more easily. Is this in an appreciable way different from what you can already do today?

// Today
func myDateThing(_ date: Date) -> Result {
    // calculate based on date
}

Similarly, what, if anything, would prevent someone from replacing all Date.now() calls with Clock.system.now()? Does Clock.system.now() imply more global state than Date.now()?


Separately, like @pvieito, I fundamentally disagree with your statement here. Dates in colloquial usage can indeed represent ranges: when someone asks "what day was last Tuesday", they're likely referring to the range of time starting on Tuesday at 00:00:00.000 and ending at 23:59:59.99999..., and this is well represented by DateInterval. However, the date represented by "May 5th, 2018, at 9:06:02 AM in America/Los_Angeles" (2018-05-21T09:06:02.000-0700) is an absolute instant in time, with no associated duration. It's not useful to discuss that instant in terms of a range.

Where I do agree with you is the difficulty in separating those concepts. Given a DateComponents, it isn't possible for Calendar to guarantee statically whether the components you're asking for correspond to a single instant in time (2018-05-21T09:06:02.000-0700), or a supposed range ("the month of May, 2018"). It's also not possible for it to do so, unless you bifurcate the methods in your system to deal either in terms of absolute points in time or in intervals.

Calendar opts for consistency here. Given a request for a date, it returns a Date representing the first (or last, depending on how you search) instant in time matching the components you're interested in. If you want to turn that into an interval, it's always possible to do so with -[NSCalendar rangeOfUnit:startDate:interval:forDate:], which would return to you the interval. One great place to make progress here, I think, is exposing that method in a better way for Swift, and potentially adding convenience methods to make this process automatic — given a DateComponents, give me back a DateInterval representing the range of this Calendar.Unit inside those components, e.g. calendar.interval(for: .year, containing: DateComponents(year: 2018, month: 5))

There's always room for improvement there, and we'd be happy to accept suggestions that make correctness easier and more ergonomic.

5 Likes

I agree that Foundation's Date type has some problematic qualities, but that argument seems unrelated to the addition of a new Date.now API.

Date.now would just be a new, alternative spelling for Date().

I understand the desire to build a new world with better fundamentals (which you have already done with Chronology), but it seems unreasonable to say that we can't make improvements to the current world (NSDate) only because the current world is flawed.

Adding Date.now seems like an easy addition we can make today to improve existing patterns. That doesn't mean we can't also explore adding new date/time related types.

3 Likes

I agree that Clock is a worthwhile API to have, and that it probably makes sense to have it be the source of truth for now.

Date is simply an instant of time without respect to time zone or other calendar overlays, and a way to measure intervals of time.

When you take a clock and "initialize" it (after buying/building it), you have to set it to "now" in the users perception, defined by time zone. So, the clock cannot tell you what now is. You have to look at another clock. These days it's an atomic clock, and I just assume that's what computers' system time is synchronized against. So Clock.system.now seems appropriate to me. Clock.now doesn't make sense unless you assume it's the system clock, but I'd rather have that be explicit.

[Date] relies on a host of external things, including the system's connection to the network time servers

in what sense? I figured the device's synchronization with external sources of truth would be fully decoupled from every request for the system time. If the device hasn't been online in 90 days to sync, why should any app expect to then get that info from the network? And, if it hasn't been online in so long, I don't see why it makes any sense to make guarantees as to the functionality of the clock–the user may keep it manually synced, or just not even care.

After abdicating responsibility for now and epochs and such, would it make more sense for Date to be renamed to Time? After all, a date is a representation of an instant or range of time.

If I may, the difference is that a time source is usually meant to be read several times, while you can only supply a concrete Date once:

func benchmark(_ code: (Void) -> ()) -> TimeInterval {
    let startTime = Date()
    for _ in 1 ... 1_000_000 {
        code()
    }
    let endTime = Date()
    return endTime - startTime
}

To make the Date dependency explicit, I guess I would do this:

func benchmark(_ code: (Void) -> (), clock: (Void) -> Date = { Date() }) -> TimeInterval {
    let startTime = clock()
    for _ in 1 ... 1_000_000 {
        code()
    }
    let endTime = clock()
    return endTime - startTime
}

And Clock would mostly be a shorthand for that:

func benchmark(_ code: (Void) -> (), clock: Clock = Clock.system) -> TimeInterval {
    let startTime = clock.now()
    for _ in 1 ... 1_000_000 {
        code()
    }
    let endTime = clock.now()
    return endTime - startTime
}

But I understand that if we were to make significant changes, this alone would not be a compelling argument.

What makes this specifically not a compelling argument to me is that in this example, what you actually need really is a clock. You don't have to do this with Date — the above example could've been written just as easily by comparing the results of ProcessInfo.processInfo.systemUptime, a mach time source, or any other high-resolution time mechanism.

If you generalize this a bit (say instead of checking the time twice, you check it N times), what you really have there is a generator of values which you draw values from in order to compare to one another later. In the general case, you indeed need to pass in the generator itself, rather than individual values.

(For this specific application, too — benchmark isn't the type of function whose inputs you'd mock out. Would it be helpful to give a benchmark a time source other than the current system clock [in as high resolution as possible]? In general, I'm having a hard time coming up with an example where you'd normally draw from Date.now() multiple times to compare, but instead pass in a new time source.)

I have obviously failed to express myself clearly, sorry. Your previous question was: why is an explicit Clock type better for testing than just passing in a Date directly? My answer was that the Clock type makes it directly possible to read the current time multiple times – that’s all.

Right, benchmark wasn’t a good example. (I was only interested in the function interface and included the first possible implementation I could think of to make it obvious what the interface does.) To get a better example, at work we have an app that has an automatic sign-out feature that is triggered two minutes after sending the app to background. This is implemented by saving the current time when deactivating and then comparing it to the moment the app is activated again. Here it makes sense to have a Clock as a dependency, replacing Clock.system with a user-controllable static clock in tests?

2 Likes

I agree — in this case, what you do really need is a repeating source of time.

I think this is a better example, but to get at the source of my issue: what are you really testing here? Is it worth testing

func testTimedLogOut(_ clock: Clock) {
    app.registerDeactivation()
    clock.after(2, .minute) {
        assert(!app.isLoggedIn)
    }
}

or is it worth factoring out the deactivation and logged-in state to a point where you're not dependent on the time? The test above is testing the functionality of Clock too, which isn't the point. You should assume that the clock works, and instead, test the underlying behavior: after the app deactivates, the user is logged out, irrelevant of timing.

Maybe this is more of a philosophical disagreement, though.

There’s a grace period: if the user returns to the app in those two minutes, the app doesn’t sign out. (We need that because we call other apps and signing out immediately after deactivation would break the flow.) There are also other conditions that may intervene in the process, which makes it more compelling to test the thing as a whole. I think I understand your point and I agree that testing things in isolation is better, but IMHO there simply are cases where it makes sense to include a running clock in the tests.

Terms of Service

Privacy Policy

Cookie Policy