Iterating Over a Range of Dates in Swift

September 7, 2014

Updated 2016/8/25: Made a few style tweaks and fixes for Swift 2.2; fixed start date issue (thanks, gist commenters!).

One thing I’ve been wanting to do with Swift is iterate over a range of NSDate objects in a for loop. Something like this:

let startDate = ...
let endDate = ...
for date in startDate...endDate {
    ...
}

While I think it might be possible to do this by making NSDate conform to ForwardIndexType, it would be fairly inflexible. As I understand date arithmetic, to do it right you need a reference to the NSCalendar being used, and of course you need to know how much to ‘step’ the date each time. You could just make it step by days but what if you later want to step by hours?

So I decided on a different approach: create a struct, DateRange, that conforms to SequenceType. It’s not nearly as succinct, but it is much more flexible. Create an instance of the struct using an extension on NSCalendar, as this seems to be in keeping with calendar-dependent date APIs. It looks like this:

let calendar = NSCalendar.currentCalendar()
let startDate = ...
let endDate = ...
let dateRange = calendar.dateRange(startDate: startDate,
                                     endDate: endDate,
                                   stepUnits: .Day,
                                   stepValue: 1)

for date in dateRange {
    print("It's \(date)!")
}

The complete code is below (also in a gist), but first a bit of a disclaimer: this code works, but I half expect to look back on it in a year, cringe, and contemplate deleting this post. My crystal ball of Swift faux pas is cloudy.

Note also that at the time this was written, NSDate did not have any Swift comparison operators built in, so I implemented >. Presumably that will change.

import Foundation

func > (left: NSDate, right: NSDate) -> Bool {
    return left.compare(right) == .OrderedDescending
}

extension NSCalendar {
    func dateRange(startDate startDate: NSDate, endDate: NSDate, stepUnits: NSCalendarUnit, stepValue: Int) -> DateRange {
        let dateRange = DateRange(calendar: self, startDate: startDate, endDate: endDate,
                                  stepUnits: stepUnits, stepValue: stepValue, multiplier: 0)
        return dateRange
    }
}

struct DateRange :SequenceType {
    
    var calendar: NSCalendar
    var startDate: NSDate
    var endDate: NSDate
    var stepUnits: NSCalendarUnit
    var stepValue: Int
    private var multiplier: Int
    
    func generate() -> Generator {
        return Generator(range: self)
    }
    
    struct Generator: GeneratorType {
        
        var range: DateRange
        
        mutating func next() -> NSDate? {
            guard let nextDate = range.calendar.dateByAddingUnit(range.stepUnits,
                                                          value: range.stepValue * range.multiplier,
                                                         toDate: range.startDate,
                                                        options: []) else {
                return nil
            }
            if nextDate > range.endDate {
                return nil
            }
            else {
                range.multiplier += 1
                return nextDate
            }
        }
    }
}

// Usage:
func testDateRange() {
    let calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
    let startDate = NSDate(timeIntervalSinceNow: 0)
    let endDate = NSDate(timeIntervalSinceNow: 24*60*60*7-1)
    let dateRange = calendar.dateRange(startDate: startDate,
                                         endDate: endDate,
                                       stepUnits: .Day,
                                       stepValue: 1)
    let datesInRange = Array(dateRange)
    XCTAssertEqual(datesInRange.count, 7, "Expected 7 days")
    XCTAssertEqual(datesInRange.first, startDate, "First date should have been the start date.")
}
Tags: