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.
So far in this series, we’ve looked at:
- The basics of dates and times in Swift, with creating dates, converting components into dates and vice versa, and converting dates into strings and vice versa, and
- date arithmetic in Swift, with adding time intervals to dates and calculating the time between two dates.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Playground - noun: a place where people can play import UIKit let userCalendar = NSCalendar.currentCalendar() // Let's create some dates to work with // ==================================== // If you need to instantiate a number of pre-defined dates, // an NSDateFormatter will get it done in the fewest number of lines. let dateMaker = NSDateFormatter() dateMaker.dateFormat = "yyyy/MM/dd hh:mm:ss Z" // I'm setting these dates according to my time zone (UTC-0500); // feel free to adjust the values according to your time zone. let valentinesDay = dateMaker.dateFromString("2015/02/14 00:00:00 -05:00")! let stPatricksDay = dateMaker.dateFromString("2015/03/17 00:00:00 -05:00")! |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// (Previous code goes here) // Comparing dates, part 1 // ======================= // First, the clunky NSDate::compare way // ------------------------------------- // Returns true because Valentine's Day comes before St. Patrick's Day valentinesDay.compare(stPatricksDay) == .OrderedAscending // Returns true because they're the same date valentinesDay.compare(valentinesDay) == .OrderedSame // Returns true because St. Patrick's Day comes after Valentine's Day stPatricksDay.compare(valentinesDay) == .OrderedDescending |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// (Previous code goes here) // Let's overload the <, >, and == operators to do date comparisons // ---------------------------------------------------------------- func ==(lhs: NSDate, rhs: NSDate) -> Bool { return lhs === rhs || lhs.compare(rhs) == .OrderedSame } func <(lhs: NSDate, rhs: NSDate) -> Bool { return lhs.compare(rhs) == .OrderedAscending } func >(lhs: NSDate, rhs: NSDate) -> Bool { return lhs.compare(rhs) == .OrderedDescending } // Comparisons are less clunky now! // -------------------------------- valentinesDay < stPatricksDay // true valentinesDay == valentinesDay // true stPatricksDay > valentinesDay // true |
With these functions, we’re simply overloading the ==
, <
, and >
operators so that they work on NSDate
s 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; ifa === b
, thena
andb
both reference the same object).
Make date comparisons more “human”
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 NSDate
s 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// (Previous code goes here) // Comparing dates, part 2 // ======================= // Let's create two dates that are only one second apart: let groundhogDay = dateMaker.dateFromString("2015/02/02 00:00:00 -05:00")! let groundhogDayPlus1Second = dateMaker.dateFromString("2015/02/02 00:00:01 -05:00")! // This line returns false: groundhogDay == groundhogDayPlus1Second // This line returns true: groundhogDay < groundhogDayPlus1Second // NSDate's compare method is too precise for a lot of uses! |
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:
|
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// (Previous code goes here) // Enter NSCalendar's compareDate method, available in iOS 8 and later // This returns false, because 2015/02/02 00:00:00 and 2015/02/02 00:00:01 // are NOT both within the same SECOND. userCalendar.compareDate(groundhogDay, toDate: groundhogDayPlus1Second, toUnitGranularity: .Second) == .OrderedSame // This returns true, because 2015/02/02 00:00:00 and 2015/02/02 00:00:01 // ARE both within the same MINUTE. userCalendar.compareDate(groundhogDay, toDate: groundhogDayPlus1Second, toUnitGranularity: .Minute) == .OrderedSame // This returns true, because 2015/02/02 00:00:00 and 2015/02/02 00:00:01 // ARE both within the same HOUR. userCalendar.compareDate(groundhogDay, toDate: groundhogDayPlus1Second, toUnitGranularity: .Hour) == .OrderedSame // This returns true, because 2015/02/02 00:00:00 and 2015/02/02 00:00:01 // ARE both within the same DAY. userCalendar.compareDate(groundhogDay, toDate: groundhogDayPlus1Second, toUnitGranularity: .Day) == .OrderedSame |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// (Previous code goes here) // 1 second before Groundhog Day, it was the previous day. // Note what happens when we use compareDate: let groundhogDayMinus1Second = dateMaker.dateFromString("2015/02/01 11:59:59 -05:00")! // This returns false, because 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // are NOT both within the same SECOND. userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Second) == .OrderedSame // This returns false, because while 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // ARE within a minute of each other, they're not both within the SAME MINUTE // (00:00 vs. 11:59). userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Minute) == .OrderedSame // This returns false, because while 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // ARE within an hour of each other, they're not both within the SAME HOUR // (0 vs. 11). userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Hour) == .OrderedSame // This returns false, because while 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // ARE within a day of each other, they're not both within the SAME DAY // (February 2 vs. February 1). userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Day) == .OrderedSame // This returns true, because while 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // ARE in the same month. userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Month) == .OrderedSame |
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”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// (Previous code goes here) // Date arithmetic // =============== // The clunky way // -------------- // Suppose we want to get the date of Groundhog Day plus // 1 month, 8 days, 6 hours, and 17 minutes let timeInterval = NSDateComponents() timeInterval.month = 1 timeInterval.day = 8 timeInterval.hour = 6 timeInterval.minute = 17 // The resulting date should be March 10, 2015, 6:17 a.m. let resultDate = userCalendar.dateByAddingComponents(timeInterval, toDate: groundhogDay, options: [])! |
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:
1 |
let resultDate = groundhogDay + 1.months + 8.days + 6.hours + 17.minutes |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
// (Previous code goes here) // The neat way // ------------ // First, we define methods that allow us to add and subtract // NSDateComponents instances // The addition and subtraction code is nearly the same, // so we've factored it out into this method func combineComponents(lhs: NSDateComponents, rhs: NSDateComponents, _ multiplier: Int = 1) -> NSDateComponents { let result = NSDateComponents() let undefined = Int(NSDateComponentUndefined) result.second = ((lhs.second != undefined ? lhs.second : 0) + (rhs.second != undefined ? rhs.second : 0) * multiplier) result.minute = ((lhs.minute != undefined ? lhs.minute : 0) + (rhs.minute != undefined ? rhs.minute : 0) * multiplier) result.hour = ((lhs.hour != undefined ? lhs.hour : 0) + (rhs.hour != undefined ? rhs.hour : 0) * multiplier) result.day = ((lhs.day != undefined ? lhs.day : 0) + (rhs.day != undefined ? rhs.day : 0) * multiplier) result.month = ((lhs.month != undefined ? lhs.month : 0) + (rhs.month != undefined ? rhs.month : 0) * multiplier) result.year = ((lhs.year != undefined ? lhs.year : 0) + (rhs.year != undefined ? rhs.year : 0) * multiplier) return result } // With combineComponents defined, // overloading + and - is simple func +(lhs: NSDateComponents, rhs: NSDateComponents) -> NSDateComponents { return combineComponents(lhs, rhs: rhs) } func -(lhs: NSDateComponents, rhs: NSDateComponents) -> NSDateComponents { return combineComponents(lhs, rhs: rhs, -1) } // We'll need to overload unary - so we can negate components prefix func -(components: NSDateComponents) -> NSDateComponents { let result = NSDateComponents() let undefined = Int(NSDateComponentUndefined) if(components.second != undefined) { result.second = -components.second } if(components.minute != undefined) { result.minute = -components.minute } if(components.hour != undefined) { result.hour = -components.hour } if(components.day != undefined) { result.day = -components.day } if(components.month != undefined) { result.month = -components.month } if(components.year != undefined) { result.year = -components.year } return result } |
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
, andyear
properties of twoNSDateComponent
instances, - subtract the
second
,minute
,hour
,day
,month
, andyear
properties of oneNSDateComponents
instance from the corresponding properties of anotherNSDateComponents
instance, and - negate the
second
,minute
,hour
,day
,month
, andyear
properties of anNSDateComponents
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:
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
// (Previous code goes here) // Next, we extend Int to bring some Ruby-like magic // to date components extension Int { var seconds: NSDateComponents { let components = NSDateComponents() components.second = self; return components } var second: NSDateComponents { return self.seconds } var minutes: NSDateComponents { let components = NSDateComponents() components.minute = self; return components } var minute: NSDateComponents { return self.minutes } var hours: NSDateComponents { let components = NSDateComponents() components.hour = self; return components } var hour: NSDateComponents { return self.hours } var days: NSDateComponents { let components = NSDateComponents() components.day = self; return components } var day: NSDateComponents { return self.days } var weeks: NSDateComponents { let components = NSDateComponents() components.day = 7 * self; return components } var week: NSDateComponents { return self.weeks } var months: NSDateComponents { let components = NSDateComponents() components.month = self; return components } var month: NSDateComponents { return self.months } var years: NSDateComponents { let components = NSDateComponents() components.year = self; return components } var year: NSDateComponents { return self.years } } |
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:
1 2 3 4 5 6 7 8 9 10 |
// (Previous code goes here) // Building an NSDateComponents instance that represents // a time interval is now a lot nicer: let newTimeInterval = 1.month + 8.days + 6.hours + 17.minutes // Let's confirm that it works newTimeInterval.month // 1 newTimeInterval.day // 8 newTimeInterval.hour // 6 newTimeInterval.minute // 17 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// (Previous code goes here) // Let's make it easy to add dates and components, // and subtract components from dates // Date + component func +(lhs: NSDate, rhs: NSDateComponents) -> NSDate { return NSCalendar.currentCalendar().dateByAddingComponents(rhs, toDate: lhs, options: [])! } // Component + date func +(lhs: NSDateComponents, rhs: NSDate) -> NSDate { return rhs + lhs } // Date - component // (Component - date doesn't make sense) func -(lhs: NSDate, rhs: NSDateComponents) -> NSDate { return lhs + (-rhs) } |
With these methods, date arithmetic now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// (Previous code goes here) // Look at how easy date arithmetic is now: // What's the date and time 2 weeks, 1 day, 13 hours, and 57 minutes // after Groundhog Day 2015? groundhogDay + 2.weeks + 1.day + 13.hours + 57.minutes // (Answer: February 17, 2015, 1:57 p.m.) // Adding dates to date components is quite flexible: 2.weeks + 1.day + 13.hours + 57.minutes + groundhogDay 2.weeks + 1.day + groundhogDay + 13.hours + 57.minutes // What was the date 1 year, 2 months, and 12 days // prior to Groundhog Day 2015? groundhogDay - 1.year - 2.months - 12.days /// (Answer: November 20, 2013) |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// (Previous code goes here) // And finally some Ruby on Rails magic, that allows us to create dates // with code like "2.days.fromNow" and "2.days.ago" extension NSDateComponents { var fromNow: NSDate { let currentCalendar = NSCalendar.currentCalendar() return currentCalendar.dateByAddingComponents(self, toDate: NSDate(), options: [])! } var ago: NSDate { let currentCalendar = NSCalendar.currentCalendar() return currentCalendar.dateByAddingComponents(-self, toDate: NSDate(), options: [])! } } |
Equipped with everything we’ve made, we can now write code like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// (Previous code goes here) // Let's test the Rails magic! // (It's August 26, 2015, 23:06 as I execute this code) // August 28, 2015, 11:06 p.m. 2.days.fromNow // August 29, 2015, 2:23 a.m. (2.days + 3.hours + 17.minutes).fromNow // August 24, 2015, 11:06 p.m. 2.days.ago // August 24, 2015, 7:49 p.m. (2.days + 3.hours + 17.minutes).ago |
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!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 |
// Date/time overloads and extensions Swift playground // by Joey deVilla - August 2015 // Partially based on Axel Schlueter's SwiftDateTimeExtensions library // (https://github.com/schluete/SwiftDateTimeExtensions) // // Released under the MIT License (MIT) // (c) 2015 Joey deVilla import UIKit let userCalendar = NSCalendar.currentCalendar() // Let's create some dates to work with // ==================================== // If you need to instantiate a number of pre-defined dates, // an NSDateFormatter will get it done in the fewest number of lines. let dateMaker = NSDateFormatter() dateMaker.dateFormat = "yyyy/MM/dd hh:mm:ss Z" // I'm setting these dates according to my time zone (UTC-0500); // feel free to adjust the values according to your time zone. let valentinesDay = dateMaker.dateFromString("2015/02/14 00:00:00 -05:00")! let stPatricksDay = dateMaker.dateFromString("2015/03/17 00:00:00 -05:00")! // Comparing dates, part 1 // ======================= // First, the clunky NSDate::compare way // ------------------------------------- // Returns true because Valentine's Day comes before St. Patrick's Day valentinesDay.compare(stPatricksDay) == .OrderedAscending // Returns true because they're the same date valentinesDay.compare(valentinesDay) == .OrderedSame // Returns true because St. Patrick's Day comes after Valentine's Day stPatricksDay.compare(valentinesDay) == .OrderedDescending // Let's overload the <, >, and == operators to do date comparisons // ---------------------------------------------------------------- func ==(lhs: NSDate, rhs: NSDate) -> Bool { return lhs === rhs || lhs.compare(rhs) == .OrderedSame } func <(lhs: NSDate, rhs: NSDate) -> Bool { return lhs.compare(rhs) == .OrderedAscending } func >(lhs: NSDate, rhs: NSDate) -> Bool { return lhs.compare(rhs) == .OrderedDescending } // Comparisons are less clunky now! // -------------------------------- valentinesDay < stPatricksDay // true valentinesDay == valentinesDay // true stPatricksDay > valentinesDay // true // Comparing dates, part 2 // ======================= // Let's create two dates that are only one second apart: let groundhogDay = dateMaker.dateFromString("2015/02/02 00:00:00 -05:00")! let groundhogDayPlus1Second = dateMaker.dateFromString("2015/02/02 00:00:01 -05:00")! // This line returns false: groundhogDay == groundhogDayPlus1Second // This line returns true: groundhogDay < groundhogDayPlus1Second // NSDate's compare method is too precise for a lot of uses! // Enter NSCalendar's compareDate method, available in iOS 8 and later // This returns false, because 2015/02/02 00:00:00 and 2015/02/02 00:00:01 // are NOT both within the same SECOND. userCalendar.compareDate(groundhogDay, toDate: groundhogDayPlus1Second, toUnitGranularity: .Second) == .OrderedSame // This returns true, because 2015/02/02 00:00:00 and 2015/02/02 00:00:01 // ARE both within the same MINUTE. userCalendar.compareDate(groundhogDay, toDate: groundhogDayPlus1Second, toUnitGranularity: .Minute) == .OrderedSame // This returns true, because 2015/02/02 00:00:00 and 2015/02/02 00:00:01 // ARE both within the same HOUR. userCalendar.compareDate(groundhogDay, toDate: groundhogDayPlus1Second, toUnitGranularity: .Hour) == .OrderedSame // This returns true, because 2015/02/02 00:00:00 and 2015/02/02 00:00:01 // ARE both within the same DAY. userCalendar.compareDate(groundhogDay, toDate: groundhogDayPlus1Second, toUnitGranularity: .Day) == .OrderedSame // 1 second before Groundhog Day, it was the previous day. // Note what happens when we use compareDate: let groundhogDayMinus1Second = dateMaker.dateFromString("2015/02/01 11:59:59 -05:00")! // This returns false, because 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // are NOT both within the same SECOND. userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Second) == .OrderedSame // This returns false, because while 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // ARE within a minute of each other, they're not both within the SAME MINUTE // (00:00 vs. 11:59). userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Minute) == .OrderedSame // This returns false, because while 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // ARE within an hour of each other, they're not both within the SAME HOUR // (0 vs. 11). userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Hour) == .OrderedSame // This returns false, because while 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // ARE within a day of each other, they're not both within the SAME DAY // (February 2 vs. February 1). userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Day) == .OrderedSame // This returns true, because while 2015/02/02 00:00:00 and 2015/02/01 11:59:59 // ARE in the same month. userCalendar.compareDate(groundhogDay, toDate: groundhogDayMinus1Second, toUnitGranularity: .Month) == .OrderedSame // Date arithmetic // =============== // The clunky way // -------------- // Suppose we want to get the date of Groundhog Day plus // 1 month, 8 days, 6 hours, and 17 minutes let timeInterval = NSDateComponents() timeInterval.month = 1 timeInterval.day = 8 timeInterval.hour = 6 timeInterval.minute = 17 // The resulting date should be March 10, 2015, 6:17 a.m. let resultDate = userCalendar.dateByAddingComponents(timeInterval, toDate: groundhogDay, options: [])! // The neat way // ------------ // First, we define methods that allow us to add and subtract // NSDateComponents instances // The addition and subtraction code is nearly the same, // so we've factored it out into this method func combineComponents(lhs: NSDateComponents, rhs: NSDateComponents, _ multiplier: Int = 1) -> NSDateComponents { let result = NSDateComponents() let undefined = Int(NSDateComponentUndefined) result.second = ((lhs.second != undefined ? lhs.second : 0) + (rhs.second != undefined ? rhs.second : 0) * multiplier) result.minute = ((lhs.minute != undefined ? lhs.minute : 0) + (rhs.minute != undefined ? rhs.minute : 0) * multiplier) result.hour = ((lhs.hour != undefined ? lhs.hour : 0) + (rhs.hour != undefined ? rhs.hour : 0) * multiplier) result.day = ((lhs.day != undefined ? lhs.day : 0) + (rhs.day != undefined ? rhs.day : 0) * multiplier) result.month = ((lhs.month != undefined ? lhs.month : 0) + (rhs.month != undefined ? rhs.month : 0) * multiplier) result.year = ((lhs.year != undefined ? lhs.year : 0) + (rhs.year != undefined ? rhs.year : 0) * multiplier) return result } // With combineComponents defined, // overloading + and - is simple func +(lhs: NSDateComponents, rhs: NSDateComponents) -> NSDateComponents { return combineComponents(lhs, rhs: rhs) } func -(lhs: NSDateComponents, rhs: NSDateComponents) -> NSDateComponents { return combineComponents(lhs, rhs: rhs, -1) } // We'll need to overload unary - so we can negate components prefix func -(components: NSDateComponents) -> NSDateComponents { let result = NSDateComponents() let undefined = Int(NSDateComponentUndefined) if(components.second != undefined) { result.second = -components.second } if(components.minute != undefined) { result.minute = -components.minute } if(components.hour != undefined) { result.hour = -components.hour } if(components.day != undefined) { result.day = -components.day } if(components.month != undefined) { result.month = -components.month } if(components.year != undefined) { result.year = -components.year } return result } // Next, we extend Int to bring some Ruby-like magic // to date components extension Int { var seconds: NSDateComponents { let components = NSDateComponents() components.second = self; return components } var second: NSDateComponents { return self.seconds } var minutes: NSDateComponents { let components = NSDateComponents() components.minute = self; return components } var minute: NSDateComponents { return self.minutes } var hours: NSDateComponents { let components = NSDateComponents() components.hour = self; return components } var hour: NSDateComponents { return self.hours } var days: NSDateComponents { let components = NSDateComponents() components.day = self; return components } var day: NSDateComponents { return self.days } var weeks: NSDateComponents { let components = NSDateComponents() components.day = 7 * self; return components } var week: NSDateComponents { return self.weeks } var months: NSDateComponents { let components = NSDateComponents() components.month = self; return components } var month: NSDateComponents { return self.months } var years: NSDateComponents { let components = NSDateComponents() components.year = self; return components } var year: NSDateComponents { return self.years } } // Building an NSDateComponents instance that represents // a time interval is now a lot nicer: let newTimeInterval = 1.month + 8.days + 6.hours + 17.minutes // Let's confirm that it works newTimeInterval.month // 1 newTimeInterval.day // 8 newTimeInterval.hour // 6 newTimeInterval.minute // 17 // Let's make it easy to add dates and components, // and subtract components from dates // Date + component func +(lhs: NSDate, rhs: NSDateComponents) -> NSDate { return NSCalendar.currentCalendar().dateByAddingComponents(rhs, toDate: lhs, options: [])! } // Component + date func +(lhs: NSDateComponents, rhs: NSDate) -> NSDate { return rhs + lhs } // Date - component // (Component - date doesn't make sense) func -(lhs: NSDate, rhs: NSDateComponents) -> NSDate { return lhs + (-rhs) } // Look at how easy date arithmetic is now: // What's the date and time 2 weeks, 1 day, 13 hours, and 57 minutes // after Groundhog Day 2015? groundhogDay + 2.weeks + 1.day + 13.hours + 57.minutes // (Answer: February 17, 2015, 1:57 p.m.) // Adding dates to date components is quite flexible: 2.weeks + 1.day + 13.hours + 57.minutes + groundhogDay 2.weeks + 1.day + groundhogDay + 13.hours + 57.minutes // What was the date 1 year, 2 months, and 12 days // prior to Groundhog Day 2015? groundhogDay - 1.year - 2.months - 12.days /// (Answer: November 20, 2013) // And finally some Ruby on Rails magic, that allows us to create dates // with code like "2.days.fromNow" and "2.days.ago" extension NSDateComponents { var fromNow: NSDate { let currentCalendar = NSCalendar.currentCalendar() return currentCalendar.dateByAddingComponents(self, toDate: NSDate(), options: [])! } var ago: NSDate { let currentCalendar = NSCalendar.currentCalendar() return currentCalendar.dateByAddingComponents(-self, toDate: NSDate(), options: [])! } } // Let's test the Rails magic! // (It's August 26, 2015, 23:06 as I execute this code) // August 28, 2015, 11:06 p.m. 2.days.fromNow // August 29, 2015, 2:23 a.m. (2.days + 3.hours + 17.minutes).fromNow // August 24, 2015, 11:06 p.m. 2.days.ago // August 24, 2015, 7:49 p.m. (2.days + 3.hours + 17.minutes).ago |
Related articles
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.