# Support for all types of ranges

## Introduction

Swift currently has 2 objects we can use to express an interval between 2 values: `Range` and `ClosedRange`. The first one includes its `lowerBound` and excludes its `upperBounds`, the second one includes both. What if we want to create an object from 2 bounds we want excluded ?

Note: I know there are also `PartialRangeFrom`, `PartialRangeUpTo` and `PartialRangeThrough` which deal with ranges with only one bound. While some part of this pitch could also apply to them, I will mostly only discuss of 2 bounded ranges here. See the "About partial ranges" section for more dedicated infos.

## Motivation

Imagine we have to check if a variable is between 2 values. Let's say an integer between 0 and 5.
A trivial solution would be to simply have `x > 0 && x < 5`.
If we want to use ranges to express this, we will have a hard time because there is no support for a range excluding its lower bound. So we would end up creating a different range to manually exclude the 0 value from it.
Example using `Int`: `0+1..<5 ~= x`.
Example using `Double`: `0.0.nextUp..<5.0 ~= x`.
Example using `Comparable & Stridable` where `Stride` using an Integer: `lowerBound.advanded(by:1)..<upperBound ~= x`.

This workaround would work, but then we lose the essence of what we are really trying to do. We should be able to simply express the fact that the range we want to work with excludes its lower bound.

## Proposed solution

`Range` and `ClosedRange` are very close: they share a lot (and when a say a lot I mean quite all) of their functions. We could unify these objects by simply having 2 new properties in `Range`: `isLowerBoundIncluded` and `isUpperBoundIncluded`. `Range` could then be any kind of interval:

• including both bounds - the actual `ClosedRange` behavior
• excluding both - a new possibility
• including the lower bound and excluding the upper bound - the actual `Range` behavior
• excluding the lower bound and including the upper bound - a new possibility

For each one of the possibilities listed above, we would need to have a distinct operator to create those kind of ranges. As you all know, there already are 2 operators, one for each existing possibility:

• `...` is the operator to create a `ClosedRange`, i.e the lower and upper bounds are included.
• `..<` is the operator to create a `Range`, i.e an included lower bound and excluded upper bound.

If we look at the characters composing those operators, we could see a pattern here: 3 characters, one for each bound, and a `.` in the middle. If the bound is included the associated character is a `.`, or a `<` if the bound is excluded.
Following this pattern, we could create 2 new operators:

• `<.<` would create a `Range` where both bounds are excluded
• `<..` would create a `Range` where the lower bound is excluded and the upper bound is included.

Bringing back our initial problem, we can now directly express the condition using `0<.<5 ~= x`.

## Detailed design

A very trivial implemention of this new `Range` object and its operators could be:

``````struct Range<Bound> where Bound : Comparable {
let lowerBound: Bound
let isLowerBoundIncluded: Bool
let upperBound: Bound
let isUpperBoundIncluded: Bool

static func ...(lhs: Bound, rhs: Bound) -> Range<Bound> {
guard lhs <= rhs else { /* ? */ }
return Range(lowerBound: lhs, isLowerBoundIncluded: true, upperBound: rhs, isUpperBoundIncluded: true)
}

static func ..<(lhs: Bound, rhs: Bound) -> Range<Bound> {
guard lhs <= rhs else { /* ? */ }
return Range(lowerBound: lhs, isLowerBoundIncluded: true, upperBound: rhs, isUpperBoundIncluded: false)
}

static func <..(lhs: Bound, rhs: Bound) -> Range<Bound> {
guard lhs <= rhs else { /* ? */ }
return Range(lowerBound: lhs, isLowerBoundIncluded: false, upperBound: rhs, isUpperBoundIncluded: true)
}

static func <.<(lhs: Bound, rhs: Bound) -> Range<Bound> {
guard lhs <= rhs else { /* ? */ }
return Range(lowerBound: lhs, isLowerBoundIncluded: false, upperBound: rhs, isUpperBoundIncluded: false)
}
}
``````

## Source compatibility

Any usage of `ClosedRange` would have to be replaced by `Range`. Something like `typealias ClosedRange = Range` might suffice to avoid those renaming.
As almost every function in `Range` also exists in `ClosedRange` and they both seem to use the same supporting types ( though `Range` has `Range.indices`) so I don't think it would break anything.

TBD

TBD

## Alternatives considered

We could have a range object specific to any possibility:

• the actual `ClosedRange` where both bounds are included.
• a new `OpenedRange` where bith bounds are excluded.
• the actual `Range` where only the lower bound is included.
• a new `<?>` where only the upper bound is included.

With this approach, the behavior of the actual `Range` is tricky to understand because its name doesn't describe its behavior as clearly as `ClosedRange` does. It would need to be renamed to better fit in the new schema.

This solution has 2 major inconvenients:

• Having 4 different objects we can use to work with ranges would be a lot more complex than having just one `Range` handling all cases.
• Those 4 objects would look very similar, and as coders we don't like it.

Like I said, this pitch is mostly about 2 bounded ranges but we could also apply it to partial ranges.
`PartialRangeUpTo` and `PartialRangeThrough` could be unified together in a `PartialRangeTo` object having an `isBoundIncluded` property.
`PartialRangeFrom` could also benefit from an `isBoundIncluded` property to add support for all values superior to the bound, excluding the bound.

3 Likes

We can't change `Range` like this because of ABI stability.

We have, however, previously discussed adding something like a `PredicateSet` (as well as other kinds of Set). Perhaps something like that would be a better way to solve your problem?

What a great pitch! This is exactly how I have always expected ranges to work.

Me too. I know it's been discussed before but I still think all types of ranges should be supported.

Another thing I often reached for is a way to write ranges that end say 5 spots from the end, without having to first find out what the end is. Not sure of the implications of using dropLast for this, it might work.