8

Swift 2.2 deprecated the C-style loop. However in some cases, the new range operator just doesn't work the same.

for var i = 0; i < -1; ++i { ... }

and

for i in 0..<-1 { ... }

The later one will fail at run-time. I can wrap the loop with an if, but it's a bit cluttered. Sometimes this kind of loop is useful.

Any thoughts?

Use cases

  1. You need to enumerate all elements of an array, except the last one.
  2. You need to enumerate all whole integer numbers in a decimal range, but the range can be like [0.5, 0.9] and so there's no integers (after some maths), which results in an empty loop.
Khanh Nguyen
  • 11,112
  • 10
  • 52
  • 65
  • Did your loop run ever run in Swift 2.2? – Code Different Mar 22 '16 at 23:11
  • Yes it does, but is marked as deprecated. – Khanh Nguyen Mar 22 '16 at 23:12
  • 1
    Even if it was valid in Swift 2.2, the inner body never execute – Code Different Mar 22 '16 at 23:12
  • No, not in the example. But sometimes I need to loop on a dynamically computed range, and the range may be empty. – Khanh Nguyen Mar 22 '16 at 23:14
  • You will have to use `for` loops only for iterating or sanitize your input, e.g. `for i in 0 ..< max(0, -1)`. Or use a `while` – Sulthan Mar 22 '16 at 23:14
  • Hey using `max` is actually a pretty good idea! Thanks. Still doesn't look as concise as the C-style one :( – Khanh Nguyen Mar 22 '16 at 23:16
  • Could you maybe add a real use case? We could probably find a much better solution. – Sulthan Mar 22 '16 at 23:17
  • @Sulthan Use case: you need to enumerate all the whole integers in a decimal range, but the range might be like [0.5, 0.9], and results in no whole integers (after some maths, of course), which results in an empty loop. – Khanh Nguyen Mar 22 '16 at 23:20
  • @Sulthan Or you need to enumerate all elements of an array, except the last one – Khanh Nguyen Mar 22 '16 at 23:21
  • @KhanhNguyen Then you should be using the functional style: `for item in array.dropLast()`. Will work even with empty arrays. – Sulthan Mar 22 '16 at 23:30
  • @Sulthan Thanks very much! That will do it if an array is involved. Any thoughts on the decimal case? It's just so much convenient if there's a range operator that returns empty set if the lower end is greater than the higher end. – Khanh Nguyen Mar 22 '16 at 23:38
  • 1
    @KhanhNguyen I could pile on many other examples from my own code where loss of C-style for loop was sort of a disaster. They were all solvable but the result was far less elegant. I sometimes wonder how the inmates got to be in charge of the Swift madhouse... – matt Mar 25 '16 at 18:14
  • Aside from the iteration problem, the run-time fail is because the implicit add is tested for overflow. Use the "overflow operators" `&+` and `&-` (and `&*`), which don't raise exceptions on overflow, if you intend to rely on wrap-around. – t0rst Oct 10 '16 at 14:01

4 Answers4

20

Although it's not as "pretty", you can use stride:

for var i in 0.stride(to: -1, by: -1) {
    print(i)
}
Michael
  • 8,891
  • 3
  • 29
  • 42
  • 1
    I agree, drive-by downvote by someone who probably did not understand this solution. I believe the `by` should be `1` rather than `-1` though, to mirror the OP:s question: `0.stride(to: -1, by: 1) ...` will yield an empty stridethrough, equivalent to the "C-style loop" described by the OP, whereas `0.stride(to: -1, by: -1) ...` will, _by coincident_ (strict `to<..from`), also yield an empty stridethough, whereas e.g. `0.stride(to: -2, by: -1) ...`, will not. – dfrib Mar 23 '16 at 08:39
9

Mimicking the "C-style loop"

Not entirely pretty, but you can wrap the range:s upper bound with a max(0, ..) to ascertain it never takes negative values.

let foo : [Int] = []
for i in 0..<max(0,foo.count-1) {
    print(i)
}

I'd prefer, however, the from.stride(to:by) solution (that has already been mentioned in the other answers, see e.g. Michael:s answer).

I think it's valuable to explicitly point out, however, that from.stride(to:by) neatly returns an empty StrideTo (or, if converted to an array: an empty array) if attempting to stride to a number that is less than from but by a positive stride. E.g., striding from 0 to -42 by 1 will not attempt to stride all the way through "∞ -> -∞ -> -42" (i.e., an error case), but simply returns an empty StrideTo (as it should):

Array(0.stride(to: -42, by: 1)) // []

// -> equivalent to your C loop:
for i in 0.stride(to: foo.count-1, by: 1) { 
    print(i) 
}

Use case 1: enumerate all but the last element of an array

For this specific use case, a simple solution is using dropLast() (as described by Sulthan in the comments to your question) followed by forEach.

let foo = Array(1...5)
foo.dropLast().forEach { print($0) } // 1 2 3 4

Or, if you need more control over what to drop out, apply a filter to your array

let foo = Array(1...5)
foo.filter { $0 < foo.count }.forEach { print($0) } // 1 2 3 4

Use case 2: enumerate all integers in a decimal range, allowing this enumeration to be empty

For your decimal/double closed interval example ([0.6, 0.9]; an interval rather than a range in the context of Swift syntax), you can convert the closed interval to an integer range (using ceil function) and apply a forEach over the latter

let foo : (ClosedInterval<Double>) -> () = {
    (Int(ceil($0.start))..<Int(ceil($0.end)))
        .forEach { print($0) }
}

foo(0.5...1.9) // 1
foo(0.5...0.9) // nothing

Or, if you specifically want to enumerate the (possible) integers contained in this interval; use as en extension fit to your purpose:

protocol MyDoubleBounds {
    func ceilToInt() -> Int
}

extension Double: MyDoubleBounds {
    func ceilToInt() -> Int {
        return Int(ceil(self)) // no integer bounds check in this simple example
    }
}

extension ClosedInterval where Bound: MyDoubleBounds {
    func enumerateIntegers() -> EnumerateSequence<(Range<Int>)> {
        return (self.start.ceilToInt()
            ..< self.end.ceilToInt())
            .enumerate()
    }
}

Example usage:

for (i, intVal) in (1.3...3.2).enumerateIntegers() {
    print(i, intVal)
} /* 0 2
     1 3 */

for (i, intVal) in (0.6...0.9).enumerateIntegers() {
    print(i, intVal)
} /* nothing */
dfrib
  • 70,367
  • 12
  • 127
  • 192
4

For reference: In swift 3.0 stride is now defined globally which makes for loop look more natural:

for i in stride(from: 10, to: 0, by: -1){
    print(i)
} /* 10 9 8 7 6 5 4 3 2 1 */
ambientlight
  • 7,212
  • 3
  • 49
  • 61
0

For Swift 3 and need to change the "index"

for var index in stride(from: 0, to: 10, by: 1){}
d0ye
  • 1,580
  • 1
  • 15
  • 26