~= vs Range.contains(_:)

Swift | April 14, 2016

Today I was working on simple validators that we use for forms (backed by awesome Eureka) and had to implement validator that validates string length. So I did it like this:

struct StringValidator: Validator {
    typealias ValueType = String
    
    let stringRange: Range<Int>
    init(stringRange: Range<Int> = 1..<Int.max) {
        self.stringRange = stringRange
    }
    
    func validate(value: String?) -> Bool {
        guard let value = value?.stringByTrimmingCharactersInSet(.whitespaceAndNewlineCharacterSet()) else {
            return true
        }
        return stringRange.contains(value.characters.count)
    }
}

The idea is simple. Form field can have a validator with default range (1..<Int.max) that will validate any not empty string, but it can also setup validator with specific range that will define minimum and maximum string length. Using isEmpty on string is not an option because it makes a special case and for that I will need to define a separate validator like NonEmptyStringValidator what looks unnecessary.

Then I wrote some tests. And noticed that when I pass an empty string as a value and expect that it will fail validation test never completes. First I thought that there is some issues when I combine several validators together. But the reason is much simpler. Range is a SequenceType. And SequenceType provides default implementation for contains(_:) method that simply iterates through all sequence members. Probably Range does not override it so it is iterated from 1 to Int.max and each index is compared with 0. For me it looks strange because I don't see any problem with providing specific implementation of that method that will only check bounds. It will not break the contract of SequenceType. It does not look like Range can contain indexes in random order or can be discontinuous. But for whatever reason we don't have it in stdlib.

I definitely didn't want to compare range startIndex and endIndex manually. So my first attempt to fix this was moving to NSRange:

return NSLocationInRange(value.characters.count, NSRange(stringRange))

It works and only checks for range bounds. But that does not look nice either.

After some time I found much better solution (I think it dawned on me at the moment when I switched to Safari tab with "Match me if you can" article):

return stringRange ~= value.characters.count

Works perfectly and looks much better than any other solution. Though I had to put a comment describing what it does because ~= is so rarely used by itself.

Also I found out that there are HalfOpenInterval and ClosedInterval that are returned from ... or ..< operators for Comparable generic argument. But for ForwardIndexType (which Int is) these operators return Range. Intervals are not collections or sequences and don't have aforementioned issue.


Profile picture

Ilya Puchka
iOS developer at Wise
Twitter | Github

Previous:

March 29, 2016

I definitely agree with those who say that you should not depend on code from external source (meaning where and how it is hosted). You should check in any code…

Next:

April 28, 2016

There were few times already when I used this little-known feature of Swift in real code and it improved (in my opinion) readability a lot and much better…