2

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?

fpg1503
  • 7,492
  • 6
  • 29
  • 49
  • 2
    what would the successor be: always a second? or a day? A millisecond? You should only define successors to constructs that have a natural successor, as integer. 1 is followed by 2. i.e. for floats such a natural successor does not exist. Also for a point in time there is no natural successor, and as NSDate is representing such a point, it should not have a successor as-well. – vikingosegundo Jan 06 '16 at 13:03
  • As I've stated in my question even though I think this is a pretty valid discussion I'll assume it should always be one second. – fpg1503 Jan 06 '16 at 14:13
  • but why does it need to be the successor method. Just create a `advanceByOneSecond()` or better `advanceBy(_ time, inUnit:NSCalendarUnit)`. – vikingosegundo Jan 06 '16 at 14:16
  • Because I wanna make a `Range` and `Range`s' elements must conform to `ForwardIndexType` – fpg1503 Jan 06 '16 at 14:17
  • 1
    http://adampreble.net/blog/2014/09/iterating-over-range-of-dates-swift/ – vikingosegundo Jan 06 '16 at 14:18
  • 2
    Why do you want to make a `Range`? Where do you think it would be useful? – JeremyP Jan 06 '16 at 14:55
  • 1
    `ClosedInterval` (or `HalfOpenInterval`) might be an alternative, you only have to make NSDate Comparable. – Martin R Jan 06 '16 at 15:02
  • 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. – fpg1503 Jan 06 '16 at 15:40

2 Answers2

2

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.

Community
  • 1
  • 1
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
0

Try this, change Self to NSDate, remove ForwardIndexType protocol

    extension NSDate: _Incrementable {
    public func successor() -> Self {
        return self.dynamicType.init(timeInterval: 1, sinceDate: self)
    }
}

use in code like,

var date = NSDate()
date = date.successor()

let newDate = date.successor()

In ForwardIndexType protocol, there are methods like, advancedBy: and distanceTo:, you are implementing method successor, just implement protocol _Incrementable. this will work.

Hitendra Solanki
  • 4,871
  • 2
  • 22
  • 29
  • 1
    Then if doesn't conform to protocol `_Incrementable`: _Error: method 'successor()' in non-final class 'NSDate' must return `Self` to conform to protocol '_Incrementable'_ – dfrib Jan 06 '16 at 12:59
  • I have got the mac and xcode, tried, please check my edited answer – Hitendra Solanki Jan 06 '16 at 13:09
  • 1
    @HitendraHckr Please read the OP answer again. You provided a method that does not work for creating `Range`. Making it _Incrementable doesn't make it conform to ForwardIndexType so it won't answer his question – Leo Dabus Jan 06 '16 at 14:58
  • Your solution does not work, the compiler keeps suggesting I add `dynamicType` after the current existing `dynamicType`. – fpg1503 Jan 06 '16 at 15:39