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, andCalendar
, which provides a context forDate
s, and allows us to convert betweenDate
s andDateComponents
.
- The
DateFormatter
class, which convertsDate
s into formattedString
s, and formattedString
s intoDate
s. - Date arithmetic, which make it possible to add time intervals to
Date
s, find the difference in time between twoDate
s, and compareDate
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
‘sdate(byAdding:to:)
method to add the time interval to aDate
.
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 notnil
,finalValue
is set tosomeOptionalValue
‘s value. - If
someOptionalValue
isnil
,finalValue
is set tofallbackValue
‘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 aDate
usingCalendar
‘sdate(from:)
method, or - Creating a
String
representation of theDate
and then using it to create aDate
usingDateFormatter
‘sdate(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