developers

Date and Time Calculations in Swift, Part 1

Learn how to perform date and time calculations with Swift’s powerful date and time objects.

Apr 27, 202324 min read

Some developers say Swift has an overengineered system for working with dates and times. I disagree; it’s complex because date and time programming is complex. To help you navigate that complexity, Swift provides powerful tools for working with time across date and time formats, time zones, languages, and even different calendar systems.

In this article, I’ll show you how to perform date calculations in Swift and answer these questions:

  • How much time is there between two given
    Date
    s? Can you express this time in terms of years, months, days, hours, minutes, seconds, or some combination thereof?
  • If you add or subtract a given amount of time to or from a given
    Date
    , what is the resulting date? For example, when does a 90-day warranty that starts today expire?
  • When is the next day of the week, day of the month, and so on? For example, what is the date of the next Sunday? Or the previous Sunday?
  • When is the next Friday the 13th? How many Friday the 13ths will there be in 2024?

To get the most out of this tutorial, you should be familiar with the following Swift objects:

  • Date
    , which represents a single point in time as seconds relative to the start of the Third Millennium.
  • DateComponents
    , a struct that 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.
  • Calendar
    , which provides a context for
    Date
    s, and allows us to convert between
    Date
    s and
    DateComponents
    .
  • DateFormatter
    , which converts
    Date
    s into formatted strings, and formatted strings into
    Date
    s.

I covered these topics in an earlier tutorial with two parts, Introduction to Date and Time Programming in Swift, Part 1 and Introduction to Date and Time Programming in Swift, Part 2.

This tutorial is an interactive exercise you’ll perform in an Xcode playground. You’ll learn by doing.

👀 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-1.playground
.

Setup

To work with dates and times in Swift effectively, it’s helpful to set up a playground with some companion objects.

Locale, calendar, and date formatter

The first ones you’ll define are:

  • A
    Locale
    object. This defines the language and format of information presented to the user. For example, if the locale for the user’s device is set to
    en_US
    , dates and times will be displayed in American English, with times shown using the 12-hour clock and “AM” and “PM”, and dates shown in month-day-year format.
  • A
    Calendar
    object, which provides a user-friendly context for
    Date
    objects, which treat time as a number of seconds before or after January 1, 2001, 00:00:00 UTC. Swift supports several calendar systems, but we’ll stick to the Gregorian calendar, the generally accepted standard worldwide.

🛠️ 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 and calendar
var userLocale = Locale.autoupdatingCurrent
var gregorianCalendar = Calendar(identifier: .gregorian)
gregorianCalendar.locale = userLocale

Locale.autoupdatingCurrent
is a property that always contains the user’s current locale settings, even when they change. Setting
userLocale
to this value means that
userLocale
always contains the current locale settings.

You’ll also make use of a couple of a

DateFormatter
, which you’ll use to convert
Date
objects into “friendly” strings that display the date, time, or both.

🛠️ Add a date formatter to the playground with the following code:

// Date formatter
var dateFormatter = DateFormatter()
dateFormatter.dateStyle = .full
dateFormatter.timeStyle = .full

Dates

Finally, you’ll need some

Date
objects, which we’ll use to perform date calculations.

Start with one of the most meaningful dates for iOS developers: the date and time when the original iPhone was introduced to the world. This happened at MacWorld San Francisco on January 1, 2007, about three minutes into Steve Jobs’ famous keynote.

🛠️ Add the following to the playground and run it:

// The original iPhone first made its appearance at about
// 10:03 a.m. (UTC-8) on January 9, 2007.
let iPhoneIntroComponents = DateComponents(
  timeZone: TimeZone(identifier: "America/Los_Angeles"),
  year: 2007,
  month: 1,
  day: 9,
  hour: 10,
  minute: 3
)
let iPhoneIntroDate = gregorianCalendar.date(from: iPhoneIntroComponents)!
let iPhoneIntroDateFormatted = dateFormatter.string(from: iPhoneIntroDate)
print("Introductions:")
print("• The iPhone was introduced to the world on \(iPhoneIntroDateFormatted).")

The code above does the following:

  • It instantiates a
    DateComponents
    object containing the components that comprise the date and time of January 9, 2007, 10:00 a.m., in the time zone of Los Angeles, USA, a time zone shared by San Francisco, where the keynote took place.
  • It uses those
    DateComponents
    with our Gregorian
    Calendar
    instance to create
    iPhoneIntroDate
    , a
    Date
    representing the date and time of the iPhone’s introduction to the world.
  • It uses our
    DateFormatter
    instance to convert
    iPhoneIntroDate
    into a human-friendly date and time string, which appears in the output console pane.

Add two more dates:

  • June 2, 2014, 11:45 a.m. Pacific Daylight Time, which is the moment when the Swift programming language was announced at WWDC ’14.
  • June 22, 2020, 11:27 a.m. Pacific Daylight Time, the point in the special online WWDC event (remember, we were all in lockdown) when Apple Silicon was announced.

🛠️ Add this code to the playground and run it:

// Swift first made its appearance at about
// 11:45 a.m. (UTC-7) on June 2, 2014
let swiftIntroDateComponents = DateComponents(
  timeZone: TimeZone(identifier: "America/Los_Angeles"),
  year: 2014,
  month: 6,
  day: 2,
  hour: 11,
  minute: 45
)
let swiftIntroDate = gregorianCalendar.date(from: swiftIntroDateComponents)!
let swiftIntroDateFormatted = dateFormatter.string(from: swiftIntroDate)
print("• Swift was introduced to the world on \(swiftIntroDateFormatted).")

// Apple Silicon first made its appearance at about
// 11:27 a.m. (UTC-7) on June 22, 2020
let appleSiliconIntroDateComponents = DateComponents(
  timeZone: TimeZone(identifier: "America/Los_Angeles"),
  year: 2020,
  month: 6,
  day: 22,
  hour: 11,
  minute: 27
)
let appleSiliconIntroDate = gregorianCalendar.date(from: appleSiliconIntroDateComponents)!
let appleSiliconIntroDateFormatted = dateFormatter.string(from: appleSiliconIntroDate)
print("• Apple Silicon was introduced to the world on \(appleSiliconIntroDateFormatted).")

Is It Daylight Saving Time in This Time Zone?

You may need to know if a given time zone is Standard Time or Daylight Saving Time for a given date and time. This is often difficult to do manually since different parts of the world have different rules about the change to Daylight Saving Time.

Fortunately,

TimeZone
has the
isDaylightSavingTime(for:)
method, which returns
true
if the time zone is under Daylight Saving Time on the given
Date
.

Consider the date commonly known as “The Ides of March” — March 15. Let’s see if Daylight Saving Time was on that date in 2023 in two different time zones: US Pacific and Berlin.

🛠️ Add the following to the playground and run it:

let idesOfMarchDateComponents = DateComponents(
  timeZone: TimeZone(secondsFromGMT: 0),
  year: 2024,
  month: 3,
  day: 15,
  hour: 0,
  minute: 0
)
let idesOfMarchDate = gregorianCalendar.date(from: idesOfMarchDateComponents)!
print("\nDaylight Saving Time:")
let pacificTimeZone = TimeZone(identifier: "America/Los_Angeles")!
let isDSTInPacificTimeZone = pacificTimeZone.isDaylightSavingTime(for: idesOfMarchDate)
print("• Is it Daylight Saving Time in the Pacific time zone on March 15, 2024?: \(isDSTInPacificTimeZone)")
let berlinTimeZone = TimeZone(identifier: "Europe/Berlin")!
let isDSTInBerlinTimeZone = berlinTimeZone.isDaylightSavingTime(for: idesOfMarchDate)
print("• Is it Daylight Saving Time in Berlin on March 15, 2024?: \(isDSTInBerlinTimeZone)")

You should see this output:

Daylight Saving Time:
• Is it Daylight Saving Time in the Pacific time zone on March 15, 2024?: true
• Is it Daylight Saving Time in Berlin on March 15, 2024?: false

On March 15, 2024, the Pacific time zone had already switched to Daylight Saving Time, but Berlin (and much of Europe) would have to wait another two weeks.

Comparing Dates

You can compare

Date
s using Swift’s standard comparison, equality and inequality operators.

🛠️ Add the following to the playground and run it:

print("\nDate comparisons:")
print("• Swift was announced BEFORE SwiftUI: " +
      "\(swiftIntroDate < appleSiliconIntroDate)")
print("• Swift was announced AFTER SwiftUI: " +
      "\(swiftIntroDate > appleSiliconIntroDate)")
print("• Swift and SwiftUI were announced at the SAME date and time: " +
      "\(swiftIntroDate == appleSiliconIntroDate)")
print("• Swift and SwiftUI were announced at DIFFERENT dates and times: " +
      "\(swiftIntroDate != appleSiliconIntroDate)")

swiftIntroDate
’s year is 2014, and
appleSiliconIntroDate
’s year is six years after that, so you should see an output that looks like this:

Date comparisons:
• Swift was announced BEFORE SwiftUI: true
• Swift was announced AFTER SwiftUI: false
• Swift and SwiftUI were announced at the SAME date and time: false
• Swift and SwiftUI were announced at DIFFERENT dates and times: true

Sorting Dates

Sorting relies on comparing two values. If two values of a given type can be compared, any number of values of that type can be sorted. You can see this by sorting an array of

Date
s into chronological order.

🛠️ Add this code to the playground and run it:

print("\nSince dates can be compared, they can also be sorted:")
let scrambledDates = [
  appleSiliconIntroDate,
  iPhoneIntroDate,
  swiftIntroDate
]
let sortedDates = scrambledDates.sorted()
for date in sortedDates {
  let dateFormatted = dateFormatter.string(from: date)
  print("• \(dateFormatted)")
}

Date Calculations in Seconds

Remember that to be as flexible as possible,

Date
objects represent time as a number of seconds before or after January 1, 2001, 00:00:00 UTC. As a result,
Date
calculations in seconds require very little code.

The difference between two dates, in seconds

The

timeIntervalSince()
method returns the difference in seconds between two given
Date
instances.

🛠️ Use

timeIntervalSince()
by adding the following to the playground and running it:

print("\nIntervals between Dates, in seconds:")
print("• Number of seconds between the Swift and SwiftUI announcements: " +
    "\(swiftIntroDate.timeIntervalSince(appleSiliconIntroDate))")
print("• Number of seconds between the SwiftUI and Swift announcements: " +
    "\(appleSiliconIntroDate.timeIntervalSince(swiftIntroDate))")

Adding or subtracting seconds to or from a date

Date and time calculations often involve adding or subtracting an interval of time — a given number of seconds, minutes, hours, days, months, and so on — to or from a given date.

Date
objects work only in terms of seconds, and those are the only values you can directly use to add or subtract time from a
Date
.

There are three ways to add seconds to or subtract seconds from a date:

🛠️ Add this code to the playground and run it:

let swiftUIIntroDates = [
  "swiftIntroDate"         : swiftIntroDate,
  
  // Adding and subtracting seconds to and from a Date with + and -
  "swiftIntroDate1SecondLater"   : swiftIntroDate + 1,
  "swiftIntroDate1SecondEarlier" : swiftIntroDate - 1,
  
  // Adding and subtracting seconds to and from a Date with addingTimeInterval()
  "swiftIntroDate1MinuteLater"   : swiftIntroDate.addingTimeInterval(60),
  "swiftIntroDate1MinuteEarlier" : swiftIntroDate.addingTimeInterval(-60),
  
  // Adding and subtracting seconds to and from a Date with advanced(by:)
  "swiftIntroDate1HourLater"   : swiftIntroDate.advanced(by: 60 * 60),
  "swiftIntroDate1HourEarlier"   : swiftIntroDate.advanced(by: -60 * 60),
]
for date in swiftUIIntroDates {
  print("• \(date.key): \(date.value.description(with: userLocale))")
}

The underlying code for all the ways listed above is the same. The implementation for

addingTimeInterval()
, which comes from the Objective-C era, uses the code for
+
. The Swift team intended
advanced(by:)
to replace
addingTimeInterval()
to support some new
Date
features, but those features invited a lot of common date and time programming errors. The only difference between
addingTimeInterval()
and
advanced(by:)
is that
advanced(by:)
’s syntax is more Swift-like.

Date Calculations in Units That Makes Sense to Us

Most people don’t say things like “I enjoyed the vacation I took 32 million seconds ago” or “Don’t forget your appointment; it’s 518,400 seconds from now!” They say that their vacation was a year ago, and that the upcoming appointment is in six days.

Swift’s

Date
object works only in terms of seconds, but when you use a
Calendar
object to put those seconds in the context of units we understand — minutes, hours, days, weeks, months, and years — you can perform date and time calculations in Swift using units that make sense to us.

The difference between two dates, in days

Calendar
’s
dateComponents(_:from:to:)
method takes an array of familiar time units and two
Dates
and returns the difference between those
Dates
in the given units.

Let’s start by calculating the number of days between the introduction of Swift (June 2, 2014) and the introduction of Apple Silicon (June 22, 2020).

🛠️ Add the following to the playground and run it:

print("\nIntervals between Dates, in more convenient units:")

let daysBetweenAnnouncements = gregorianCalendar.dateComponents(
  [.day],
  from: swiftIntroDate,
  to: appleSiliconIntroDate
)
print("• There were \(daysBetweenAnnouncements.day!) days between the introductions of Swift and Apple Silicon.")

You’ll see an output that says that 2,211 days passed between the introductions of Swift and Apple Silicon. That’s more meaningful than the number of seconds between the two dates, but we can do better.

The difference between two dates, in weeks

Make a similar call to

dateComponents(_:from:to:)
, but this time, specify that the answer should be in terms of weeks instead of days.

🛠️ Add this code to the playground and run it:

let weeksBetweenAnnouncements = gregorianCalendar.dateComponents(
  [.weekOfYear],
  from: swiftIntroDate,
  to: appleSiliconIntroDate
)
print("• There were \(weeksBetweenAnnouncements.weekOfYear!) weeks between the introductions of Swift and Apple Silicon.")

You’ll see that the Swift and Apple Silicon introductions are 315 weeks apart. You can improve on this result.

The difference between two dates in years, months, days, hours, and minutes

dateComponents(_:from:to:)
’s first parameter doesn’t take a single value, but an array of values. You can use this array to specify a number of time units — in this case, years, months, days, hours, and minutes — to provide a more meaningful result.

🛠️ Add the following to the playground and run it:

let ymdhmBetweenAnnouncements = gregorianCalendar.dateComponents(
  [.year, .month, .day, .hour, .minute],
  from: swiftIntroDate,
  to: appleSiliconIntroDate
)
var years = ymdhmBetweenAnnouncements.year!
var months = ymdhmBetweenAnnouncements.month!
var days = ymdhmBetweenAnnouncements.day!
var hours = ymdhmBetweenAnnouncements.hour!
var minutes = ymdhmBetweenAnnouncements.minute!
print("• There were \(years) years, \(months) months, \(days) days, \(hours) hours, and \(minutes) minutes between the introductions of Swift and Apple Silicon.")

When you run the playground, the output will specify that the time between, say, the introductions of Swift and Apple Silicon spanned 6 years, 19 days, 23 hours, and 42 minutes. This is the form that most people would prefer.

It’s helpful to define a constant array containing the date components representing year, month, day, hour, minute, and second, as shown below.

🛠️ Define the constant array and use it by adding the code below to the playground and running it:

// Year-month-date-hours-minutes-seconds components list
let ymdhmsComponentsList: Set = [
  Calendar.Component.year,
  Calendar.Component.month,
  Calendar.Component.day,
  Calendar.Component.hour,
  Calendar.Component.minute,
  Calendar.Component.second
]
let ymdhmsBetweeniPhoneIntroAndNow = gregorianCalendar.dateComponents(
  ymdhmsComponentsList,
  from: iPhoneIntroDate,
  to: Date()
)
print("• ymdhmsBetweeniPhoneIntroAndNow: \(ymdhmsBetweeniPhoneIntroAndNow)")

Note that negative time units mean intervals going into the past.

🛠️ Add the code below and run it to calculate the time between the present moment and the introduction of Swift:

let ymdhmsBetweenNowAndSwiftIntro = gregorianCalendar.dateComponents(
  ymdhmsComponentsList,
  from: Date(),
  to: swiftIntroDate
)
var seconds: Int
(years, months, days, hours, minutes, seconds) = (
  ymdhmsBetweenNowAndSwiftIntro.year!,
  ymdhmsBetweenNowAndSwiftIntro.month!,
  ymdhmsBetweenNowAndSwiftIntro.day!,
  ymdhmsBetweenNowAndSwiftIntro.hour!,
  ymdhmsBetweenNowAndSwiftIntro.minute!,
  ymdhmsBetweenNowAndSwiftIntro.second!
)
print("• To go to the keynote when Swift was introduced, you’d have to travel \(years) years, \(months) months, \(days) days, \(hours) hours, \(minutes) minutes, and \(seconds) seconds.")

As you can see, the resulting time units are negative because Swift’s introduction happened in the past.

Adding or subtracting days, weeks, months, and years to or from a date

Calendar
objects do more than convert
Date
values into meaningful time units, such as minutes, hours, days, weeks, months, and more. They also provide the
date(byAdding:value:to:)
method, which performs date and time arithmetic using those units. Let’s answer some questions using this method.

Suppose you bought something today, and it has a 90-day warranty. You want to know the date when the warranty expires. You can find out by adding 90 days to the present date with

date(byAdding:value:to:)
.

🛠️ Add the following to the playground and run it:

print("\nAdding and subtracting to and from Dates:")

// I bought something today, and it has a 90-day warranty.
// When does that warranty end?
let ninetyDaysFromNow = gregorianCalendar.date(
  byAdding: .day,
  value: 90,
  to: Date()
)!
dateFormatter.timeStyle = .none
let ninetyDaysFromNowFormatted = dateFormatter.string(from: ninetyDaysFromNow)
print("• 90 days from now is: \(ninetyDaysFromNowFormatted).")

How about determining what the date was five weeks ago?

🛠️ Add this code to the playground and run it:

// What was the date five weeks ago?
let fiveWeeksAgo = gregorianCalendar.date(
  byAdding: .weekOfYear,
  value: -5,
  to: Date()
)!
let fiveWeeksAgoFormatted = dateFormatter.string(from: fiveWeeksAgo)
print("• 5 weeks ago was: \(fiveWeeksAgoFormatted).")

In the previous examples, you added and subtracted only one kind of time unit to and from a date: 90 days in the first one, and 5 weeks in the second. To add and subtract combinations of time units— for example, 4 hours and 30 minutes — you’ll need to assemble them into a

DateComponents
instance.

🛠️ Add the following to the playground and run it:

// What time will it be 4 hours and 30 minutes from now?
// First, we need to define a DateComponents struct representing
// a time interval of 4 hours and 30 minutes
var fourHoursThirtyMinutes = DateComponents()
fourHoursThirtyMinutes.hour = 4
fourHoursThirtyMinutes.minute = 30

// Now add the interval to the Date
let fourHoursThirtyMinutesFromNow = gregorianCalendar.date(
  byAdding: fourHoursThirtyMinutes,
  to: Date()
)!
dateFormatter.timeStyle = .full
let fourHoursThirtyMinutesFromNowFormatted = dateFormatter.string(from: fourHoursThirtyMinutesFromNow)
print("• 4 hours and 30 minutes from now will be: \(fourHoursThirtyMinutesFromNowFormatted).")

Here’s another example, which calculates what time it was 2 years, 17 days, 10 hours and 3 minutes ago.

🛠️ Add this code to the playground and run it:

// What time was it 2 years, 17 days, 10 hours and 3 minutes ago?
var pastInterval = DateComponents()
pastInterval.year = -2
pastInterval.day = -17
pastInterval.hour = -10
pastInterval.minute = -3
let pastDate = gregorianCalendar.date(
  byAdding: pastInterval,
  to: Date()
)!
let pastDateFormatted = dateFormatter.string(from:pastDate)
print("• 2 years, 17 days, 10 hours and 3 minutes ago was: \(pastDateFormatted).")

Date Comparisons That Feel 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 a few writers were aware of this annoying trope in later series. Consider this dialogue from Star Trek: 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.

🛠️ To see this level of precision in action, add the following to the playground and run it:

// Technically speaking, these two dates and times are different:
// - the date and time when Apple Silicon made its first public appearance
// - one second afterward
// ...are two different dates and times
let appleSiliconIntroDatePlus1Second = appleSiliconIntroDate + 1
print("• appleSiliconIntroDate == appleSiliconIntroDatePlus1Second: " +
    "\(appleSiliconIntroDate == appleSiliconIntroDatePlus1Second)")

The output shows that

appleSiliconIntroDate
and
appleSiliconIntroDatePlus1Second
are not equal. This is technically correct, and while
Date
’s microsecond-level accuracy is useful in some cases, it’s too precise for many day-to-day activities. For a keynote presentation, appointment, or many other social events, a difference of a few seconds or even a few minutes doesn’t matter.

Fortunately,

Calendar
provides us with the
compare(_:to:toGranularity)
method, which compares two dates at a given level of precision. This precision can be minutes, hours, days, and more.

To see this method in action, create these extra

Date
values:

  • 5 minutes after the moment when Apple Silicon was announced
  • 3 hours after the moment when Apple Silicon was announced

🛠️ Add the following to the playground:

// Defining a couple of extra dates for later comparison:
// - 5 minutes after the first public appearance of Apple Silicon
// - 3 hours after the first public appearance of Apple Silicon
let appleSiliconIntroDatePlus5Minutes = gregorianCalendar.date(
  byAdding: .minute,
  value: 5,
  to: appleSiliconIntroDate
)!
let appleSiliconIntroDatePlus3Hours = gregorianCalendar.date(
  byAdding: .hour,
  value: 3,
  to: appleSiliconIntroDate
)!

With these

Date
s defined, you can put
compare(_:to:toGranularity)
to use. Compare
appleSiliconIntroDate
to
appleSiliconIntroDatePlus1Second
at the
second
level of granularity.

🛠️ Add this code to the playground and run it:

// At the .second level of granularity,
// appleSiliconIntroDate != appleSiliconIntroDatePlus1Second
let test1 = gregorianCalendar.compare(
  appleSiliconIntroDate,
  to: appleSiliconIntroDatePlus1Second,
  toGranularity: .second
) == .orderedSame
print("• appleSiliconIntroDate == appleSiliconIntroDatePlus1Second (with second granularity): \(test1)")

// At the .second level of granularity,
// appleSiliconIntroDate < appleSiliconIntroDatePlus1Second
let test2 = gregorianCalendar.compare(
  appleSiliconIntroDate,
  to: appleSiliconIntroDatePlus1Second,
  toGranularity: .second
) == .orderedAscending
print("• appleSiliconIntroDate < appleSiliconIntroDatePlus1Second (with second granularity): \(test2)")

The output will tell you that at the

.second
level of granularity,
appleSiliconIntroDate
and
appleSiliconIntroDatePlus1Second
are not the same time, and that
appleSiliconIntroDatePlus1Second
takes place after
appleSiliconIntroDate
.

If you compare the two

Date
s at the
.minute
level of granularity, you get a different result.

🛠️ Add the following to the playground and run it:

// At the .minute level of granularity,
// appleSiliconIntroDate == appleSiliconIntroDatePlus1Second
let test3 = gregorianCalendar.compare(
  appleSiliconIntroDate,
  to: appleSiliconIntroDatePlus1Second,
  toGranularity: .minute
) == .orderedSame
print("• appleSiliconIntroDate == appleSiliconIntroDatePlus1Second (with minute granularity): \(test3)")

With

.minute
level of granularity,
appleSiliconIntroDatePlus5Minutes
is a later time than
appleSiliconIntroDate
:

🛠️ Add this code to the playground and run it:

// At the .minute level of granularity,
// appleSiliconIntroDatePlus5Minutes > appleSiliconIntroDate
let test4 = gregorianCalendar.compare(
  appleSiliconIntroDatePlus5Minutes,
  to: appleSiliconIntroDate,
  toGranularity: .minute
) == .orderedDescending
print("• appleSiliconIntroDatePlus5Minutes > appleSiliconIntroDate (with minute granularity): \(test4)")

At the

.second
level of granularity,
compare(_:to:toGranularity)
reports that
appleSiliconIntroDate
and
appleSiliconIntroDatePlus1Second
are roughly the same time. You’ll see the same result when comparing
appleSiliconIntroDate
and
appleSiliconIntroDatePlus5Minutes
at the
.hour
level of granularity.

🛠️ Add the following to the playground and run it:

// At the .hour level of granularity,
// appleSiliconIntroDate == appleSiliconIntroDatePlus5Minutes
let test5 = gregorianCalendar.compare(
  appleSiliconIntroDate,
  to: appleSiliconIntroDatePlus5Minutes,
  toGranularity: .hour
) == .orderedSame
print("• appleSiliconIntroDate == appleSiliconIntroDatePlus5Minutes (with hour granularity): \(test5)")

Try one more comparison:

appleSiliconIntroDate
and
appleSiliconIntroDatePlus3Hours
at the
.day
level of granularity:

// At the .day level of granularity,
// appleSiliconIntroDate == appleSiliconIntroDatePlus3Hours
let test6 = gregorianCalendar.compare(
  appleSiliconIntroDate,
  to: appleSiliconIntroDatePlus3Hours,
  toGranularity: .day
) == .orderedSame
print("• appleSiliconIntroDate == appleSiliconIntroDatePlus3Hours (with day granularity): \(test6)")

When viewed in terms of days, the time when an event occurred and three hours afterward are the same.

Calculating “Next Dates”

Applications that schedule events will often have to calculate “next dates.” On many other platforms, this would require a lot of work, but in Swift, a call to

Calendar
’s
nextDate(after:matching:matchingPolicy:repeatedTimePolicy:direction:)
method is all you need.

For simplicity’s sake, I’ll break with Swift convention and refer to this method as

nextDate()
.

The concept of “Next dates” is easier to demonstrate rather than explain, so I’ll do just that. Let’s start with some code to display a

Date
representing the next time it will be 3 in the morning.

🛠️ Add this code to the playground and run it:

// When is the next time it will be 3:00 a.m.?
print("\nNext Dates:")
let next3AmComponents = DateComponents(hour: 3)
let next3AmDate = gregorianCalendar.nextDate(
  after: Date(),
  matching: next3AmComponents,
  matchingPolicy: .nextTime
)!
let next3AmFormatted = dateFormatter.string(from: next3AmDate)
print("• The next time it will be 3:00 a.m. is: \(next3AmFormatted).")

What are the dates of the previous and next Sundays?

nextDate()
can do this calculation in two lines of code.

🛠️ Add the following to the playground and run it:

// When are the dates of:
// - The previous Sunday?
// - Next Sunday?
let sundayComponents = DateComponents(
  weekday: 1
)
let previousSunday = gregorianCalendar.nextDate(
  after: Date(),
  matching: sundayComponents,
  matchingPolicy: .nextTime,
  direction: .backward
)!
let nextSunday = gregorianCalendar.nextDate(
  after: Date(),
  matching: sundayComponents,
  matchingPolicy: .nextTime,
  direction: .forward
)!
dateFormatter.timeStyle = .none
let previousSundayFormatted = dateFormatter.string(from: previousSunday)
let nextSundayFormatted = dateFormatter.string(from: nextSunday)
print("• The previous Sunday was \(previousSundayFormatted).")
print("• The next Sunday will be \(nextSundayFormatted).")

By setting the optional

direction:
parameter to
.backward
, you can get the “previous next date.” That’s how you got the date for the previous Sunday.

Many meetings are scheduled in relative terms, such as “the third Friday of the month.” When is the next third Friday of the month?

🛠️ Add this code to the playground and run it:

// When is the next “third Friday of the month?”
let nextThirdFridayComponents = DateComponents(
  weekday: 6,
  weekdayOrdinal: 3
)
let nextThirdFridayDate = gregorianCalendar.nextDate(
  after: Date(),
  matching: nextThirdFridayComponents,
  matchingPolicy: .nextTime
)!
let nextThirdFridayFormatted = dateFormatter.string(from: nextThirdFridayDate)
print("• The next third Friday of the month will be \(nextThirdFridayFormatted).")

When is the next unlucky day?

In many Western cultures, Friday the 13th is considered an unlucky day. When is the next Friday the 13th?

🛠️ Add the following to the playground and run it:

print("\nUnlucky days:")

// When is the next Friday the 13th, an unlucky day in many western cultures?
let friday13thComponents = DateComponents(
  day: 13,
  weekday: 6)
let nextFriday13thDate = gregorianCalendar.nextDate(
  after: Date(),
  matching: friday13thComponents,
  matchingPolicy: .nextTime
)!
let nextFriday13thFormatted = dateFormatter.string(from: nextFriday13thDate)
print("• The next Friday the 13th will be on \(nextFriday13thFormatted).")

Note that Sunday is considered the first

weekday
and its index is 1. Hence the
weekday
value of
6
represents Friday.

In Spain, the day associated with bad luck isn’t Friday the 13th, but Tuesday the 13th.

🛠️ Add this code to the playground and run it:

// When is the next Tuesday the 13th, an unlucky day in Spain?
let tuesday13thComponents = DateComponents(
  day: 13,
  weekday: 3)
let nexttuesday13thDate = gregorianCalendar.nextDate(
  after: Date(),
  matching: tuesday13thComponents,
  matchingPolicy: .nextTime
)!
let nexttuesday13thFormatted = dateFormatter.string(from: nexttuesday13thDate)
print("• The next Tuesday the 13th will be on \(nexttuesday13thFormatted).")

And finally, in Italy, the unlucky day is Friday the 17th.

🛠️ Add the following to the playground and run it:

// When is the next Friday the 17th, an unlucky day in Italy?
let friday17thComponents = DateComponents(
  day: 17,
  weekday: 6)
let nextFriday17thDate = gregorianCalendar.nextDate(
  after: Date(),
  matching: friday17thComponents,
  matchingPolicy: .nextTime
)!
let nextFriday17thFormatted = dateFormatter.string(from: nextFriday17thDate)
print("• The next Friday the 17th will be on \(nextFriday17thFormatted).")

Enumerating Specific Dates

It’s tradition to conclude a “Date and Time Programming” article on this blog, and this one is no exception. The challenge: write a function that returns the dates for all the Friday the 13ths in a given year.

On most other platforms, the appropriate answer would be “No.” But with Swift, you can say “Sure — I can use

Calendar
’s
enumerateDates(startingAfter:matching:matchingPolicy:repeatedTimePolicy:direction:using:)
method!”

Once again, I’ll break with Swift convention and simply refer to this method as

enumerateDates()
.

I’ll show

enumerateDates()
in action by using it to implement
fridayThe13ths(inYear:)
, which returns an array of
Dates
that are Friday the 13th for the given year.

🛠️ Add this code to the playground and run it:

// This function returns the dates for all the Friday the 13ths in a given year.
func fridayThe13ths(inYear year: Int) -> [Date] {
  var result: [Date] = []
  let startingDateComponents = DateComponents(year: year)
  let startingDate = gregorianCalendar.date(from: startingDateComponents)!
  
  gregorianCalendar.enumerateDates(
    startingAfter: startingDate,
    matching: friday13thComponents,
    matchingPolicy: .nextTime
  ) { (date, strict, stop) in
    if let validDate = date {
      let dateComponents = gregorianCalendar.dateComponents(
        [.year],
        from: validDate
      )
      if dateComponents.year! > year {
        stop = true
      } else {
        result.append(validDate)
      }
    }
  }
  
  return result
}

print("\n👻 Here are the Friday the 13ths for 2024:")
for fridayThe13th in fridayThe13ths(inYear: 2024) {
  let fridayThe13thFormatted = dateFormatter.string(from: fridayThe13th)
  print("• \(fridayThe13thFormatted)")
}

enumerateDates()
steps through the calendar, starting with the
startingAfter:
date and looking for dates that meet both the
matching:
and
matchingPolicy:
criteria. It calls the trailing closure for any matching date.

The trailing closure extracts the year from

date
, the current matching date. If the extracted year matches the given year, the date is added to
result
, the list of Friday the 13ths for the given year. If the extracted year is greater than the given year, the
stop
value is set to
true
, which stops the enumeration.

When you run the code above, you’ll see that 2024 has two Friday the 13ths: in September and December.

Next Steps

💻 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-1.playground
.

The next part will take everything covered so far and make working with dates and times in Swift even better with syntactic magic.