[swift-evolution] Feature proposal: Range operator with step

Brent Royal-Gordon brent at architechies.com
Thu Apr 7 07:01:47 CDT 2016


>> 	calendar.using(.Day).stride(from: startDate, to: endDate, by: 1)
> 
>> The `start` and `end` parameters could be grouped together into a
>> single parameter to match `stride(over:by:)`, but you can't put the
>> calendar or the unit into the stride—without them, there is no
>> coherent way to calculate the distance between two dates.
>> 
>> So if some types need a strider, and will need to have the method
>> structured as `strider.stride(something:by:)`, it seems like the free
>> function version for types which *don't* need a strider ought to be
>> `stride(something:by:)`. The `something.striding(by:)` design can't be
>> easily adapted to this situation.
> 
>  calendar[startDate..<endDate].striding(by: .Day)
> 
> ?

Actually, it would need to be something like `calendar[startDate..<endDate, unit: .Day].striding(by: 1)`, because NSCalendarUnit is not itself a stride, it is the *unit* of the stride. But even that doesn't quite help.

Here's the issue.

Take a look at today's Strideable. Stripped of comments and irrelevant annotations, you have:

	protocol Strideable : Comparable {
	    associatedtype Stride : SignedNumberType
	    func distanceTo(other: Self) -> Stride
	    func advancedBy(n: Stride) -> Self
	}

The problem is that there's no good way to get the calendar and unit into these methods. If you package them inside `Stride`, then `distanceTo` has no idea what calendar or unit it's supposed to return. If you package them inside `Self`, then `distanceTo` has two calendars and two units, and they might not agree (and the type system can't catch this issue). 

To fix this, you need to have a single instance which performs the calculation. For simple Strideable types, this instance would not do very much, but for dates, it would factor in the calendar and unit.

For example, suppose you modify `Strideable` so that, instead of applying to the values participating in the striding, it applies to the instance containing those values:

	public protocol Strideable {
		associatedtype Element: Comparable
		associatedtype Stride: SignedNumber
		
		var start: Value
		var end: Value
		
		public func distance(from earlier: Element, to later: Element) -> Stride
		public func advance(element: Element, by stride: Stride) -> Element
	}

Now, with some *really* aggressive conditional conformance work, you could do this:

	extension Range: Strideable where Element == Int {
		typealias Stride = Int
		
		public func distance(from earlier: Int, to later: Int) -> Int {
			return later - earlier
		}
		public func advance(element: Int, by stride: Int) {
			return value + stride
		}
	}
	extension Range: Strideable where Element == Double {
		// Ignoring the accumulation issue.
		typealias Stride = Double
		
		public func distance(from earlier: Int, to later: Int) -> Int {
			return later - earlier
		}
		public func advance(element: Double, by stride: Double) {
			return element + stride
		}
	}
	// etc., for each type you care about.
	// 
	// This could be automated by creating a `RangeStrideable` protocol similar to the old Strideable,
	// and having types with this automatic behavior conform to it. You could then have a single:
	// 	extension Range: Strideable where Element: RangeStrideable

It would not be possible to stride directly over a range of NSDates, but you could have a "calendar range" which could be `Strideable`, like so:

	struct NSCalendarRange {
		var start: NSDate
		var end: NSDate
		
		var calendar: NSCalendar
		var unit: NSCalendarUnit

		var options: NSCalendarOptions
		// This mainly contains rounding options. Interestingly, NSDate is like Double in that 
		// it accumulates errors through repeated addition. To fix that, I have discovered 
		// a truly marvelous design which this email is too small to contain.
	}
	extension NSCalendarRange: Strideable {
		typealias Value = NSDate
		typealias Stride = Int
		
		public func distance(from earlier: NSDate, to later: NSDate) -> Int {
			let components = calendar.components(unit, from: earlier, to: later, options: options)
			return components.value(forComponent: unit)
		}
		public func advance(value: NSDate, by stride: Int) -> NSDate {
			return calendar.date(byAdding: unit, value: distance, to: value, options: options)!
		}
	}

So I guess `striding(by:)` can be adapted to date arithmetic, but only if we adjust our conception of what a stride is striding over.

-- 
Brent Royal-Gordon
Architechies



More information about the swift-evolution mailing list