0

I'm looking for cool ways to stride through Date ranges with different increments (either seconds aka TimeInterval or with DateComponents aka .hour, .minute)

import Foundation

extension Date: Strideable {
    // typealias Stride = SignedInteger // doesn't work (probably because declared in extension
    public func advanced(by n: Int) -> Date {
        self.addingTimeInterval(TimeInterval(n))
    }

    public func distance(to other: Date) -> Int {
        return Int(self.distance(to: other))
    }
}
let now = Date()

let dayAfterNow = Date().addingTimeInterval(86400)
let dateRange = now ... dayAfterNow
let dateArr : [Date] = Array(stride(from: now, to: dayAfterNow, by: 60)) // Solves my task but  not exactly how I wanted.
let formatter: DateFormatter = {
    let df = DateFormatter()
    df.timeStyle = .short
    return df }()
print (dateArr.prefix(7).map { formatter.string(from: $0) })

/// This hoever doesn't work
// There must be a way to make it work but couldn't figure it out for now
let errDateArr: [Date] = Array(dateRange)
// Error: Initializer 'init(_:)' requires that 'Date.Stride' (aka 'Double') conform to 'SignedInteger'

The second part of the question is that also i'd like to have something like:

var components = DateComponents()
components.hour = 8
components.minute = 0
let date = Calendar.current.date(from: components)
let dateByComponentArr : [Date] = Array(stride(from: now, to: dayAfterNow, by: components))
Paul B
  • 3,989
  • 33
  • 46
  • 1
    Related: https://stackoverflow.com/questions/46857393/what-is-the-most-effective-way-to-iterate-over-a-date-range-in-swift/46857486#46857486 and https://stackoverflow.com/questions/35601583/get-weekdays-within-a-month/35603441#35603441 – vadian Jul 03 '20 at 15:56
  • 1
    Also: [How can I iterate day by day from an StartDate to an EndDate using strideable on Swift?](https://stackoverflow.com/questions/55444933/how-can-i-iterate-day-by-day-from-an-startdate-to-an-enddate-using-strideable-on). – Martin R Jul 03 '20 at 16:59
  • Yes, `Calendar`'s `enumerateDates()` is very helpful, @Martin. But there are cases when we need only an iterable sequence itself, lazy one perhaps. I wonder why `DateRange` from `NSCalendar` was discarded. – Paul B Jul 03 '20 at 18:22

3 Answers3

1

As already mentionerd by @Alexander you shouldn't try to conform Date to Strideable but you can implement your own methods to generate the next n days, hours or minutes:

extension Date {
    func year(using calendar: Calendar = .current) -> Int { calendar.component(.year, from: self) }
    func month(using calendar: Calendar = .current) -> Int { calendar.component(.month, from: self) }
    func day(using calendar: Calendar = .current) -> Int { calendar.component(.day, from: self) }
    func hour(using calendar: Calendar = .current) -> Int { calendar.component(.hour, from: self) }
    func minute(using calendar: Calendar = .current) -> Int { calendar.component(.minute, from: self) }
    func nextDays(n: Int, nth: Int = 1, using calendar: Calendar = .current) -> [Date] {
        let year  = self.year(using: calendar)
        let month = self.month(using: calendar)
        let day   = self.day(using: calendar)
        var days: [Date] = []
        for x in 0..<n where x.isMultiple(of: nth) {
            days.append(DateComponents(calendar: calendar, year: year, month: month, day: day + x, hour: 12).date!)

        }
        return days
    }
    func nextHours(n: Int, nth: Int = 1, using calendar: Calendar = .current) -> [Date] {
        let year  = self.year(using: calendar)
        let month = self.month(using: calendar)
        let day   = self.day(using: calendar)
        let hour   = self.hour(using: calendar)
        var hours: [Date] = []
        for x in 0..<n where x.isMultiple(of: nth) {
            hours.append(DateComponents(calendar: calendar, year: year, month: month, day: day, hour: hour + x).date!)

        }
        return hours
    }
    func nextMinutes(n: Int, nth: Int = 1, using calendar: Calendar = .current) -> [Date] {
        let year  = self.year(using: calendar)
        let month = self.month(using: calendar)
        let day   = self.day(using: calendar)
        let hour   = self.hour(using: calendar)
        let minute   = self.minute(using: calendar)
        var minutes: [Date] = []
        for x in 0..<n where x.isMultiple(of: nth) {
            minutes.append(DateComponents(calendar: calendar, year: year, month: month, day: day, hour: hour, minute: minute + x).date!)

        }
        return minutes
    }
}

extension Date {
    static let formatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .short
        formatter.timeStyle = .short
        return formatter
    }()
}

Playgournd testing:

let days = Date().nextDays(n: 10)
for day in days {
    print(Date.formatter.string(from: day))
}

let hours = Date().nextHours(n: 10)
for hour in hours {
    print(Date.formatter.string(from: hour))
}

let minutes = Date().nextMinutes(n: 10)
for minute in minutes {
    print(Date.formatter.string(from: minute))
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
0

There's a conflicting implementation of distance(to:) already declared on Date: Date.distance(to:). The implementation you defined has the same name, but a different type. This other overload could work, but unfortunately for you, Date already declares a typealias called Stride, which it sets to TimeInterval (just an alias for Double).

I think conforming to Date to Strideable is a bad idea. Take Double for example, which intentionally doesn't conform to Strideable: there's no clear universally-best stride definition. Should it go up by 0.1? 0.01? 3.14? It's not obvious. stride(from:to:by:) exists precisely for this reason, for you to be explicit by how much you're striding by.

let errDateArr: [Date] = Array(now ... dayAfterNow) would definitely score high "WTFs/m" points

Alexander
  • 59,041
  • 12
  • 98
  • 151
  • It is indeed a bad idea to generate default sequences (without an explicit step) between boundaries whose `Stride` type is other then `Int`. Yet `Double` does conform to `Stridable` otherwise it would be be impossible to create `stride` sequence with it. – Paul B Jul 03 '20 at 17:47
  • Ah, I had an incorrect understanding of Strideable's purpose. The ability to `...` doesn't just depend on conformance to `Strideable`, it requires the `Stride` be a `SignedInteger` – Alexander Jul 03 '20 at 22:53
0

For the second part of the question I created a simple sequence that takes daylight saving settings and other stuff in account. It might be helpful in some specific cases.

let dayAfterNow = Date().addingTimeInterval(86400)
var components = DateComponents()
components.hour = 8
components.minute = 0

func dateIterator(start: Date = Date(), by components: DateComponents, wrappingComponents: Bool = false) -> AnyIterator<Date> {
    var state = start
    return AnyIterator {
        
        let nextDate = Calendar.current.date(byAdding: components, to: state, wrappingComponents: wrappingComponents)
        state = nextDate ?? state
        return state
    }    
}


let dateCompSequence = AnySequence(dateIterator(by: components))
let dateArray = Array(dateCompSequence.prefix(10))
dateArray.map{print($0.description)}
print("starting for loop...")

for d in dateCompSequence.prefix(10) {
    print(d.description)
}

It is worth noting that Calendar.current.enumerateDates() will likely always help with similar tasks. But having a sequence is sometimes preferable. In earlier version of Swift there seemed to be a strideable DateRange type provided by NSCalendar (just the thing I wanted), but now there is nothing like it in standard library.

Paul B
  • 3,989
  • 33
  • 46
  • Why are you passing minute 0? You are not setting the component you are adding it. – Leo Dabus Jul 03 '20 at 18:25
  • This very error prone. If you pass day component equal to 1 and start now it will return 7/14/20, 3:32 PM 7/15/20, 3:32 PM 7/16/20, 3:32 PM 7/17/20, 3:32 PM 7/18/20, 3:32 PM 7/19/20, 3:32 PM 7/20/20, 3:32 PM 7/21/20, 3:32 PM 7/22/20, 3:32 PM 7/23/20, 3:32 PM – Leo Dabus Jul 03 '20 at 18:32
  • If the start date is "6/30/20, 11:45 AM" and you pass day component = 1 it will result in 6/11/20, 11:45 AM 6/12/20, 11:45 AM 6/13/20, 11:45 AM 6/14/20, 11:45 AM 6/15/20, 11:45 AM 6/16/20, 11:45 AM 6/17/20, 11:45 AM 6/18/20, 11:45 AM 6/19/20, 11:45 AM 6/20/20, 11:45 AM – Leo Dabus Jul 03 '20 at 18:35
  • Is it really what you expect as the result of your attempt? – Leo Dabus Jul 03 '20 at 18:36
  • 1
    I am not sure but looks like you are trying to achieve something like https://stackoverflow.com/a/62665165/2303865 – Leo Dabus Jul 03 '20 at 18:38
  • I can post a method that would return the next n days, hours or minutes with an option to return just the nth days, hours or minutes as well if you would like to – Leo Dabus Jul 03 '20 at 18:45
  • Any related suggestions are very welcome, @Leo. Dealing with Dates is always tricky and it is harder than it seems at first glance. I'll try to reproduce the error you' ve pointed to. Perhaps there are cases with times I haven't even imagine. I know there are cases with dates when seconds and even days can be missing. Usually speaking of range of dates we want the same time for some repeating day in calendar sense. – Paul B Jul 03 '20 at 19:02
  • The reason it printed out 10 day later is that the sequence was triggered twice: once for array creation and once in a for loop. Array values were not logged. Now looks OK to me, @Leo. – Paul B Jul 03 '20 at 20:26
  • 1
    but still have some issues. try `let start = DateComponents(calendar: .current, year: 2020, month: 6, day: 30, hour: 23, minute: 45).date!` `for d in dateIterator(start: start, by: .init(month: 1)).prefix(10) {` `print(Date.formatter.string(from: d))` `}` and check the dates returned after february – Leo Dabus Jul 03 '20 at 20:43
  • Yes, that's interesting. Nice catch. This could be handled with `state` but I'm afraid there can be other surprises. – Paul B Jul 03 '20 at 21:14