The NSDateInterval Object

Somehow, this shiny new object, which was actually introduced in iOS 10, flew past my radar. Today I want to take a few minutes to talk about the NSDateInterval object. This object allows us to quickly calculate the time interval (represented as a NSTimeInterval) between dates, it allows us to check if two dates overlap, and it allows us to check if a given date is within a certain interval.

The NSDateInterval Class

This small object is made of a handful of property and functions.

To create a NSDateInterval, you can provide either a closed range with the start and end date, or you can provide the start date with a duration. In the example below we will use two dates, seven days apart.

let now = Date()

let components = DateComponents(day: 7)
if let sevenDaysAhead = Calendar.current.date(byAdding: components, to: now) {
    let interval = DateInterval(start: now, end: sevenDaysAhead)
}

Our internal variable now has a couple of handful properties. You can get the startDate and endDate, but perhaps most interesting the duration, which contains the number of seconds between both dates.

Having the duration can be useful on its own, but what’s even more useful is that we can now compare and check if said date intervals intersect, or if a date is part of a certain date interval.

Comparing Date Intervals

To compare two DateIntervals, simply call compare(_) on one of them and pass it the second interval. This operation will return a ComparisonResult which will let you know which one is “bigger” than the other.

let now = Date()

let components = DateComponents(day: 7)
if let sevenDaysAhead = Calendar.current.date(byAdding: components, to: now) {
    let interval = DateInterval(start: now, end: sevenDaysAhead)
    
    // Let's create a differemt but similar interval, but eight months ago
    let eightMonthsAgoComponents = DateComponents(month: -8)
    if let eightMonthsAgo = Calendar.current.date(byAdding: eightMonthsAgoComponents, to: now),
       let eightMonthsAgoPlus7Days = Calendar.current.date(byAdding: DateComponents(day: 7), to: eightMonthsAgo){
        let interval8MonthsAgo = DateInterval(start: eightMonthsAgo, end: eightMonthsAgoPlus7Days)
        
        let comparison = interval.compare(interval8MonthsAgo)
        switch comparison {
        case .orderedAscending: print("orderedAscending")
        case .orderedDescending: print("orderedDescending")
        case .orderedSame: print("orderedSame")
        }
    }
}

This is a bit of a mouthful, but essentially what it does is:

  1. Create an interval between the current day and the date seven days in the future.
  2. Create a date 8 months in the past, create a date 7 days after that day in the past, and create an interval with the two.
  3. Compare them.

The result will be .orderedDescending, since the left side of the operation (interval) is more recent than interval8MonthsAgo. When comparing, the framework takes into account the startDate and the duration if necessary. The documentation on comparing intervals has more info.

Equality of DateIntervals

We can check if two intervals are equal by using the == operator (the documentation incorrectly states that there is a isEqual function, but I wasn’t able to access it.

Two DateIntervals are considered equal when their startDate and duration properties are the same.

let equalIntervals = interval == interval8MonthsAgo // false

The take away from the last two sections is that equality and comparison takes into account the startDate and duration properties in order to do their calculations. If you need to see if two date ranges are the same duration, but they don’t overlap or have any relation with each other, use Calendar's Calendar.current.dateComponents((_from:to:) method instead.

let difference = Calendar.current.dateComponents([.day], from: now, to: eightMonthsAgo) // -242 days ago (feel free to use abs() for the absolute value

Checking Interval Intersections

You can check if two DateIntervals intersect by calling the intersects(_) method in one of them.

let now = Date()

if let sevenDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 7), to: now),
   let sixDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 6), to: now),
   let eightDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 8), to: now),
   let ninetDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 9), to: eightDaysAhead){
    let sevenDayInterval = DateInterval(start: now, end: sevenDaysAhead)
    let sixDayInterval = DateInterval(start: now, end: sixDaysAhead)
    let eightDaysInterval = DateInterval(start: now, end: eightDaysAhead)
    let sevenAndSixIntersect = sevenDayInterval.intersects(sixDayInterval) // true
    let sevenAndEightIntersect = sevenDayInterval.intersects(eightDaysInterval) // true
    
    let farAwayInterval = DateInterval(start: eightDaysAhead, end: ninetDaysAhead)
    let sevenDaysIntervalIntersectsFarAwayInterval = sevenDayInterval.intersects(farAwayInterval) // false
}

And, you can also find the interval at which two date intervals intersect, by calling intersection(with:) on either one.

import Foundation

let now = Date()

if let sevenDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 7), to: now),
   let sixDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 6), to: now),
   let eightDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 8), to: now),
   let ninetDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 9), to: eightDaysAhead){
    let sevenDayInterval = DateInterval(start: now, end: sevenDaysAhead)
    let eightDaysInterval = DateInterval(start: now, end: eightDaysAhead)
    
    if let intersectionInterval = sevenDayInterval.intersection(with: eightDaysInterval) {
        print("The intervals intersect starting on \(intersectionInterval.start) and ending on \(intersectionInterval.end)")
    }
}

Checking if a date exists within an interval.

Finally, the last useful thing we can do is to check if a single date fits in an interval. For this, simply call contains(_).

let now = Date()

if let sevenDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 7), to: now),
   let sixDaysAhead = Calendar.current.date(byAdding: DateComponents(day: 6), to: now),
   let sevenDaysAndASecondAhead = Calendar.current.date(byAdding: DateComponents(day: 7, second: 1), to: now) {
    
    let intervalSevenDaysAhead = DateInterval(start: now, end: sevenDaysAhead)
    
    let containsSixDaysAhead = intervalSevenDaysAhead.contains(sixDaysAhead)
    let containsSevenDaysAndASecondAhead = intervalSevenDaysAhead.contains(sevenDaysAndASecondAhead)
}

Conclusion

If there is one thing Apple likes to do, is to make dealing with dates a simple affair for us. All their date-related APIs are packed with functionality to make calculation and manipulation of time-related data really easily. 5 years ago, Apple added NSDateInterval to their collection of date tools, and it’s amazing how simple it is to work date intervals with it.