
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 Date
s, and allows us to convert between Date
s and DateComponents
.
- The
DateFormatter
class, which converts Date
s into formatted String
s, and formatted String
s into Date
s.
- Date arithmetic, which make it possible to add time intervals to
Date
s, find the difference in time between two Date
s, and compare Date
s.
A more readable way to work with Date
s 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
DateComponent
s 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 Date
s and DateComponents
and subtract DateComponents
from Date
s
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 Date
s 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 Date
s
When we’re trying to determine the time between two given Date
s, 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 Date
s, 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 Int
s 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 Date
s 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