Comparing numbers sequentially

For a few weeks now, the Swift community has started to think of a cool new way of testing if a number is contained in a certain range. Previously, this was done using a normal if (since that’s how it’s done in most languages):

if 0 < x && x < 10 {
...
}

But Swift’s flexibility allows us to choose much more readable approaches! Here’s the definition of a new operator for the CountableClosedRange type:

infix operator =~
func =~(range: CountableClosedRange<Int>, x: Int) -> Bool {
return range.lowerBound <= x && range.upperBound >= x
}

This operator can make that if statement much more readable:

if 0...10 =~ x {
...
}

Better, right?

I’m not sure. The added flexibility is really useful — for instance, we might want to test if x is inside some generic Range:

if indices =~ x {
...
}

Personally, I’d switch the order:

if x =~ 0...10 {
...
}

But somehow this still doesn’t feel right. Let’s go back to the original problem: what would an ideal solution be?

To me, it would be this:

if 0 < x < 10 {
...
}

That’s what we write when we’re doing math and it’s what we understand more easily for numeric applications. Only there isn’t really a way to get this to compile in Swift, for a simple reason: the comparison on the right must return a number, while the one on the left must return a boolean.

To better understand this, we can think about it like we do with normal functions. Imagine this operator is actually a function called lessThan:

func lessThan(_ a: Int, _ b: Int) -> Bool {
return a < b
}
lessThan(4, 5) // true

When we chain it, it doesn’t really work:

lessThan(0, lessThan(x, 10))
// ^ error: Cannot convert Bool to Int

That’s because the function expects an Int but it returns a Bool. So, to solve this, the function on the right needs to return an Int, right? But which Int?

Simple! If x is less than 10, we just return x so that the test on the left will work normally. Otherwise, we need to force the whole test to fail — which means forcing the test (the one that will give the final answer) on the left to fail. We do that by returning the smallest number we possibly can, such that it will always be less than whatever we choose to compare it to.

func lessThan(_ a: Int, _ b: Int) -> Int {
if a < b { return a }
else { return Int.min }
}
lessThan(0, lessThan(5, 10)) // true!

Great! This means that to solve our operator problem, we just need to use a different operator in the right:

// ≤ is "option + <" in macOS
infix operator ≤: AdditionPrecedence
func ≤(a: Int, b: Int) -> Int {
if a < b { return a }
else { return -99999 }
}
0 < 5 ≤ 10 // true!

It works*!

You can change the operator from ≤ to some other symbol of your choosing, like <<, if you prefer not using unicode (or want to avoid the ambiguity with the ≤ and <= symbols). I use it because it looks good :)

The result isn’t as flexible as the indices approach, but I find it much more readable. Also, it allows for arbitrarily long chains, which is really useful for more complex bits of math:

0 < x ≤ y ≤ f(z) ≤ 10

The final implementation, including the greater-then operator and all “or -equal-to” versions, can be found in this gist:

* AdditionPrecedence was used here for conciseness, but it would be ideal to define a new precedence group with a higher priority than the Comparison group, as in the gist above.

Edit (Apr 12th, 2018): fixed the last code block, which used << instead of . No changes were needed to update to Swift 4.1.

Edit (Jan 31st, 2017): removed broken links and updated code for Swift 3.

Like what you read? Give Vinicius Vendramini a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.