8

I have a list of directions and want to find the next direction when I take a right or left turn. Here is the working code I have:

enum class Turn { R, L }
enum class Direction { N, E, S, W }
val directionsInRightTurnOrder = listOf(Direction.N, Direction.E, Direction.S, Direction.W)

private fun calculateNextHeading(heading: Direction, turn: Turn): Direction {
    val currentIndex = directionsInRightTurnOrder.indexOf(heading)
    var nextIndex = currentIndex + if (turn == Turn.R) 1 else -1
    if (nextIndex >= directionsInRightTurnOrder.size)
        nextIndex = directionsInRightTurnOrder.size - nextIndex
    if (nextIndex < 0)
        nextIndex += directionsInRightTurnOrder.size

    return directionsInRightTurnOrder.get(nextIndex)
}
  1. However, this would be so much simpler and easier to read if I could take the directionsInRightTurnOrder list and cycle through it infinitely (and lazily). In Clojure, I can do that, using clojure.core/cycle:
(take 5 (cycle ["a" "b"]))
# ("a" "b" "a" "b" "a")
  1. Another thing that would help is if I could look up in a list using a negative index, like in Ruby or Python:

Question:

  • Can I do cycle through a list/collection in Kotlin?
  • Is there an idiomatic way to do a negative-index-lookup in Kotlin?
Community
  • 1
  • 1
or9ob
  • 2,313
  • 4
  • 25
  • 45

4 Answers4

10

Here's cycle:

fun <T : Any> cycle(vararg xs: T): Sequence<T> {
    var i = 0
    return generateSequence { xs[i++ % xs.size] }
}

cycle("a", "b").take(5).toList() // ["a", "b", "a", "b", "a"]

Here's how you could implement turn application:

enum class Turn(val step: Int) { L(-1), R(1) }

enum class Direction {
    N, E, S, W;

    fun turned(turn: Turn): Direction {
        val mod: (Int, Int) -> Int = { n, d -> ((n % d) + d) % d }
        return values()[mod(values().indexOf(this) + turn.step, values().size)]
    }
}

Sounds like modulo is what you're looking for -- negative index wrap-around. Couldn't find it in Kotlin's stdlib so I brought my own.

Direction.N
    .turned(Turn.R) // E
    .turned(Turn.R) // S
    .turned(Turn.R) // W
    .turned(Turn.R) // N
    .turned(Turn.L) // W

Enum#values() and Enum#valueOf(_) are what let you access an enum's members programmatically.

danneu
  • 9,244
  • 3
  • 35
  • 63
  • I think the best answer should be this https://stackoverflow.com/questions/40938716/how-to-cycle-a-list-infinitely-and-lazily-in-kotlin/40940840#40940840 remove the last `toList()` call, that should be lazy – William Leung Dec 24 '18 at 12:38
  • Well, the .toList() is only for demonstration purposes. Without it, my example would print "kotlin.sequences.TakeSequence@1fb3ebeb". It's not part of the solution. Also, my solution indexes into a single collection which I preferred over the flatten() solution. The benefit of the flatten() solution is that it's nice to inline. – danneu Feb 06 '19 at 01:02
  • Note that since Kotlin 1.5 there is a [mod](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/mod.html) function in stdlib. – Sven Jacobs May 05 '23 at 12:27
5

Custom sequence, which repeats indefinitely the given sequence or list can be written quite easily in terms of flatten:

fun <T> Sequence<T>.repeatIndefinitely(): Sequence<T> = 
    generateSequence(this) { this }.flatten()

fun <T> List<T>.repeatIndefinitely(): Sequence<T> =
    this.asSequence().repeatIndefinitely()
Ilya
  • 21,871
  • 8
  • 73
  • 92
  • That looks simple! Thanks, I will try it out. – or9ob Dec 02 '16 at 19:35
  • Why not just `generateSequence { this }.flatten()`? Do you need the seed? – arekolek Aug 14 '17 at 16:25
  • 1
    @arekolek `generateSequence` without a seed is constrained to be iterated only once. If there is a seed, then the sequence can be iterated multiple times, each time starting with seed. https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.sequences/generate-sequence.html – aSemy Jun 05 '21 at 10:00
4

You can cycle through a list/collection in Kotlin by generating a sequence that returns the list/collection repeatedly and then flattening it. e.g.:

generateSequence { listOf("a", "b") }.flatten().take(5).toList()
// [a, b, a, b, a]

You can define your own modulo function for coercing both negative and positive numbers to valid indices to access elements in a list (see also Google Guava's IntMath.mod(int, int)):

infix fun Int.modulo(modulus: Int): Int {
    if (modulus <= 0) throw ArithmeticException("modulus $modulus must be > 0")
    val remainder = this % modulus
    return if (remainder >= 0) remainder else remainder + modulus
}

val list = listOf("a", "b", "c", "d")
list[-1 modulo list.size] // last element
list[-2 modulo list.size] // second to last element
list[+9 modulo list.size] // second element
list[-12 modulo list.size] // first element
mfulton26
  • 29,956
  • 6
  • 64
  • 88
  • Note that since Kotlin 1.5 there is a [mod](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/mod.html) function in stdlib. – Sven Jacobs May 05 '23 at 12:29
0

Paraphrasing the discussion on kotlin Slack:

  • using List#modulo would make this simpler, but not as elegant as cycle still, since negative indexes will still need to be handled.

  • One option to implement a cyclical list is a Sequence. However, a custom Sequence will need to be written, or generated using generateSequence. We thought it's an overkill for this situation.

Eventually I went with:

  1. Making Direction aware of next and previous:
enum class Direction {
    N, E, S, W;

    private val order by lazy { listOf(N, E, S, W) }

    fun add(turns: Int): Direction {
        val currentIndex = order.indexOf(this)

        var nextIndex = (currentIndex + turns) % order.size
        return order.possiblyNegativeLookup(nextIndex)
    }

    fun subtract(turns: Int) = add(-1 * turns)
    fun next(): Direction = add(1)
    fun previous(): Direction = subtract(1)
}
  1. Extending List with possiblyNegativeLookup:
fun <E> List<E>.possiblyNegativeLookup(i: Int): E {
    return if (i < 0) this[this.size + i] else this[i]
}

So the final code turns into:

val nextHeading = if (move.turn == Turn.R) heading.next() else heading.previous()
or9ob
  • 2,313
  • 4
  • 25
  • 45