In this part of the tutorial, we’ll expand on what you learned in the previous part by taking Swift’s date and time calculation methods and adding syntactic magic to them.
👀 Look for the 🛠 emoji if you’d like to skim through the content while focusing on the build and execution steps.
💻 You can find an Xcode playground with all the code in this article in this GitHub repository — it’s
date-time-calculations-swift-2.playground
.
Let’s Make Date Calculations in Swift More Elegant
Suppose you 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.
First, you would create a DateComponents
instance representing an interval of 2 months, 3 days, 4 hours, 5 minutes, and 6 seconds.
🛠️ Open Xcode, start a new blank macOS playground (I find them more reliable and less crash-prone than iOS playgrounds) and add the following code to it:
import Cocoa
// Locale, calendar, and date formatter
var userLocale = Locale.autoupdatingCurrent
var gregorianCalendar = Calendar(identifier: .gregorian)
gregorianCalendar.locale = userLocale
let dateFormatter = DateFormatter()
// Time interval of 2 months, 3 days, 4 hours,
// 5 minutes, and 6 seconds
var timeInterval = DateComponents(
month: 2,
day: 3,
hour: 4,
minute: 5,
second: 6
)
You would then add that time interval to the current date using Calendar
’s date(byAdding:to:)
method.
🛠️ Add this code to the playground and run it:
let futureDate = gregorianCalendar.date(
byAdding: timeInterval,
to: Date()
)!
print("\n2 months, 3 days, 4 hours, 5 minutes, and 6 seconds from now is \(futureDate.description).")
The code works, but we can do better in Swift. Wouldn’t it be cool if you could turn the code above into the code below...
let coolFutureDate = Date() + 2.months + 3.days + 4.hours + 5.minutes + 6.seconds
...or perhaps this code?
let coolerFutureDate = (2.months + 3.days + 4.hours + 5.minutes + 6.seconds).fromNow
I’d much rather write the cool code in the examples above. Swift won’t let you do this “out of the box,” but with the code in this article, you can use this syntactic magic.
Extending Date for Simpler Date Creation and Debugging
There are two commonly-used approaches for creating a Date
instance for a specific date and time:
- Creating the
DateComponents
instance with the components that make up the date (the year, month, day, hour, and so on) and aCalendar
instance to give context to convert theDateComponents
into aDate
. - Using a
DateFormatter
— or my preferred version, anISO8601DateFormatter
— to convert a date in string form into aDate
.
Swift’s (or more accurately, Foundation’s) date and time objects make it possible to work with a wide array of calendar systems and date and time formats, but most of the time, you’ll just want to create dates and times for the Gregorian calendar. I’ve always wished that there was a way to instantiate a Date
in a single line of code.
Another issue with the Date
class is the description
property, which returns a terse representation of the Date
value expressed in terms of the UTC time zone. I’ve always wanted a property that fully represents the Date
in my preferred time zone.
The extension below takes items from my wishlist and makes them real.
🛠️ Add this extension to the playground:
extension Date {
// 1
init(
year: Int,
month: Int,
day: Int,
hour: Int? = nil,
minute: Int? = nil,
second: Int? = nil,
timeZoneIdentifier: String = "UTC"
) {
let components = DateComponents(
timeZone: TimeZone(identifier: timeZoneIdentifier),
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second
)
self = Calendar(identifier: .gregorian).date(from: components)!
}
// 2
init(
year: Int,
month: Int,
day: Int,
hour: Int? = nil,
minute: Int? = nil,
second: Int? = nil,
timeZoneAbbreviation: String = "UTC"
) {
let components = DateComponents(
timeZone: TimeZone(abbreviation: timeZoneAbbreviation),
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second
)
self = Calendar(identifier: .gregorian).date(from: components)!
}
// 3
init(iso8601Date: String) {
self = ISO8601DateFormatter().date(from: iso8601Date)!
}
// 4
var desc: String {
get {
let PREFERRED_LOCALE = "en_US" // Use whatever locale you prefer!
return self.description(with: Locale(identifier: PREFERRED_LOCALE))
}
}
}
Here’s a quick explanation of the new additions to Date
. Each item corresponds to the numbered comments in the code above:
- This initializer creates a
Date
instance given a year, month, day, hour, minute, second, and time zone identifier. I prefer using time zone identifiers, which specify time zones in “Continent/City” format, because it frees me from figuring out if it’s under Standard Time or Daylight Saving Time at that moment. TheTimeZone.knownTimeZoneIdentifiers
property contains an array of all the recognized time zone identifier strings. - This initializer is like the first one, except it takes a three-letter time zone abbreviation instead of a time zone identifier.
- This initializer takes a date string in ISO8601 format.
- The
desc
property displays theDate
as a full date and time in the preferred format. I’ve set it to the format I tend to use —en_US
— but you can set it to the format that works for you.
Let’s take these new initializers and property for a test.
🛠️ Add this code to the playground and run it:
// Creating dates with these new initializers
print("Dates:")
// The moment that Steve Jobs told MacWorld 2007 attendees that their
// new “iPod, phone, and internet communicator” was one thing,
// and it was called the iPhone: January 9, 2007, 10:03 a.m. PST.
let iPhoneIntroDate = Date(
year: 2007,
month: 01,
day: 09,
hour: 10,
minute: 3,
timeZoneIdentifier: "America/Los_Angeles"
)
print("• iPhoneIntroDate: \(iPhoneIntroDate.desc)")
// The moment when the Swift programming language was announced
// at WWDC 2014: June 2, 2014, 11:45 PDT.
let swiftIntroDate = Date(
year: 2014,
month: 06,
day: 02,
hour: 11,
minute: 45,
timeZoneAbbreviation: "PDT"
)
print("• swiftIntroDate: \(swiftIntroDate.desc)")
// The moment when Apple Silicon was announced at a special
// WWDC online event: June 22, 2020, 11:27 a.m. PDT.
let appleSiliconIntroDate = Date(iso8601Date: "2020-06-22T11:27:00-07:00")
print("• appleSiliconIntroDate: \(appleSiliconIntroDate.desc)")
Here‘s the output from my computer:
Dates:
• iPhoneIntroDate: Tuesday, January 9, 2007 at 1:03:00 PM Eastern Standard Time
• swiftIntroDate: Monday, June 2, 2014 at 2:45:00 PM Eastern Daylight Time
• appleSiliconIntroDate: Monday, June 22, 2020 at 2:27:00 PM Eastern Daylight Time
Overloading “+” And “-” For Date and Time Calculations
While Calendar
’s date(byAdding:to:)
method works just fine, it might be more elegant if date and time calculations could be performed with +
and -
operators. Let’s give them date and time functionality!
Adding and Subtracting DateComponents
The first step in adding +
and -
to date and time calculations is overloading them so that you can add and subtract DateComponents
instances.
🛠️ Add the following code to the playground:
// The overloads of “+” and “-” rely on this function
func combineComponents(
_ lhs: DateComponents,
_ rhs: DateComponents,
multiplier: Int = 1
) -> DateComponents {
var result = DateComponents()
result.nanosecond = (lhs.nanosecond ?? 0) + (rhs.nanosecond ?? 0) * multiplier
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
}
// Overload “+” so that you can use it to calculate
// DateComponents + DateComponents
func +(_ lhs: DateComponents, _ rhs: DateComponents) -> DateComponents {
return combineComponents(lhs, rhs)
}
// Overload “-” so that you can use it to calculate
// DateComponents - DateComponents
func -(_ lhs: DateComponents, _ rhs: DateComponents) -> DateComponents {
return combineComponents(lhs, rhs, multiplier: -1)
}
}
In the code above, you’ve overloaded the +
and -
operators so that DateComponents
can be added to and subtracted from each other. I derived these functions from Axel Schlueter’s SwiftDateTimeExtensions library. He wrote them when Swift was still in beta; I updated them to compile with the current version and added a couple of tweaks.
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 addition and subtraction of DateComponents
.
You may have noticed many ??
operators in the code for combineComponents()
. ??
is referred to as the nil coalescing operator, and it’s a clever bit of syntactic shorthand.
Consider this expression:
let finalValue = someOptionalValue ?? fallbackValue
In the code above,
- If
someOptionalValue
is notnil
,finalValue
is set tosomeOptionalValue
‘s value. - If
someOptionalValue
isnil
,finalValue
is set tofallbackValue
‘s value.
Let’s confirm that the new operator overloads work.
🛠️ Add the following to the playground and run it:
// Testing the “+” and “-” overloads
let threeDaysTenHoursThirtyMinutes = DateComponents(
day: 3,
hour: 10,
minute: 30
)
let oneDayFiveHoursTenMinutes = DateComponents(
day: 1,
hour: 5,
minute: 10
)
print("\nOverloaded + and - for DateComponents:")
// 3 days, 10 hours, and 30 minutes + 1 day, 5 hours, and 10 minutes
let additionResult = threeDaysTenHoursThirtyMinutes + oneDayFiveHoursTenMinutes
print("• 3 days, 10 hours, and 30 minutes + 1 day, 5 hours, and 10 minutes = \(additionResult.day!) days, \(additionResult.hour!) hours, and \(additionResult.minute!) minutes.")
// 3 days, 10 hours, and 30 minutes - 1 day, 5 hours, and 10 minutes
let subtractionResult = threeDaysTenHoursThirtyMinutes - oneDayFiveHoursTenMinutes
print("• 3 days, 10 hours, and 30 minutes - 1 day, 5 hours, and 10 minutes = \(subtractionResult.day!) days, \(subtractionResult.hour!) hours, and \(subtractionResult.minute!) minutes.")
You should see the following output:
Overloaded + and - for DateComponents:
• 3 days, 10 hours, and 30 minutes + 1 day, 5 hours, and 10 minutes = 4 days, 15 hours, and 40 minutes.
• 3 days, 10 hours, and 30 minutes - 1 day, 5 hours, and 10 minutes = 2 days, 5 hours, and 20 minutes.
Negating DateComponents
Now that you can add and subtract DateComponents
, let’s overload -
— the unary minus.
🛠️ Add this code to the playground:
// Overload unary “-” so that you negate DateComponents
prefix func -(components: DateComponents) -> DateComponents {
var result = DateComponents()
if components.nanosecond != nil { result.nanosecond = -components.nanosecond! }
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
}
This code negates every non-nil
numeric component inside the DateComponent
. With this overload defined, we can now use the unary minus to negate DateComponents
.
🛠️ Add this code to the playground and run it:
// Testing the unary “-” overload
// - (1 day, 5 hours, and 10 minutes)
print("\nOverloaded unary - for DateComponents:")
let negativeTime = -oneDayFiveHoursTenMinutes
print("• Negating 1 day, 5 hours, and 10 minutes turns it into \(negativeTime.day!) days, \(negativeTime.hour!) hours, and \(negativeTime.minute!) minutes.")
You’ll see this output:
Overloaded unary - for DateComponents:
• Negating 1 day, 5 hours, and 10 minutes turns it into -1 days, -5 hours, and -10 minutes.
Adding and Subtracting Dates and DateComponents
With the unary minus defined, we can now define the following operations:
Date + DateComponents
, which makes it easier to answer questions like “What time will it be 1 day, 5 hours, and 10 minutes from now?”DateComponents + Date
, which should be possible because addition is commutative (which is just a fancy way of saying that a + b and b + a should give you the same result).Date - DateComponents
, which makes it easier to answer questions like “What time was it 3 days, 10 hours, and 30 minutes ago?”
🛠️ Add this code to the playground:
// Overload “+” so that you can use it to calculate
// Date + DateComponents
func +(_ lhs: Date, _ rhs: DateComponents) -> Date
{
return Calendar.current.date(byAdding: rhs, to: lhs)!
}
// Overload “+” so that you can use it to calculate
// DateComponents + Dates
func +(_ lhs: DateComponents, _ rhs: Date) -> Date
{
return rhs + lhs
}
// Overload “-” so that you can use it to calculate
// Date - DateComponents
func -(_ lhs: Date, _ rhs: DateComponents) -> Date
{
return lhs + (-rhs)
}
Note that we didn’t define an overload for calculating DateComponents
- Date
— 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.
🛠️ Add the following to the playground and run it:
// What time will it be 1 day, 5 hours, and 10 minutes from now?
print("\nOverloaded + and - for Date/DateComponent calculations:")
// Here's the standard way of finding out:
let futureDate0 = Calendar.current.date(
byAdding: oneDayFiveHoursTenMinutes,
to: Date()
)
// With our overloads and function definitions, we can now do it this way:
print("• Date() + oneDayFiveHoursTenMinutes = \((Date() + oneDayFiveHoursTenMinutes).desc).")
// This will work as well:
print("• oneDayFiveHoursTenMinutes + Date() = \((oneDayFiveHoursTenMinutes + Date()).desc).")
// Let’s see subtraction in action:
print("• Date() - threeDaysTenHoursThirtyMinutes = \((Date() - threeDaysTenHoursThirtyMinutes).desc).")
Calculating the Difference between Two Dates
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.
🛠️ Add this code to the playground and run it:
func -(_ lhs: Date, _ rhs: Date) -> DateComponents
{
return Calendar.current.dateComponents(
[.year, .month, .weekOfYear, .day, .hour, .minute, .second, .nanosecond],
from: rhs,
to: lhs)
}
The code above overloads -
when both operands are Date
s so that it uses Calendar’s dateComponents(_:from:to:)
method, which calculates the date components for an interval between two given dates.
🛠️ Add the following to the playground and run it:
print("\nOverloaded - for Date subtraction:")
// What’s the time difference between:
// - The date Apple Silicon was introduced (June 22, 2020, 11:27 a.m. UTC-7) and
// - The date the iPhone was introduced (January 9, 2007, 10:03 a.m. UTC-8)?
let introInterval = appleSiliconIntroDate - iPhoneIntroDate
print("• The interval between the introduction of the iPhone and the introduction of Apple Silicon is \(introInterval.year!) years, \(introInterval.month!) months, \(introInterval.day!) days, \(introInterval.hour!) hours, and \(introInterval.minute!) minutes.")
You can now perform date calculations using +
and -
!
Adding Syntactic Magic to DateComponents
You’ve already got some syntactic niceties for date calculations, but you’re not done yet!
Extending Int to Make DateComponent Creation More Readable
🛠️ Add the code below to the playground:
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
}
}
These additions to Int
allow you to convert Int
s to DateComponents
in an easy-to-read way. Each property in this extension has a singular and plural version, which allows you to write grammatically correct expressions such as 1.second
and 2.seconds
.
With the overloads to add and subtract DateComponents
to and from each other, and the overloads to add Date
s to DateComponents
, you can now perform all sorts of syntactic magic.
🛠️ Add the following to the playground and run it:
// A quick test of some future dates
print("One hour from now is: \((Date() + 1.hour).desc)")
print("One day from now is: \((Date() + 1.day).desc)")
print("One week from now is: \((Date() + 1.week).desc)")
print("One month from now is: \((Date() + 1.month).desc)")
print("One year from now is: \((Date() + 1.year).desc)")
// What was the date 10 years, 9 months, 8 days, 7 hours, and 6 minutes ago?
let aLittleWhileBack = Date() - 10.years - 9.months - 8.days - 7.hours - 6.minutes
print("10 years, 9 months, 8 days, 7 hours, and 6 minutes ago, it was: \(aLittleWhileBack.desc)")
Adding Even More Syntactic Magic with “fromNow” And “ago”
And finally, a couple of additions to DateComponents
to make Date
/DateComponent
calculations even more concise and readable.
🛠️ Add this code to the playground:
extension DateComponents {
var fromNow: Date {
return Calendar.current.date(
byAdding: self,
to: Date()
)!
}
var ago: Date {
return Calendar.current.date(
byAdding: -self,
to: Date()
)!
}
}
🛠️ To see the syntactic magic in action, add the following to the playground and run it:
// Test “fromNow” and “ago”
print("\nSome serious syntactic magic:")
print("• 2.weeks.fromNow: \(2.weeks.fromNow.desc)")
print("• 3.months.fromNow: \(3.months.fromNow.desc)")
// What date/time will it be 2 months, 3 days, 4 hours,
// 5 minutes, and 6 seconds from now?
let futureDate3 = (2.months + 3.days + 4.hours + 5.minutes + 6.seconds).fromNow
print("• (2.months + 3.days + 4.hours + 5.minutes + 6.seconds).fromNow: \(futureDate3.desc).")
// What date/time was it 2 months, 3 days, 4 hours,
// 5 minutes, and 6 seconds ago?
let pastDate2 = (2.months + 3.days + 4.hours + 5.minutes + 6.seconds).ago
print("• (2.months + 3.days + 4.hours + 5.minutes + 6.seconds).ago: \(pastDate2.desc)")
Summary
💻 Once again, you can find an Xcode playground with all the code in this article in this GitHub repository — it’s
date-time-calculations-swift-2.playground
.
In this tutorial, you covered a lot of ground:
- Determining if a given time zone is under Standard Time or Daylight Saving Time on a given date
Date
comparisons, including comparisons at more “human” levels of granularity- Sorting
Date
s - Find the difference between two
Date
s in seconds, minutes, hours, days, months, years, and any combination of these - Adding a time interval to a
Date
to create a newDate
- Calculating the next occurrence of a specific date or time
- Extending
Date
with new initializers and a new property to make date instantiation easier - Overloading
+
and-
to work withDate
s andDateComponents
- Adding syntactic magic to
DateComponents
Use the material in this tutorial to expand your understanding of working with dates and times in Swift, and to write your own date and time utilities. I recommend that you consult these other pages to expand your knowledge:
- Dates and Times from Apple’s documentation
- Swift Date by Harish Suthar
- Time Steering in Swift 5.5 by Sai Durga Mahesh
- Why is Programming with Dates So Hard? by Dave Taubler
- Falsehoods programmers believe about time
Here’s some bonus reading: The Hummingbird Effect: How Galileo Invented Timekeeping and Forever Changed Modern Life. It’s not directly connected to Swift’s date- and time-related objects, but Galileo’s observations of a swinging altar lamp in the cathedral in Pisa led to a revolution in timepieces and timekeeping that lives on in today’s iOS (and watchOS!) devices.