4

I need to validate the length of a string. The allowed values for the character count are:

  • 6 – 9 characters
  • 12 characters
  • 15 characters

All strings with a different character count are invalid. Thus, I would like to create a Swift function that accepts a number of ranges and evaluates the string:

extension String {

    func evaluateLength(validCharacterCounts: Range<Int>...) -> Bool {
        // Implementation
    }

}

Now I can call the function for a single Int range:

"Live long and prosper".evaluateLength(validCharacterCounts: 6..<10)

and multiple Int ranges:

"Live long and prosper".evaluateLength(validCharacterCounts: 6..<10, 15..<20)

But I cannot call the function with single, isolated integer values:

"Live long and prosper".evaluateLength(validCharacterCounts: 6..<10, 12, 15)

because 12 and 15 are typed as Int and not as Range<Int>.

Swift compile error: Cannot convert value of type 'Int' to expected argument type 'Range'

Is there a way to treat a single Integer as a Range in Swift, like casting it automatically to Range<Int>?

(After all 5 is equivalent to 5..<6, so mathematically speaking 5 is a range as well.)

Mischa
  • 15,816
  • 8
  • 59
  • 117
  • An alternative would be to pass an argument of type `IndexSet`. – Martin R Aug 10 '17 at 21:04
  • You mean something like `evaluateLength(validCharacterCounts: IndexSet(6..<10), IndexSet(12, 15))`? Or is there a neater solution with `IndexSet` that allows a similar syntax as described above? – Mischa Aug 13 '17 at 07:22
  • @Mischa, Did you even find a solution? – adev Aug 11 '18 at 02:50

2 Answers2

0

I came up with 3 solutions but I don't either of them are as pleasing as would be if the method accepts Int or Range<Int> but here we go?

1 - You have type safety but have to use some workarounds:

extension String {
    func evaluateLength(validCharacterCounts: [Range<Int>]? = nil) -> Bool {
        if let validCharCounts = validCharacterCounts {
            for validCharCount in validCharCounts {
                if validCharCount.contains(characters.count) {
                    return true
                }
            }
        }

        return false
    }
}

extension Int {
    var asRange: Range<Int> {
        return self..<self+1 as Range<Int>
    }
}

"Hello, playground".evaluateLength(validCharacterCounts: [17.asRange, 1, 25, 1..<45]) // true

2 - You don't have type safety:

extension String {
    func evaluateLength(validCharacterCounts: [Any]? = nil) -> Bool {
        if let validCharCounts = validCharacterCounts {
            for validCharCount in validCharCounts {
                if let range = validCharCount as? Range<Int> {
                    if range.contains(characters.count) {
                        return true
                    }
                } else if let range = validCharCount as? CountableRange<Int> {
                    if range.contains(characters.count) {
                        return true
                    }
                } else if let range = validCharCount as? CountableClosedRange<Int> {
                    if range.contains(characters.count) {
                        return true
                    }
                } else if let int = validCharCount as? Int {
                    if int.asRange.contains(characters.count) {
                        return true
                    }
                } else {
                    fatalError("Unexpected type: \(type(of: validCharCount))")
                }
            }
        }

        return false
    }
}

extension Int {
    var asRange: Range<Int> {
        return self..<self+1 as Range<Int>
    }
}

"Hello, playground".evaluateLength(validCharacterCounts: [12, 1, 4, 6, 14...18]) // true

3 - The best so far, but still not perfect solution:

extension String {
    func evaluateLength(validCharacterCounts: [Int]? = nil) -> Bool {
        guard let validCharCounts = validCharacterCounts else {
            return false
        }

        return validCharCounts.contains(characters.count)

    }
}

"Hello, playground".evaluateLength(validCharacterCounts: [12, 1, 4, 6] + Array(14...18)) // true
henrique
  • 1,072
  • 10
  • 17
  • I'm a fanatic when it comes to type safety so I don't appreciate 2 that much. 3 is pretty much what I ended up implementing myself but it doesn't solve the problem either because you still have to treat ranges and single integers differently. Thanks for the ideas though! :) – Mischa Aug 11 '17 at 11:47
0

This is how I would do it. Rather than dealing with range semantics, I'm just creating a Set extension with a relevant initializer.

extension Set where Element == Int {
    init(with elements: [Int] = [], and ranges: Range<Int>...) {
        var allElements = [elements]
        ranges.forEach {
            allElements.append(Array($0.lowerBound..<$0.upperBound))
        }
        self.init(allElements.joined())
    }
}

let newSet = Set(with: [1, 3, 328], and: 6..<10, 15..<20)
newSet.contains(3)  //true
newSet.contains(4)  //false
newSet.contains(16) //true

This allows you to pass only single values, only ranges, or both. The only caveat is the named params separating them.

The Set semantics also mean you can create static constants if desired and should be significantly faster as well.

GetSwifty
  • 7,568
  • 1
  • 29
  • 46
  • I've been experimenting with Sets as well. However, this approach introduces a separation between "single integer ranges" and "real" intervals between two different integers. And that's exactly the reason why I asked the question because semantically they are _both_ ranges and so I would like to treat them the same way. – Mischa Aug 11 '17 at 11:38
  • `evaluateLength(validCharacterCounts: 5, 7, 10...20, 25)` is a lot more intuitive than `evaluateLength(validCharacterCounts: 5, 7, 25, validCharacterCountRanges: 10...20)`, for example. – Mischa Aug 11 '17 at 11:42
  • @Mischa I agree it would be nice to do that, I tried to find a way with protocols but it may not be currently possible with Type safety. (that might change in Swift 4) – GetSwifty Aug 11 '17 at 16:03