In Swift, outwards pingpong sequence?

Viewed 223

Say you have

for i in 0 ... 10 {
 print(i)
}

of course it will print 0,1,2,3,4,5,6,7,8,9,10

for i in 0 ..< 5 {

that's 0,1,2,3,4.

I want to start at a certain integer and pingpong outwards over the count of numbers

So,

for i in function or something (10, 3)

that's 3 4 2 5 1 6 0 7 8 9

for i in function or something (10, 8) {

would be 8 9 7 6 5 4 3 2 1 0

for i in function or something (10, 2) {

would be 2 3 1 4 0 5 6 7 8 9

So it's just an outwards pingpong.

What should I type where I have written function or something (10, 2)?

There might be some really cool syntax possible, along the lines 0 # 7 # 10.

What about something like (0..<10).outPong(3)?

How to formulate such a sequence?


Here's a naive example of how you'd do an outwards pingpong, at the call level.

Call exampleLoad for each of the items in RA, outwards pingpoing:

func loadItemsPongwise(startWith: Int) {

    // RA = ... this is your array of some type

    exampleLoad(startWith)
    let k = RA.count
    var howManyDone: Int = 0
    var distance: Int = 1

    while howManyDone < ( k - 1 ) {

        let tryRight = alreadyLoaded + distance
        if tryRight < k {
            howManyDone = howManyDone + 1
            exampleLoad(RA[tryRight])
        }

        let tryLeft = alreadyLoaded - distance
        if tryLeft >= 0 {
            howManyDone = howManyDone + 1
            exampleLoad(RA[tryLeft])
        }

        distance = distance + 1
    }
}

Of course, something like this wouild be much nicer:

func loadItemsPongwise(startWith: Int) {
    for i in ???? {
      exampleLoad(i)
    }
}
4 Answers
public extension ClosedRange where Bound: AdditiveArithmetic {
  func (
    by contiguousAdvancement: Bound,
    startingAt start: Bound
  ) -> AnySequence<Bound> {
    guard contains(start)
    else { return .init( EmptyCollection() ) }

    var advancement = contiguousAdvancement

    typealias Operate = (Bound, Bound) -> Bound
    var pingPong: Operate = (+)
    var contiguouslyAdvance: Operate = (-)

    return .init(
      sequence(first: start) { previous in
        pingPongIterate: do {
          defer { advancement += contiguousAdvancement }

          let pingPonged = pingPong(previous, advancement)

          guard self.contains(pingPonged)
          else { break pingPongIterate }

          (pingPong, contiguouslyAdvance) = (contiguouslyAdvance, pingPong)
          return pingPonged
        }

        let contiguouslyAdvanced = contiguouslyAdvance(previous, contiguousAdvancement)
        return self.contains(contiguouslyAdvanced)
          ? contiguouslyAdvanced
          : nil
      }
    )
  }
}

public extension ClosedRange where Bound: AdditiveArithmetic & ExpressibleByIntegerLiteral {
  func (startingAt start: Bound) -> AnySequence<Bound> {
    (by: 1, startingAt: start)
  }
}

public extension ClosedRange where Bound: BinaryInteger {
  func (by contiguousAdvancement: Bound = 1) -> AnySequence<Bound> {
    (by: contiguousAdvancement, startingAt: (upperBound + lowerBound) / 2)
  }
}

public extension ClosedRange where Bound: FloatingPoint {
  func (by contiguousAdvancement: Bound = 1) -> AnySequence<Bound> {
    (by: contiguousAdvancement, startingAt: (upperBound + lowerBound) / 2)
  }
}
XCTAssertEqual(
  Array( (2...10).() ),
  [6, 7, 5, 8, 4, 9, 3, 10, 2]
)

XCTAssertEqual(
  Array( (2...10).(startingAt: 7) ),
  [7, 8, 6, 9, 5, 10, 4, 3, 2]
)

XCTAssertEqual(
  Array( (-1.5...7.5).(by: 1.5) ),
  [3, 4.5, 1.5, 6, 0, 7.5, -1.5]
)

XCTAssertEqual(
  Array( (0...6).(by: -1) ),
  [3, 2, 4, 1, 5, 0, 6]
)

XCTAssertEqual(
  Array( (0...3).(startingAt: 4) ),
  []
)

So I went the route of taking the analogy of ping-pong seriously. I left some comments for clarity.

It simulates an actual ping pong ball bouncing (starting from the net, oddly), back and forth on a ping pong table that has a net that might not be centered. If it's about to go off the edge on one side, then it just goes to the other side and I like to imagine it does smaller and smaller bounces until it rolls off the table.

Here's the code with comments and a test:

// It's supposed to be a ping pong table ‍♂️
struct : IteratorProtocol, Sequence {

    typealias Element = Int
    // The table *is* the iterator
    typealias Iterator = 

    let leftEdgePosition: Int
    /// The starting point for the ball
    let netPosition: Int
    let rightEdgePosition: Int

    /// For convenience in checking whether different ball positions are on the table.
    private let tableBounds: ClosedRange<Int>

    init(leftEdgePosition: Int, netPosition: Int, rightEdgePosition: Int) {
        self.leftEdgePosition = leftEdgePosition
        self.netPosition = netPosition
        self.rightEdgePosition = rightEdgePosition
        self.tableBounds = leftEdgePosition...rightEdgePosition
    }

    private var distanceFromNet = 0

    /// The side of the table the ping pong ball is headed toward
    private var ballDirection: PingPongBallDirection = .towardLeftEdge

    func makeIterator() ->  {
        return self
    }

    /// This gets called for each iteration in the for loop. Once the ball goes beyond the table, we should return nil to stop the for loop.
    mutating public func next() -> Int? {
        // the ball position we will return if this position is on the table
        let ballPosition = ballDirection.locationCalculator(netPosition, distanceFromNet)
        // the ball position we will return if the first ball position is not on the table
        let redirectedPosition = (!ballDirection).locationCalculator(netPosition, distanceFromNet)

        // determine which ball position to return and set up our state for the next call to next()
        var ballPositionToReturn: Int?
        if tableBounds.contains(ballPosition) {
            ballPositionToReturn = ballPosition

            let ballMirrorPosition = (!ballDirection).locationCalculator(netPosition, distanceFromNet)
            let ballIsTrailingOff = !tableBounds.contains(ballMirrorPosition)

            if !ballIsTrailingOff {
                // switch the direction because the ball hit the table
                ballDirection = !ballDirection
            }

            // If we're heading to the right, i.e 3 -> 4 in the case of 0 << 3 >> 10, then increase
            // the distance from the net.
            // If we're trailing off and not ping-ponging any more, then we need to add distance.
            if ballDirection == .towardRightEdge || ballIsTrailingOff  {
                distanceFromNet += 1
            }
        } else if tableBounds.contains(redirectedPosition) {
            ballPositionToReturn = redirectedPosition

            // reflect the redirection
            ballDirection = !ballDirection
            // add distance when we redirect
            distanceFromNet += 1
        }

        return ballPositionToReturn
    }

}

enum PingPongBallDirection {

    case towardLeftEdge
    case towardRightEdge

    /// Returns the oppposite direction
    static prefix func !(direction: PingPongBallDirection) -> PingPongBallDirection {
        switch direction {
        case towardLeftEdge: return towardRightEdge
        case towardRightEdge: return towardLeftEdge
        }
    }

    // In our world, right is greater and left is lesser.
    var locationCalculator: (Int, Int) -> Int {
        switch self {
        case .towardLeftEdge: return (-)
        case .towardRightEdge: return (+)
        }
    }

}

// Make the syntax work
precedencegroup PingPongPrecedenceGroup {
    associativity: left
    // this makes sure the ping pong operator gets evaluated before the assignment operator
    higherThan: AssignmentPrecedence
}

infix operator ...: PingPongPrecedenceGroup

func ... (lhs: ClosedRange<Int>, rhs: Int) ->  {
    return (leftEdgePosition: lhs.lowerBound, netPosition: lhs.upperBound, rightEdgePosition: rhs)
}

for i in 0...10 {
    for j in 0...i...10 {
        print(j, terminator: " ")
    }
    print()
}

// OUTPUT:
// 0 1 2 3 4 5 6 7 8 9 10
// 1 2 0 3 4 5 6 7 8 9 10
// 2 3 1 4 0 5 6 7 8 9 10
// 3 4 2 5 1 6 0 7 8 9 10
// 4 5 3 6 2 7 1 8 0 9 10
// 5 6 4 7 3 8 2 9 1 10 0
// 6 7 5 8 4 9 3 10 2 1 0
// 7 8 6 9 5 10 4 3 2 1 0
// 8 9 7 10 6 5 4 3 2 1 0
// 9 10 8 7 6 5 4 3 2 1 0
// 10 9 8 7 6 5 4 3 2 1 0

stateless

Just for anyone working on the syntax.

I wasted an hour of my life figuring out a stateless conversion.

(I couldn't make it simple or elegant - maybe someone else can!)

var plaground = "directly convert a single ping pong index to a plain index"

let L: Int = 10
let S: Int = 7

func ppiToIndex(_ ppi: Int) -> Int {
    let inner = S+1 < (L-S) ? (S+1) : (L-S)
    let pp = (ppi+1) / ( (ppi % 2 == 1) ? 2 : -2 )
    let way = (S < L/2) ? -(inner-ppi-1) : (inner-ppi-1)
    return (ppi < inner*2-1) ? S+pp : S+way
}

for i in 0..<L {
    print(" \(i) \(ppiToIndex(i)) ")
}

inner is how many from the start inclusive to the nearer end inclusive.

pp is a full-on endless ping pong.

way is the correct direction +/- to add once you pass the inner area.

Think of how you'd do this with a deck of cards, or a set of matchsticks. Just split the series into two at the point where you want to start, reverse the order of one of the resulting series, and alternate pulling values off the two series.

Here's a utility that alternates between two series until both are exhausted:

func alternateUntilBothAreExhausted<T> (arr1:Array<T>, arr2:Array<T>)
    -> Array<T> {
        var result = Array<T>()
        var arr1 = arr1; var arr2 = arr2
        while true {
            if let last1 = arr1.popLast() {
                result.append(last1)
            }
            if let last2 = arr2.popLast() {
                result.append(last2)
            }
            if arr1.isEmpty && arr2.isEmpty {
                return result
            }
        }
}

So we start with one series, split it, reverse one, and alternate:

func pingPong<T>(array:Array<T>, startingAtIndex ix:Int) -> Array<T> {
    let arr1 = array[..<ix]
    let arr2 = array[ix...]
    return alternateUntilBothAreExhausted(
        arr1: Array(arr1), arr2: Array(arr2.reversed()))
}

Example:

let ping = pingPong(array: Array(0..<10), startingAtIndex:4)
// [3, 4, 2, 5, 1, 6, 0, 7, 8, 9]

Wrapping that up in your desired syntax is trivial and is left as an exercise for the reader.

Related