How to work with dates and times in Swift, part three: Making date arithmetic more Swift-like [Updated for Swift 2]

by Joey deVilla on February 2, 2015

date and time

Update, August 26, 2015: I’ve updated this article so that its code works with Swift 2. It compiles under the latest version of Xcode 7, beta 6.

swift kickSo far in this series, we’ve looked at:

So far, everything we’ve done has a distinctly un-Swift-like feel to it. That’s because Cocoa’s date and time classes were built with its original programming language, Objective-C, in mind. In this article, we’ll look at ways to make date calculations feel more “human” and Swift-like.

Make date comparisons more Swift-like

Let’s start with a new playground and some quick definitions:

  • A reference to the user’s calendar,
  • an NSDateFormatter and format string that makes it easy to define dates in a hurry,
  • and two dates:
    • Valentine’s Day (February 14, 2015 at midnight)
    • St. Patrick’s Day (March 17, 2015 at midnight)

Here’s what your code should look like:

In the previous article, we looked at NSDate's compare method, which compares two NSDates and returns a result of type NSComparisonResult as shown in the table below:

If… compare returns…
the first date is earlier than the second date .OrderedAscending
the first date is equal to the second date .OrderedSame
the first date is later than the second date .OrderedDescending

Add the following code to your playground:

The compare method works well, but its syntax has that C-style clunkiness. It’s a bit jarring in Swift, which has a lot of features that so-called “scripting” languages have. Wouldn’t it be nice if we could compare dates using the ==, <, and > operators?

Let’s make it happen. Add the following code to your playground:

With these functions, we’re simply overloading the ==, <, and > operators so that they work on NSDates and hide the clunky compare syntax behind some syntactic sugar. In case you’re wondering about the parameter names, lhs is short for “left-hand side” and rhs is short for “right-hand side”.

Note than in our overload of the == operator, there are a couple of ways that two dates can be considered equal:

  • Their compare result is NSComparisonResult.OrderedSame, or
  • the two dates being compared are the same NSDate object (=== is the identity operator; if a === b, then a and b both reference the same object).

Make date comparisons more “human”

picard data riker

One recurring theme in science fiction and especially in Star Trek is the tendency for ultra-smart characters and computers to be overly, needlessly, pointlessly precise. The writers for the original series often did this with Spock, and it seemed that at least a few writers were aware of this annoying trope in later series. Here’s a bit of dialogue from The Next Generation:

Data: 6 days, 13 hours, 47 minutes.
Riker: What, no seconds?
Data: I have discovered, sir, a certain level of impatience when I calculate a lengthy time interval to the nearest second. [beat] However if you wish…
Riker: No. No. Minutes is fine.

NSDate‘s compare method, and, by extension, the ==, <, and > overloads we defined, have the same problem with being overly precise. Let’s consider a case where we have two NSDates that are only a second apart:

  • Groundhog Day 2015 (February 2, 2015) at 12:00 a.m. EST
  • One second after Groundhog Day 2015 at 12:00 a.m. EST

For most purposes, we’d consider midnight on Groundhog Day and one second after midnight Groundhog Day the to be the same time. We need a way to do date comparisons at granularities other than seconds.

If you’re targeting iOS 8 or later, such a way already exists: NSCalendar‘s compareDate method! It expects the following parameters:

Parameter Description
fromDate The first date in the comparison.
toDate The other date in the comparison.
toUnitGranularity The level of precision for the comparison, expressed as an NSCalendarUnit value, which includes:

  • .CalendarUnitSecond
  • .CalendarUnitMinute
  • .CalendarUnitHour
  • .CalendarUnitDay
  • .CalendarUnitMonth
  • .CalendarUnitYear

This is a Cocoa method with the word “compare” in its name, and you’ve probably guessed that its return type is NSComparisonResult. Here’s what it returns:

If… compareDate returns…
fromDate is earlier than toDate, when compared at the specified level of precision .OrderedAscending
fromDate is equal to toDate, when compared at the specified level of precision .OrderedSame
fromDate is later than toDate, when compared at the specified level of precision .OrderedDescending

Let’s try compareDate out:

Note that compareDate checks to see if the two given dates are in the same given time period. It doesn’t check to see if the two given dates are separated at most by the given time period. If you’re finding that distinction hard to follow, don’t worry; it’s hard to explain.

It’s easy to demonstrate, however. Suppose we create a new NSDate that represents one second before Groundhog Day and run some compareDate tests on it:

Note that compareDate isn’t available in iOS versions prior to 8. Code targeting iOS 7 or earlier will require writing an equivalent method, which I’ll leave as an exercise for the reader.

Making date arithmetic more Swift-like

You may have noticed in the code so far that I’ve been creating NSDates by using an instance of NSDateFormatter and a defined format string. That’s because this approach uses fewer lines than creating an NSDateComponents instance, setting its properties, then using a calendar to use the NSDateComponents instance to create an NSDate. Unfortunately, there’s no built-in quick way to build an NSDateComponents instance that represents an interval of time.

This means that answering questions like “What will the date be 1 month, 8 days, 6 hours, and 17 minutes after Groundhog Day?” requires a lot of yak shaving, my favorite term for “tedious setting-up”:

This approach is a clunky Objective-C-flavored way of doing things. I’d much rather do this calculation with code that looked like this:

Luckily, we’re working with Swift. Some judicious use of operator overloading and extensions will let us do just that!

First, we need to overload some operators to simplify date component arithmetic:

I derived these functions from Axel Schlueter’s SwiftDateTimeExtensions library. He wrote them when Swift was still in beta; I updated them so that they compile with the current version and added a couple of tweaks of my own. They make it possible to:

  • Add the respective second, minute, hour, day, month, and year properties of two NSDateComponent instances,
  • subtract the second, minute, hour, day, month, and year properties of one NSDateComponents instance from the corresponding properties of another NSDateComponents instance, and
  • negate the second, minute, hour, day, month, and year properties of an NSDateComponents instance.

The addition and subtraction operations are so similar and so tedious; that’s a sign that there’s an opportunity to DRY up the code. That’s why we have the combineComponents method doing the work and the + and - overloads calling it with the right parameters. The combineComponents code is dense with ternary conditional operators, so I thought I’d explain what’s going on under the hood with this flowchart:

combineComponents

You may be wondering why we defined a negation method and didn’t use it when performing subtraction. That’s because the negation method simply ignores undefined components, while addition and subtraction require treating undefined component values as 0. The negation method comes in handy in other scenarios, which I’ll show later.

Now that we’ve got date component addition, subtraction, and negation defined, let’s extend the Int type so that it has some instance properties that let us define components with statements like 5.seconds, 3.minutes, 7.hours, 2.days, 4.weeks, and so on:

Once again, I derived these functions from Axel Schlueter’s SwiftDateTimeExtensions library and added a couple of tweaks of my own.

There’s a little redundancy in the code above; it’s to allow for grammatically correct code. I didn’t like seeing code like 1.seconds, 1.minutes, 1.hours, 1.days, and so on.

With the date component addition and subtraction overloads and the extension to Int, building date components that represent time intervals is far less tedious:

With all our tweaks, adding components to dates using NSCalendar‘s dateByAddingComponents feels clunky by comparison. Here are some operator overloads that make this sort of coding more elegant if you’re working with dates and date components expressed in terms of the user’s current calendar:

With these methods, date arithmetic now looks like this:

That code is so much more pleasant to read (and write!).

And finally, a Ruby on Rails trick comes to Swift

Ruby on Rails lets you do very readable calculations like 2.days.from_now and 2.days.ago. We can bring that Rails magic to Swift by using everything we’ve build so far and extending NSDateComponents with two computer properties:

Equipped with everything we’ve made, we can now write code like this:

The entire playground

And with that. we’ve got a more Swift-like way of doing date arithmetic. I’m going to take all these methods and extensions and post them as a library on GitHub, but in the meantime, here’s the complete playground for the exercises in this article. Go forth and write some readable date/time code!

Related articles

dates and times in swift - small

A very brief introduction to date formatting in Swift and iOS: The oversight in a mostly-good book on Swift programming led me down the path of writing articles about dates and times in Swift, starting with this one, where I look atNSDateFormatter.

How to work with dates and times in Swift, part one: An introduction of Cocoa’s date and time classes, and how they work together. This article covers UTC (Coordinated Universal Time), and the key classes: NSDate, NSCalendar, NSDateComponents.

How to work with dates and times in Swift, part two: Calculations with dates: Now that we’ve got the basics, it’s time to do some date arithmetic: comparing two dates to see which one is the earlier and later one, finding out how far apart two dates are, and adding and subtracting from dates.

How to work with dates and times in Swift, part four: A more Swift-like way to get the time interval between two dates: This quick article shows you how to make an operator overload that makes getting the time interval between two dates more like subtraction.

{ 2 comments… read them below or add one }

1 Mikaila February 24, 2015 at 8:23 pm

Great Post. Very detailed but I am a newbie and i’m working on an app in swift that requires that i get the difference between three dates using datepicker and labels and divide it by an integer. I was wondering if you could point me in the right direction.

Here are the details:
I have 3 UIDatePickers when user scrolls picker and selects a date. The date populates in a textLabel. Then i need to do some calculations with the dates picked by the user which populates in the textLabels. I am solving for Q5

The logic is if (f5 <q2 ) then (c5-q2-r2) / 7 else (c5-f5-r2) / 7

let dateMaker = NSDateFormatter()
dateMaker.dateFormat = "MM/dd/yyyy"

var f5 = dateMaker.dateFromString("\(futureDateLabel)")
var q2 = dateMaker.dateFromString("\(StartSchoolDateLabel)")
var c5 = dateMaker.dateFromString("\(StrClassDateLabel)")
var divisor = 7
var days2Complete = daysToComplete.text.toInt()
var Q5 = 0

let userCalendar = NSCalendar.currentCalendar()

if (f5?.earlierDate(q2!) != nil)
{

let dayCalendarUnit:NSCalendarUnit = .DayCalendarUnit
var Difference = userCalendar.components(dayCalendarUnit, fromDate: c5!, toDate: q2!, options: nil)

Q5 = (Difference.day – days2Complete!) / divisor
}
else{

let dayCalendarUnit:NSCalendarUnit = .DayCalendarUnit
var Difference = userCalendar.components(dayCalendarUnit, fromDate: c5!, toDate: f5!, options: nil)

Q5 = (Difference.day – days2Complete!) / divisor

}

when i run it the app crashes on the else statement but when i comment out the else statement the app runs but Q5 populates as 0 in the textbox. I have been trying to solve this for days and i have read all three post of yours in regards to dates and arithmetic and I'm still stuck. Please help a newbie out at your convenience. Hope to hear from you soon. Thanks

2 Robert Nix June 17, 2016 at 12:22 pm

Wonderful series of articles! Exactly sync’ed with what I was just beginning to research. Thank you very much for your time and effort.

Leave a Comment

{ 3 trackbacks }

Previous post:

Next post: