[Proposal] Make Foundation.Date conform to Strideable

I think Foundation.Date should conform to Strideable.
I think there is an unambiguous, preferable, implementation of Strideable for Date.
I have written out this implementation Here.

2 Likes

Since date and time is notoriously tricky to manipulate, what are the implications of such difficulties for Strideable conformance?

The conformance I implemented - by design - only strides by (partial) seconds.
Any misbehavior can only happen in acquiring the desired number of seconds to stride by, which people do anyway.

1 Like

I think it would be more useful to provide date iterators with an API surface that provides calendar accurate iteration for a variety time units. I’ve written them myself before but it seems like a logical Foundation extension in Swift to, say, iterate all Mondays between dates.

2 Likes

While that would be nice, from what I know about time and date manipulations, it'd be very, very hard to do correctly in the general case and require significant domain-specific expertise, would it not?

Not really, considering Foundation already provides the abstractions and iteration logic. The main pain point is trying to map such broad functionality onto an Iterator. My implementations have always used the Gregorian calendar, for example.

Yeah you could totally do that. Foundation's Calendar has APIs for scanning dates which could easily encapsulated in an Iterator:

https://developer.apple.com/documentation/foundation/calendar/2293473-nextdate

This is an interesting request, but IMO striding through dates like you’re suggesting isn’t a common enough operation that would merit it inclusion in to the standard library.

This would make sense for a Date/Time-specific library though.

As a broader thought, striding through dates is calendrically difficult. The simple illustration for this is: “Jan 31 + 1 month + 1 month = Mar 28”, but “Jan 31 + 2 months = Mar 31”.

You could write an iterator to handle this sort of thing, but at that point you forego the nextDate API on Calendar...

This sort of functionality falls under the broader topic of “recurrence rules”, which would be an ideal topic for a non-standard library.

I am not suggesting adding it to the stdlib, but to Foundation

The suggested addition only strides seconds, not getting into the complexities of calendars.

Do we have examples of what adding this functionality would enable in practice?

Not currently, by me or anyone else.
I just like conforming types to protocols, when the conformance isn't ambiguous.

Hmm, I think that's problematic. I had assumed you had a concrete use case in mind. Conformances in Swift should always enable useful code to be written, not least because it tests the implementation in a real-world scenario.

Without that, I think we should hold off on adding this particular conformance, as it may block alternative ways of conforming Date to Strideable that would enable a greater number of useful things to be done with it.

4 Likes

You don't want to use strides with TimeInterval, but date.stride(by: 1.weeks, through: date + 52.weeks) has a certain appeal.

4 Likes

The types on that expression need to be very carefully thought about, because '52 weeks' is not a TimeInterval and I don't think we have a type that can describe it yet.

1 Like

It's also insufficient to write date.stride(by: 1.weeks, through: date + 52.weeks) as the meaning (and result) of the operation depends on the calendar the date is being interpreted in.

1 Like

Right. You can't really make the API for advancing dates any simpler than Calendar, or that's what they would have done in the first place. This isn't the first version of the Calendar API - its been revised a couple of times over the years. That said, you can wrap those APIs to give you a more convenient interface for your needs - that's why I recommended encapsulating the Foundation API in an iterator.

So I had a little go at a quick version of an iterator:

import Foundation

extension Calendar {

  func makeIterator(from date: Date, until: Date?, every components: DateComponents) -> Calendar.DateComponentsIterator {
    return DateComponentsIterator(date: date, calendar: self, cutoff: until, components: components)
  }

  struct DateComponentsIterator: IteratorProtocol {
    var date: Date?

    let calendar: Calendar
    let cutoff: Date?
    let components: DateComponents

    mutating func next() -> Date? {
      guard let nextDate = self.date else { return nil } // Ended.

      self.date = calendar.date(byAdding: components, to: nextDate)
      if let advancedDate = self.date, let cutoff = self.cutoff, advancedDate > cutoff { self.date = nil }
      return nextDate
    }
  }
}

And it seems to work well enough:

var components = NSDateComponents(); components.day = 1
var iter = Calendar.current.makeIterator(from: Date(), until: Date().addingTimeInterval(10_000_000),
                                         every: components as DateComponents)

IteratorSequence(iter).forEach { print($0) }

// outputs this:

2018-01-22 20:36:42 +0000
2018-01-23 20:36:42 +0000
2018-01-24 20:36:42 +0000
2018-01-25 20:36:42 +0000
2018-01-26 20:36:42 +0000
2018-01-27 20:36:42 +0000
2018-01-28 20:36:42 +0000
2018-01-29 20:36:42 +0000
2018-01-30 20:36:42 +0000
... etc

nextDate(after:matching:matchingPolicy:repeatedTimePolicy:direction:) attempts to find the next date which matches the components you're setting, not by adding them — what you're asking for is "give me the next date whose minute is 01", which indeed happens once an hour. Asking for the list of dates whose hours are 03 will similarly iterate once a day, etc.

What your iterator should use is date(byAdding:to:wrappingComponents:) or date(byAdding:value:to:wrappingComponents:), which allows you to add 1 minute at a time, 1 hour at a time, etc.

1 Like

:man_facepalming: Thanks, fixed!

1 Like

I'm going to defer to @davedelong on this, but it might be interesting to have a date instance that can be set with reference to a calendar, so that doing date offsetting its components by +1 week would always be with respect to that calendar used at initialization. I can think of many many cases where a date would stick to a single calendar for the lifetime of the instance and its persistance/rehydration. Can you think of non-esoteric use-cases for when a date would move between calendars?