
In this article, we’ll expand on material covered in the three previous articles in this series:
- The basic structs, which let us create dates and times, and work with their components. They are:
Date, which represents a single point in time,
DateComponents, which represents the units that make up a date, such as year, month, day, hour, and minute, and which can be used to represent either a single point in time or a duration of time, and
Calendar, which provides a context for Dates, and allows us to convert between Dates and DateComponents.
- The
DateFormatter class, which converts Dates into formatted Strings, and formatted Strings into Dates.
- Date arithmetic, which make it possible to add time intervals to
Dates, find the difference in time between two Dates, and compare Dates.
A more readable way to work with Dates and DateComponents
Suppose we want to find out what the date and time will be 2 months, 3 days, 4 hours, 5 minutes, and 6 seconds from now will be. If you recall what we covered in the last installment in this series, you’d probably use code like this:
var timeInterval = DateComponents()
timeInterval.month = 2
timeInterval.day = 3
timeInterval.hour = 4
timeInterval.minute = 5
timeInterval.second = 6
let futureDate = Calendar.current.date(byAdding: timeInterval, to: Date())!
In the code above, we did the following:
- We created an instance of a
DateComponents struct.
- We set its properties so that it would represent a time interval of 2 months, 3 days, 4 hours, 5 minutes, and 6 seconds.
- We then used
Calendar‘s date(byAdding:to:) method to add the time interval to a Date.
This code wouldn’t look out of place in a lot of other programming languages, but we can do better in Swift. What if I told you that by defining a few helper functions, you can turn the code above into the code below?
let coolerFutureDate = Date() + 2.months + 3.days + 4.hours + 5.minutes + 6.seconds
let coolerPastDate = Date() - 2.months - 3.days - 4.hours - 5.minutes - 6.seconds
Or this code?
let coolerFutureDate = (2.months + 3.days + 4.hours + 5.minutes + 6.seconds).fromNow
let coolerPastDate = (2.months + 3.days + 4.hours + 5.minutes + 6.seconds).ago
I’d much rather write the code above. This article will cover the code necessary to make this kind of syntactic magic possible.
Overloading + and - so that we can add and subtract DateComponents
First, let’s write some code that allows us to add and subtract DateComponents. Start a new playground and enter the following code into it:
func +(_ lhs: DateComponents, _ rhs: DateComponents) -> DateComponents {
return combineComponents(lhs, rhs)
}
func -(_ lhs: DateComponents, _ rhs: DateComponents) -> DateComponents {
return combineComponents(lhs, rhs, multiplier: -1)
}
func combineComponents(_ lhs: DateComponents,
_ rhs: DateComponents,
multiplier: Int = 1)
-> DateComponents {
var result = DateComponents()
result.second = (lhs.second ?? 0) + (rhs.second ?? 0) * multiplier
result.minute = (lhs.minute ?? 0) + (rhs.minute ?? 0) * multiplier
result.hour = (lhs.hour ?? 0) + (rhs.hour ?? 0) * multiplier
result.day = (lhs.day ?? 0) + (rhs.day ?? 0) * multiplier
result.weekOfYear = (lhs.weekOfYear ?? 0) + (rhs.weekOfYear ?? 0) * multiplier
result.month = (lhs.month ?? 0) + (rhs.month ?? 0) * multiplier
result.year = (lhs.year ?? 0) + (rhs.year ?? 0) * multiplier
return result
}
In the code above, we’ve overloaded the + and – operators so that we can add and subtract DateComponents. 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.
The addition and subtraction operations are so similar and so tedious, which is a sign that there’s an opportunity to DRY up the code. I factored out the duplicate code from both the + and - overloads and put it into its own method, combineComponents, which does the actual DateComponents addition and subtraction.
You may have noticed a lot of ?? operators in the code for combineComponents. ?? is referred to as the nil coalescing operator, and it’s a clever bit of syntactic shorthand. For the expression below:
let finalValue = someOptionalValue ?? fallbackValue
- If
someOptionalValue is not nil, finalValue is set to someOptionalValue‘s value.
- If
someOptionalValue is nil, finalValue is set to fallbackValue‘s value.
Let’s confirm that our code works. Try out the following code:
// (Previous code goes here)
// Let's define a couple of durations of time
var oneDayFiveHoursTenMinutes = DateComponents()
oneDayFiveHoursTenMinutes.day = 1
oneDayFiveHoursTenMinutes.hour = 5
oneDayFiveHoursTenMinutes.minute = 10
var threeDaysTenHoursThirtyMinutes = DateComponents()
threeDaysTenHoursThirtyMinutes.day = 3
threeDaysTenHoursThirtyMinutes.hour = 10
threeDaysTenHoursThirtyMinutes.minute = 30
// Now let's add and subtract them
let additionResult = OneDayFiveHoursTenMinutes + ThreeDaysTenHoursThirtyMinutes
additionResult.day // 4
additionResult.hour // 15
additionResult.minute // 40
let subtractionResult = ThreeDaysTenHoursThirtyMinutes - OneDayFiveHoursTenMinutes
subtractionResult.day // 2
subtractionResult.hour // 5
subtractionResult.minute // 20
Overloading - so that we can negate DateComponents
Now that we can add and subtract DateComponents, let’s overload the unary minus so that we can negate DateComponents:
// (Previous code goes here)
prefix func -(components: DateComponents) -> DateComponents {
var result = DateComponents()
if components.second != nil { result.second = -components.second! }
if components.minute != nil { result.minute = -components.minute! }
if components.hour != nil { result.hour = -components.hour! }
if components.day != nil { result.day = -components.day! }
if components.weekOfYear != nil { result.weekOfYear = -components.weekOfYear! }
if components.month != nil { result.month = -components.month! }
if components.year != nil { result.year = -components.year! }
return result
}
With this overload defined, we can now use the unary minus to negate DateComponents:
// (Previous code goes here)
let negativeTime = -oneDayFiveHoursTenMinutes
negativeTime.day // -1
negativeTime.hour // -5
negativeTime.minute // -10
Overloading + and - so that we can add Dates and DateComponents and subtract DateComponents from Dates
With the unary minus defined, we can now define the following operations:
Date + DateComponents
DateComponents + Date
Date – DateComponents
// (Previous code goes here)
// Date + DateComponents
func +(_ lhs: Date, _ rhs: DateComponents) -> Date
{
return Calendar.current.date(byAdding: rhs, to: lhs)!
}
// DateComponents + Dates
func +(_ lhs: DateComponents, _ rhs: Date) -> Date
{
return rhs + lhs
}
// Date - DateComponents
func -(_ lhs: Date, _ rhs: DateComponents) -> Date
{
return lhs + (-rhs)
}
Note that we didn’t define an overload for subtracting Date from DateComponents — such an operation doesn’t make any sense.
With these overloads defined, a lot of Date/DateComponents arithmetic in Swift becomes much easier to enter and read:
// (Previous code goes here)
// What time will it be 1 day, 5 hours, and 10 minutes from now?
// Here's the standard way of finding out:
Calendar.current.date(byAdding: oneDayFiveHoursTenMinutes, to: Date())
// With our overloads and function definitions, we can now do it this way:
Date() + oneDayFiveHoursTenMinutes
// This will work as well:
oneDayFiveHoursTenMinutes + Date()
// What time was it 3 days, 10 hours, and 30 minutes ago?
// Doing it the standard way takes some work
var minus3Days5Hours30minutes = threeDaysTenHoursThirtyMinutes
minus3Days5Hours30minutes.day = -threeDaysTenHoursThirtyMinutes.day!
minus3Days5Hours30minutes.hour = -threeDaysTenHoursThirtyMinutes.hour!
minus3Days5Hours30minutes.minute = -threeDaysTenHoursThirtyMinutes.minute!
Calendar.current.date(byAdding: minus3Days5Hours30minutes, to: Date())
// With our overloads and function definitions, it's so much easier:
Date() - threeDaysTenHoursThirtyMinutes
Extending Date so that creating dates is simpler
Creating Dates in Swift is a roundabout process. Usually, you end up creating them in one of two ways:
- Instantiating a
DateComponents struct and then using it to create a Date using Calendar‘s date(from:) method, or
- Creating a
String representation of the Date and then using it to create a Date using DateFormatter‘s date(from:) method.
Let’s simplify things by extending the Date struct with a couple of convenient init method overloads:
extension Date {
init(year: Int,
month: Int,
day: Int,
hour: Int = 0,
minute: Int = 0,
second: Int = 0,
timeZone: TimeZone = TimeZone(abbreviation: "UTC")!) {
var components = DateComponents()
components.year = year
components.month = month
components.day = day
components.hour = hour
components.minute = minute
components.second = second
components.timeZone = timeZone
self = Calendar.current.date(from: components)!
}
init(dateString: String) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss zz"
self = formatter.date(from: dateString)!
}
}
With these methods, initializing Dates is a lot more simple:
// (Previous code goes here)
// The Stevenote where the original iPhone was announced took place
// on January 9, 2007 at 10:00 a.m. PST
let iPhoneStevenoteDate = Date(year: 2007,
month: 1,
day: 9,
hour: 10,
minute: 0,
second: 0,
timeZone: TimeZone(abbreviation: "PST")!)
// The original iPhone went on sale on June 27, 2007
let iPhoneReleaseDate = Date(year: 2007, month: 6, day: 27) // June 27, 2007, 00:00:00 UTC
// The Stevenote where the original iPhone was announced took place
// on January 27, 2010 at 10:00 a.m. PST
let iPadStevenoteDate = Date(dateString: "2010-01-27 10:00:00 PST")
Overloading - so that we can use it to find the difference between two Dates
When we’re trying to determine the time between two given Dates, what we’re doing is finding the difference between them. Wouldn’t it be nice if we could use the - operator to find the difference between Dates, just as we can use it to find the difference between numbers?
Let’s code an overload to do just that:
// (Previous code goes here)
func -(_ lhs: Date, _ rhs: Date) -> DateComponents
{
return Calendar.current.dateComponents([.year, .month, .day, .hour, .minute],
from: lhs,
to: rhs)
}
Let’s test it in action:
let timeFromAnnouncementToRelease = iPhoneReleaseDate - iPhoneStevenoteDate
timeFromAnnouncementToRelease.year // 0
timeFromAnnouncementToRelease.month // 5
timeFromAnnouncementToRelease.day // 17
timeFromAnnouncementToRelease.hour // 7
timeFromAnnouncementToRelease.minute // 0
// How long ago was the first moon landing, which took place
// on July 20, 1969, 20:18 UTC?
Date() - Date(dateString: "1969-07-20 20:18:00 UTC")
// At the time of writing, this value was a Date with the following properties:
// - year: 47
// - month: 1
// - day: 9
// - hour: 22
// - minute: 14
Extending Int to add some syntactic magic to date components
We’ve already got some syntactic niceties, but the real Swift magic happens when we add this code to the mix:
// (Previous code goes here)
extension Int {
var second: DateComponents {
var components = DateComponents()
components.second = self;
return components
}
var seconds: DateComponents {
return self.second
}
var minute: DateComponents {
var components = DateComponents()
components.minute = self;
return components
}
var minutes: DateComponents {
return self.minute
}
var hour: DateComponents {
var components = DateComponents()
components.hour = self;
return components
}
var hours: DateComponents {
return self.hour
}
var day: DateComponents {
var components = DateComponents()
components.day = self;
return components
}
var days: DateComponents {
return self.day
}
var week: DateComponents {
var components = DateComponents()
components.weekOfYear = self;
return components
}
var weeks: DateComponents {
return self.week
}
var month: DateComponents {
var components = DateComponents()
components.month = self;
return components
}
var months: DateComponents {
return self.month
}
var year: DateComponents {
var components = DateComponents()
components.year = self;
return components
}
var years: DateComponents {
return self.year
}
}
This additions to Int allow us to convert Ints to DateComponents in an easy-to-read way, and with our overloads to add and subtract DateComponents to and from each other, and to add Dates to DateComponents, we can now perform all sorts of syntactic magic like this:
// From our earlier test of Date subtraction, we know that
// there were 5 months, 17 days, and 7 hours between
// the Stevenote when the iPhone was announced and
// midnight UTC on the day it was released.
iPhoneStevenoteDate + 5.months + 17.days + 7.hours // June 27, 2007, 00:00:00 UTC
// What was the date 10 years, 9 months, 8 days, 7 hours, and 6 minutes ago?
Date() - 10.years - 9.months - 8.days - 7.hours - 6.minutes
// At the time of writing, this value was Nov 22, 2005, 6:51 a.m. EST.
Extending DateComponents to add even more syntactic magic: fromNow and ago
And finally, a couple of additions to the DateComponents struct to make Date/DateComponent calculations even more concise and readable:
extension DateComponents {
var fromNow: Date {
return Calendar.current.date(byAdding: self,
to: Date())!
}
var ago: Date {
return Calendar.current.date(byAdding: -self,
to: Date())!
}
}
Here are these additions in action:
2.weeks.fromNow
// At the time of writing, this value was
// Sep 13, 2016, 2:55 PM
3.months.fromNow
// At the time of writing, this value was
// Nov 30, 2016, 2:55 PM
(2.months + 3.days + 4.hours + 5.minutes + 6.seconds).fromNow
// At the time of writing, this value was
// Nov 2, 2016, 7:04 PM
(2.months + 3.days + 4.hours + 5.minutes + 6.seconds).ago
// At the time of writing, this value was
// Nov 2, 2016, 7:04 PM
Wrapping it all up
Here’s the playground containing all the code we just worked with:
import UIKit
// Overloading + and - so that we can add and subtract DateComponents
// ------------------------------------------------------------------
func +(_ lhs: DateComponents, _ rhs: DateComponents) -> DateComponents {
return combineComponents(lhs, rhs)
}
func -(_ lhs: DateComponents, _ rhs: DateComponents) -> DateComponents {
return combineComponents(lhs, rhs, multiplier: -1)
}
func combineComponents(_ lhs: DateComponents,
_ rhs: DateComponents,
multiplier: Int = 1)
-> DateComponents {
var result = DateComponents()
result.second = (lhs.second ?? 0) + (rhs.second ?? 0) * multiplier
result.minute = (lhs.minute ?? 0) + (rhs.minute ?? 0) * multiplier
result.hour = (lhs.hour ?? 0) + (rhs.hour ?? 0) * multiplier
result.day = (lhs.day ?? 0) + (rhs.day ?? 0) * multiplier
result.weekOfYear = (lhs.weekOfYear ?? 0) + (rhs.weekOfYear ?? 0) * multiplier
result.month = (lhs.month ?? 0) + (rhs.month ?? 0) * multiplier
result.year = (lhs.year ?? 0) + (rhs.year ?? 0) * multiplier
return result
}
// Let's define a couple of durations of time
var oneDayFiveHoursTenMinutes = DateComponents()
oneDayFiveHoursTenMinutes.day = 1
oneDayFiveHoursTenMinutes.hour = 5
oneDayFiveHoursTenMinutes.minute = 10
var threeDaysTenHoursThirtyMinutes = DateComponents()
threeDaysTenHoursThirtyMinutes.day = 3
threeDaysTenHoursThirtyMinutes.hour = 10
threeDaysTenHoursThirtyMinutes.minute = 30
// Now let's add and subtract them
let additionResult = oneDayFiveHoursTenMinutes + threeDaysTenHoursThirtyMinutes
additionResult.day // 4
additionResult.hour // 15
additionResult.minute // 40
let subtractionResult = threeDaysTenHoursThirtyMinutes - oneDayFiveHoursTenMinutes
subtractionResult.day // 2
subtractionResult.hour // 5
subtractionResult.minute // 20
// Overloading - so that we can negate DateComponents
// --------------------------------------------------
// We'll need to overload unary - so we can negate components
prefix func -(components: DateComponents) -> DateComponents {
var result = DateComponents()
if components.second != nil { result.second = -components.second! }
if components.minute != nil { result.minute = -components.minute! }
if components.hour != nil { result.hour = -components.hour! }
if components.day != nil { result.day = -components.day! }
if components.weekOfYear != nil { result.weekOfYear = -components.weekOfYear! }
if components.month != nil { result.month = -components.month! }
if components.year != nil { result.year = -components.year! }
return result
}
let negativeTime = -oneDayFiveHoursTenMinutes
negativeTime.day // -1
negativeTime.hour // -5
negativeTime.minute // -10
// Overloading + and - so that we can add Dates and DateComponents
// and subtract DateComponents from Dates
// Date + DateComponents
func +(_ lhs: Date, _ rhs: DateComponents) -> Date
{
return Calendar.current.date(byAdding: rhs, to: lhs)!
}
// DateComponents + Dates
func +(_ lhs: DateComponents, _ rhs: Date) -> Date
{
return rhs + lhs
}
// Date - DateComponents
func -(_ lhs: Date, _ rhs: DateComponents) -> Date
{
return lhs + (-rhs)
}
// What time will it be 1 day, 5 hours, and 10 minutes from now?
// Here's the standard way of finding out:
Calendar.current.date(byAdding: oneDayFiveHoursTenMinutes, to: Date())
// With our overloads and function definitions, we can now do it this way:
Date() + oneDayFiveHoursTenMinutes
// This will work as well:
oneDayFiveHoursTenMinutes + Date()
// What time was it 3 days, 10 hours, and 30 minutes ago?
// Doing it the standard way takes some work
var minus3Days5Hours30minutes = threeDaysTenHoursThirtyMinutes
minus3Days5Hours30minutes.day = -threeDaysTenHoursThirtyMinutes.day!
minus3Days5Hours30minutes.hour = -threeDaysTenHoursThirtyMinutes.hour!
minus3Days5Hours30minutes.minute = -threeDaysTenHoursThirtyMinutes.minute!
Calendar.current.date(byAdding: minus3Days5Hours30minutes, to: Date())
// With our overloads and function definitions, it's so much easier:
Date() - threeDaysTenHoursThirtyMinutes
// Extending Date so that creating dates is simpler
// ------------------------------------------------
extension Date {
init(year: Int,
month: Int,
day: Int,
hour: Int = 0,
minute: Int = 0,
second: Int = 0,
timeZone: TimeZone = TimeZone(abbreviation: "UTC")!) {
var components = DateComponents()
components.year = year
components.month = month
components.day = day
components.hour = hour
components.minute = minute
components.second = second
components.timeZone = timeZone
self = Calendar.current.date(from: components)!
}
init(dateString: String) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss zz"
self = formatter.date(from: dateString)!
}
}
// The Stevenote where the original iPhone was announced took place
// on January 9, 2007 at 10:00 a.m. PST
let iPhoneStevenoteDate = Date(year: 2007,
month: 1,
day: 9,
hour: 10,
minute: 0,
second: 0,
timeZone: TimeZone(abbreviation: "PST")!)
// The original iPhone went on sale on June 27, 2007
let iPhoneReleaseDate = Date(year: 2007, month: 6, day: 27) // June 27, 2007, 00:00:00 UTC
// The Stevenote where the original iPhone was announced took place
// on January 27, 2010 at 10:00 a.m. PST
let iPadStevenoteDate = Date(dateString: "2010-01-27 10:00:00 PST")
// Overloading - so that we can use it to find the difference between two Dates
func -(_ lhs: Date, _ rhs: Date) -> DateComponents
{
return Calendar.current.dateComponents([.year, .month, .day, .hour, .minute],
from: rhs,
to: lhs)
}
let timeFromAnnouncementToRelease = iPhoneReleaseDate - iPhoneStevenoteDate
timeFromAnnouncementToRelease.year // 0
timeFromAnnouncementToRelease.month // 5
timeFromAnnouncementToRelease.day // 17
timeFromAnnouncementToRelease.hour // 7
timeFromAnnouncementToRelease.minute // 0
// How long ago was the first moon landing, which took place
// on July 20, 1969, 20:18 UTC?
Date() - Date(dateString: "1969-07-20 20:18:00 UTC")
// At the time of writing, this value was a Date with the following properties:
// - year: 47
// - month: 1
// - day: 9
// - hour: 22
// - minute: 14
// Extending Int to add some syntactic magic to date components
// ------------------------------------------------------------
extension Int {
var second: DateComponents {
var components = DateComponents()
components.second = self;
return components
}
var seconds: DateComponents {
return self.second
}
var minute: DateComponents {
var components = DateComponents()
components.minute = self;
return components
}
var minutes: DateComponents {
return self.minute
}
var hour: DateComponents {
var components = DateComponents()
components.hour = self;
return components
}
var hours: DateComponents {
return self.hour
}
var day: DateComponents {
var components = DateComponents()
components.day = self;
return components
}
var days: DateComponents {
return self.day
}
var week: DateComponents {
var components = DateComponents()
components.weekOfYear = self;
return components
}
var weeks: DateComponents {
return self.week
}
var month: DateComponents {
var components = DateComponents()
components.month = self;
return components
}
var months: DateComponents {
return self.month
}
var year: DateComponents {
var components = DateComponents()
components.year = self;
return components
}
var years: DateComponents {
return self.year
}
}
// From our earlier test of Date subtraction, we know that
// there were 5 months, 17 days, and 7 hours between
// the Stevenote when the iPhone was announced and
// midnight UTC on the day it was released.
iPhoneStevenoteDate + 5.months + 17.days + 7.hours // June 27, 2007, 00:00:00 UTC
// What was the date 10 years, 9 months, 8 days, 7 hours, and 6 minutes ago?
Date() - 10.years - 9.months - 8.days - 7.hours - 6.minutes
// At the time of writing, this value was Nov 22, 2005, 6:51 a.m. EST.
// Extending DateComponents to add even more syntactic magic: fromNow and ago
// --------------------------------------------------------------------------
extension DateComponents {
var fromNow: Date {
return Calendar.current.date(byAdding: self,
to: Date())!
}
var ago: Date {
return Calendar.current.date(byAdding: -self,
to: Date())!
}
}
2.weeks.fromNow
// At the time of writing, this value was
// Sep 13, 2016, 2:55 PM
3.months.fromNow
// At the time of writing, this value was
// Nov 30, 2016, 2:55 PM
(2.months + 3.days + 4.hours + 5.minutes + 6.seconds).fromNow
// At the time of writing, this value was
// Nov 2, 2016, 7:04 PM
(2.months + 3.days + 4.hours + 5.minutes + 6.seconds).ago
// At the time of writing, this value was
// Nov 2, 2016, 7:04 PM