Search code examples
swiftswift2nsdateclass-extensions

How to add conformance to _Incrementable in NSDate


I am trying to add conformance to ForwardIndexType in NSDates so I can make a Range<NSDate>, in order to do it I must implement public func successor() -> Self from _Incrementable.

My implementation is really simple and aims to state the date that succeeds another one is the one exactly one second after it, that's not what is being asked here.

extension NSDate: ForwardIndexType {
    public func successor() -> Self {
        return NSDate(timeInterval: 1, sinceDate: self) 
    }
}

The error I'm getting is

Cannot convert return expression of type 'NSDate' to return type 'Self'

I've tried adding as Self or as! Self but the compiler does not allow me since converting from NSDate to Self always succeeds in that case. Replacing Self by NSDate also does not do the trick.

How can I do it the right way?


Solution

  • As several comments have said, you should not do this even if it's possible. NSDate does not have a logical "next." Inventing one risks side effects. Extensions of public types are global. What happens if you say "the next date is the next second" and another extension says "the next date is the next day?" Both are equally reasonable (and equally incorrect). Never add extensions that are likely to collide with different meanings if others did them too.

    You said your goal is:

    I want to create a set of n random dates in a given interval. I wanted to shuffle a range and select the first n values

    That's no problem at all. First, as you say, you want "in a given interval." Excellent. That's a ClosedInterval<NSDate>. To get that, NSDate must be Comparable. There's nothing wrong with adding that extension. Anyone who implemented it reasonably would have to implement it this way.

    extension NSDate: Comparable {}
    
    public func <(lhs: NSDate, rhs: NSDate) -> Bool {
        return lhs.compare(rhs) == NSComparisonResult.OrderedAscending
    }
    

    Now you want to convert this to a range of integral seconds, not a range of dates. Then shuffle the elements in that range, pull off the first n values, and map those back to dates. We'll assume you already have Nate Cook's shuffle code.

    func randomDatesInInterval<DateInterval: IntervalType where DateInterval.Bound == NSDate>
        (interval: DateInterval, count: Int) -> [NSDate] {
        // For convenience we're going to assume that the range is no larger than 68 years.
        // If you need ranges larger than that, it's a bit more work and probably the subject
        // of a second question. (See https://stackoverflow.com/a/34388108/97337 for the basis.)
        let intervalSize = UInt32(interval.end.timeIntervalSinceDate(interval.start))
    
        let offsets = (0...intervalSize).shuffle()
    
        return Array(offsets.map { interval.start.dateByAddingTimeInterval(NSTimeInterval($0)) }.prefix(count))
    }
    

    And you can even use it with ... or ..<to define your intervals:

    // 100 second-interval dates from the last hour
    randomDatesInInterval(NSDate(timeIntervalSinceNow: -3600)...NSDate(), count: 100)
        .forEach { print($0) }
    

    Note that this algorithm is a bit slow and memory intensive if n is dramatically smaller than the number of seconds in the interval. We have to create what could be a pretty enormous array of numbers in order to do it the way you requested. If you don't care about duplicates, then it's all much simpler:

    let intervalSize = UInt32(interval.end.timeIntervalSinceDate(interval.start))
    
    return (1...count).map { _ in
        let offset = arc4random_uniform(intervalSize)
        return interval.start.dateByAddingTimeInterval(Double(offset))
    }
    

    If the interval is dramatically larger than n, then the chance of duplicates is low. If you still want to avoid duplicates without having to allocate that huge initial array, consider a Set:

    func randomDatesInInterval<DateInterval: IntervalType where DateInterval.Bound == NSDate>
        (interval: DateInterval, count: Int) -> [NSDate] {
            let intervalSize = UInt32(interval.end.timeIntervalSinceDate(interval.start))
    
            var offsets = Set<UInt32>()
            while offsets.count < count {
                offsets.insert(arc4random_uniform(intervalSize))
            }
    
            return offsets.sort().map { interval.start.dateByAddingTimeInterval(NSTimeInterval($0)) }
    }
    

    The trade-off of a Set is that this approach is very slow if n is of similar magnitude to the number of seconds in the interval. In that case, the shuffle is much more efficient.