Introducing Swift Index Revolver (plus a question)

I wrote a Swift Index Revolver (requiring Swift 5.6).
It's not compilable with Swift 5.5, hence my question: how should I rewrite the parameter types?

Swift Index Revolver (requiring Swift 5.6) (github.com)

// (c) 2021 and onwards Shiki Suen (MIT-NTL License).	
// ====================	
// This code is released under the MIT license (SPDX-License-Identifier: MIT)	

import Foundation

extension Collection {
  public func revolvedIndex(_ id: Int, clockwise: Bool = true, steps: Int = 1) -> Int {
    if id < 0 || steps < 1 { return id }
    var result = id
    func revolvedIndexByOneStep(_ id: Int, clockwise: Bool = true) -> Int {
      let newID = clockwise ? id + 1 : id - 1
      if (0..<count).contains(newID) { return newID }
      return clockwise ? 0 : count - 1
    }
    for _ in 0..<steps {
      result = revolvedIndexByOneStep(result, clockwise: clockwise)
    }
    return result
  }
}

extension Int {
  public mutating func revolveAsIndex(with target: any Collection, clockwise: Bool = true, steps: Int = 1) {
    if self < 0 || steps < 1 { return }
    self = target.revolvedIndex(self, clockwise: clockwise, steps: steps)
  }
}

// MARK: - Tests

let theArray: [Int] = [1,2,3,4]
var currentIndex = 0
for _ in 0..<8 {
  currentIndex.revolveAsIndex(with: theArray)
  print(currentIndex)
}

print("")

for _ in 0..<8 {
  currentIndex.revolveAsIndex(with: theArray, clockwise: false)
  print(currentIndex)
}

I wonder whether this one is right. Haven't found any bug with it.
Please feel free to let me know if it looks wrong to you.

extension Int {
  public mutating func revolveAsIndex<T: Collection>(with target: T, clockwise: Bool = true, steps: Int = 1) {
    if self < 0 || steps < 1 { return }
    self = target.revolvedIndex(self, clockwise: clockwise, steps: steps)
  }
}
1 Like

Why wouldn't you use the generic solution for Swift > 5.5?

Also, is this what you really want? Collections don't necessarily have indexes that are Int,nor, even if it is Int does the startIndex have to be 0 or the endIndex have to be count.

1 Like

(You are probably aware of it:) Collection indices are not necessarily of type Int, and even if they are, the index of the first element need not be zero. What your revolvedIndex() operates on are “offsets” with respect to the startIndex of the collection, not indices into the collection.

The computation can be more efficiently done using the remainder operator %.

2 Likes
  1. I am writing something friendly to early releases of Xcode 13 (which doesn't support Swift 5.6).

  2. This snippet is helpful when designing something like the candidate list of an input method. When the last item is highlighted and you press the downArrow button, it revolves to the first index. I do admit that I don't have sufficient knowledges with Collection, still.

Maybe I shouldn't use Collection here.
Is Array the basic-enough type in this situation?

P.S.: Thanks for your suggestion regarding computation method.
My math is really bad. I need some time to figure out how % works.

It would certainly be easier to start with Arrayif you are just doing something simple as a learning exercise.

1 Like

Thanks. Is Array already the most basic type in this scenario?

I think it's the easiest to use collection type and its index is of type Int and indices are always in the range 0 ..< self.count, so yes.

Collection is not just a protocol but it is one with associated types, which is why you can't just write revolveAsIndex(with target: Collection ... You can't really avoid generics as soon as you start doing anything interesting with Collection.

1 Like

It's just the "remainder operator". It's not directly useful when you use negative offsets. Instead, you need a "real" modulo function:

public extension BinaryInteger {
  func modulo(_ divisor: Self) -> Self {
    (self % divisor + divisor) % divisor
  }
}

Using that, you can avoid your loops. And you can just use negative numbers instead of the "clockwise" argument.

extension Collection {
  public func revolvedIndex(_ index: Index, steps: Int = 1) -> Index {
    self.index(
      startIndex,
      offsetBy:
        (distance(from: startIndex, to: index) + steps)
        .modulo(count)
    )
  }
}
extension Comparable {
  public mutating func revolveAsIndex<Collection: Swift.Collection>(
    with target: Collection,
    steps: Int = 1
  ) where Self == Collection.Index {
    self = target.revolvedIndex(self, steps: steps)
  }
}
1 Like

Thanks for your explanation.
Looks like I can use negative divisor to reverse-revolve.

Hi, Jessy,

Sorry for a supplemental question.
Supposing that someone is gonna use your version instead, which license are you going to use? MIT or something else?

Warm Regards,
Shiki

1 Like