Fun With Date Calculations

Date calculations are a common trap for the unwary. How do you work out the start of the day today, tomorrow or in five days or five months time? What is the correct answer when you add a month to January 31? If you are doing date and time calculations by adding the number of seconds in an hour or day you are probably doing it wrong.

Date Calculations

First the problem I wanted to solve. I needed to figure out a date to fire a notification. The date needed to be a variable number of days from today. I did not care what time the notification fired as long as it was on the day so I went for the start of the day.

Spoiler alert: It turns out that since iOS 8 the solution is trivial as long as you are familiar with the basics of the Date, DateComponents and Calendar classes.

Date, DateComponents and Calendar - A Recap

If you are working with Objective-C that would be NSDate, NSDateComponents and NSCalendar.

Creating Dates

The most basic way to get a date in iOS is with the Date class:

let now = Date()
// Nov 20, 2016, 6:55 PM

let later = Date(timeIntervalSinceNow: 300)
// Nov 20, 2016, 7:00 PM

 let evenLater = Date(timeInterval: 300, since: later)
// Nov 20, 2016, 7:05 PM

A Date is a fixed point in time. It is not dependent on the user’s locale, calendar or time zone. If you want to do date and time calculations resist the temptation to add a fixed constant to a date. For example, there is no guarantee that a day will contain 86,400 seconds.

Date Components

The other way to create a date is with a DateComponents structure. It has properties for the components that make up a date and time.

var jan10 = DateComponents()
jan10.hour = 9
jan10.minute = 30
jan10.day = 10
jan10.month = 1
jan10.year = 2017
jan10.timeZone = TimeZone(abbreviation: "GMT")

You do not need to set all of the components which is useful when you are comparing or searching for dates. Also note there is nothing to stop you setting values that do not have a valid date meaning:

// 31-feb ??
dateComponents.day = 31
dateComponents.month = 2

Calendar

Neither a Date or DateComponents object know anything about a user’s calendar. To get the current calendar for a user:

let calendar = Calendar.current

If you are holding on to the calendar for any length of time use the auto-updating version which updates if the user changes locale:

let calendar = Calendar.autoupdatingCurrent

The Calendar class has methods for common date calculations, comparisons and tests. It knows how to convert between date components and Date objects taking into account the oddities of different calendar systems:

To convert between date components and dates you need a calendar:

let jan10 = calendar.date(from: dateComponents)
// Jan 10, 2017, 9:30 AM

These conversions return an optional Date which will be nil if a valid date can not be found.

To set a specific time for an existing date there is a convenient method:

let today = Date()
let nineMorning = calendar.date(bySettingHour: 9,
                                minute: 0,
                                second: 0,
                                of: today)

To get one or more date components from a date:

let weekOfYear = calendar.component(.weekOfYear, from: jan10!)
let monthAndYear = calendar.dateComponents([.month,.year], from: jan10!)

Adding Dates

The safest way to do date arithmetic is with the user’s calendar. Some examples:

let today = Date()

// Same time tomorrow
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today)

// Same time one week later
let oneWeekLater = calendar.date(byAdding: .day, value: 7, to: today)

The date(byAdding:value:to:) method returns an optional date which will be nil if the date does not exist. It tries to do “the right thing” so for example if you add a month to the last day in January you get the last day in February (allowing for leap years if they exist in the calendar):

var jan31Components = DateComponents()
jan31Components.day = 31
jan31Components.month = 1
jan31Components.year = 2016   // A leap year

if let jan31 = calendar.date(from: jan31Components) {
  let nextMonth = calendar.date(byAdding: .month,
                                value: 1,
                                to: jan31,
                                wrappingComponents: false)
  // Feb 29, 2016, 12:00 AM
}

Finding Next Dates

For those situations when you want to find the next occurrence of a date, use a DateComponents object to specify what you are looking for. For example to find the next monday morning at 09:00 in the user’s calendar:

let today = Date()
var nextMonday = DateComponents()
nextMonday.weekday = 2
nextMonday.hour = 9
let result = calendar.nextDate(after: today,
                               matching: nextMonday,
                               matchingPolicy: .nextTime)

Useful tests

Some useful tests all introduced with iOS 8:

let today = Date()                // Nov 20,2016, 7:00 PM
calendar.isDateInToday(today)     // true
calendar.isDateInWeekend(today)   // true
calendar.isDateInTomorrow(today)  // false
calendar.isDateInYesterday(today) // false

Getting the Start of Day

And since iOS 8 the method that makes my problem simple:

let today = Date()
// Nov 20, 2016, 7:00 PM

let startOfToday = calendar.startOfDay(for: today)
// Nov 20, 2016, 12:00 AM

Putting it all together

Armed with all this knowledge we can come back to my original question. How can I work out the start of a day a given number of days from today? We have the startOfDay(for: Date) method since iOS 8 so we can break the problem down into two steps:

  1. Work out the date for the given number of days from now.
  2. Get the start of day for the calculated date.

We can even generalize step 1 to allow us to add any calendar component. For convenience I will make this an extension on Calendar.

extension Calendar {
  func startOfDay(byAdding component: Calendar.Component,
                value: Int,
                to date: Date,
                wrappingComponents: Bool = false) -> Date? {
    guard let newDate = self.date(byAdding: component,
                                  value: value,
                                  to: date,
              wrappingComponents: wrappingComponents) else {
        return nil
    }
    return self.startOfDay(for: newDate)
  }
}

Example usage, to get the start of the day that is 1 month from today:

let calendar = Calendar.current
let startInMonth = calendar.startOfDay(byAdding: .month,
                                       value: 1,
                                       to: Date())

Or to get the start of the day that is in five days time:

let startInDays = calendar.startOfDay(byAdding: .day,
                                      value: 5,
                                      to: Date())

We can add a second convenience method for the situation of wanting a number of days from today that calls the first method:

func startOfDay(in days: Int) -> Date? {
  return self.startOfDay(byAdding: .day,
                         value: days,
                         to: Date())
}

Then to get the start of the day that is five days from today:

let startInFive = calendar.startOfDay(in: 5)

Further Reading

I highly recommend watching the WWDC 2013 session on date and time calculations. This was marked as a macOS session at the time but I think the API’s are now fully available in iOS 10 (and most since iOS 8):