ClosedRange init with unordered bounds

Hello Swift Community.

During discussion of clamped function Add a `clamp` function to Algorithm.swift, it was found an interesting case with Ranges initialization.

func doSomething(val1: Double, val2: Double) {
  let offset = 10.clamped(to: val1...val2)
}

If val1 > val2, the app will crash with error "Can't form Range with upperBound < lowerBound"
To prevent this, we are forced to check that val1 < val2 every time, which is annoying.

let offset = 10.clamped(to: val1 <= val2 ? val1...val2 : val2...val1)

There also other situations, where Range bounds come from function arguments and because of that can't be checked at compile time:

func validatePaymentAmount(_ amount: Int, minAmount: Int, maxAmount: Int) -> ValidationResult {
  let validRange = ClosedRange(uncheckedBounds: (minAmount, maxAmount))
}

func didEndEditing(text: String) {
  let validator = SymbolsCountValidator(validCount: ClosedRange(uncheckedBounds: (lowerBound, upperBound))
}

let horizontalArea = ClosedRange(uncheckedBounds: (view1.frame.origin.x, view2.frame.origin.x)

Proposed solution

The idea is to add new initializer to Range, thanks to @benrimmington

extension ClosedRange {
  init(unorderedBounds: (leftBound: Bound, rightBound: Bound)) {
    let (leftBound, rightBound) = unorderedBounds
    
    let bounds: (lower: Bound, upper: Bound) = (leftBound <= rightBound ? (leftBound, rightBound) : (rightBound, leftBound))
    
    self.init(uncheckedBounds: bounds)
  }
}

Source compatibility

This change is purely additive and should not affect source compatibility.

Effect on ABI stability

No known effect.

Effect on API resilience

No known effect.

4 Likes

If the intention is that the bounds are unordered, what is the meaning of “left” and “right” here, and why do they need to be labeled?

1 Like

“left” and “right” are used here as an example and for readability, keeping in mind number axis. I don't propose for this variant. We can just as well use "firstBound" / "secondBound" names or even unlabeled tuple.

My own thoughts now is that labeled tuple is better than unlabeled.

  1. labeled variables are not visible form the call site, ClosedRange(unorderedBounds: (1, 3)).
    In that time, they are visible when reading method documentation and declaration.
  2. "firstBound" / "secondBound" seems to be more suitable than "leftBound" / "rightBound"

Who do others think?

I've just needed this in my project. Suggestion though, wouldn't it be more readable if simplified to something like:

public extension ClosedRange {
    
    init(unorderedLeft left: Bound, right: Bound) {
        if left <= right {
            self.init(uncheckedBounds: (left, right))
        } else {
            self.init(uncheckedBounds: (right, left))
        }
    }
}

I will try to make an implementation during this weekend to push this pitch further.

1 Like