Custom date iterators in Swift 3

In this post we will explore how to create own sequences in Swift 3. Our goal is to iterate over a range of Date objects with a for...in loop, and also apply some functions like map, flatMap or prefix to it.

First of all, we need to create a custom class which confirms Sequence protocol.

struct DateRange: Sequence {
  typealias Iterator = AnyIterator<Date>

  func makeIterator() -> Iterator {
    return AnyIterator {
      // Our iteration logic goes here
    }
  }
}

AnyIterator should return Date object as next value and nil if it is the end of sequence.

Next add properties to our date range object, which needed to calculate next date in range. So startDate is the first date of our range and endDate is the last. If there is no endDate value our sequence will be infinity. Using component and step values we can calculate next date in sequence using given calendar.

struct DateRange: Sequence {
  typealias Iterator = AnyIterator<Date>

  var calendar: Calendar
  var startDate: Date
  var endDate: Date?
  var component: Calendar.Component
  var step: Int

  func makeIterator() -> Iterator {
    return AnyIterator {
      // Our iteration logic goes here
    }
  }
}

Now implement makeIterator function:

struct DateRange: Sequence {

  typealias Iterator = AnyIterator<Date>

  var calendar: Calendar
  var startDate: Date
  var endDate: Date?
  var component: Calendar.Component
  var step: Int

  func makeIterator() -> Iterator {

    precondition(step != 0, "Step should not be zero!")

    var current = startDate
    return AnyIterator {
      guard let next = self.calendar.date(byAdding: self.component,
                                          value: self.step,
                                          to: current) else {
        return nil
      }

      let orderedType: ComparisonResult = self.step > 0 ?
                                            .orderedDescending :
                                            .orderedAscending
      if let last = self.endDate, next.compare(last) == orderedType {
        return nil
      }
      current = next
      return next
    }
  }
}

To generate date range with step of two months and limit to 12 months we can do like this:

for date in DateRange(calendar: Calendar.autoupdatingCurrent,
                      startDate: Date(),
                      endDate: nil,
                      component: .month,
                      step: 2).prefix(12) {
  // Use date
  print(date)
}

For better experience we can extend Calendar object:

extension Calendar {
  func dateRange(from: Date, to: Date? = nil, component: Calendar.Component, by step: Int = 1) -> DateRange {
    return DateRange(calendar: self, startDate: from, endDate: to, component: component, step: step)
  }
}

Iterate between today and future date objects with step of one day:

Calendar.autoupdatingCurrent
        .dateRange(from: today,
                   to: future,
                   component: .day).forEach { date in
  print(date)
}

Links


Опубликовано: Июнь 28, 2017 ~ Swift, Functional Programming