breezymri
breezymri

Reputation: 4353

How to perform action for all combinations of elements in lists?

I have multiple lists, for the sake of simplicity, let's say 2:

val names = listOf("John", "Tom")
val days = listOf(1, 2, 3)

And I want to iterate over all the combinations of these possible values with a function f: (String, Int): Unit:

f("John", 1)
f("John", 2)
f("John", 3)
f("Tom", 1)
f("Tom", 2)
f("Tom", 3)

I'd like to find out the idiomatic way to do this in kotlin. Off the top of my head, I imagine that this can be done with a nested map. Something along the lines of:

a.map { itA -> 
  b.map { itB ->
    f(itA, itB)
  }
}

EDIT: This doesn't seem to work for me though as it returns:

[() -> kotlin.collections.List<kotlin.Unit>, () -> kotlin.collections.List<kotlin.Unit>]

Upvotes: 12

Views: 13696

Answers (5)

Mr.Q
Mr.Q

Reputation: 4524

You can generate different combinations via flat mapping.

val names = listOf("John", "Tom")
val days = listOf(1, 2, 3)
val isSht = listOf(true, false)

val combz =
     names.flatMap { name ->
        days.flatMap { day ->
            isSht.map { isSht -> "${day} ${isSht}" }
        }.map { daySht -> "${daySht} ${name}" }
    }.toList()

Upvotes: 0

Tom Hanley
Tom Hanley

Reputation: 1322

The name of the output you're looking for is a Cartesian Product.

If you don't need all of the combinations up front, and you're just searching for one or more, you could make it a lazily generated sequence:

private fun <A, B> lazyCartesianProduct(
    listA: Iterable<A>,
    listB: Iterable<B>
): Sequence<Pair<A, B>> =
    sequence {
        listA.forEach { a ->
            listB.forEach { b ->
                yield(a to b)
            }
        }
    }

Then you can map, filter etc only what you need:

val names = listOf("John", "Tom")
val days = listOf(1, 2, 3)

lazyCartesianProduct(names, days)
    .map { it.toSomethingElse() }
    .find { it.isWhatImLookingFor() }

Upvotes: 13

TWiStErRob
TWiStErRob

Reputation: 46480

In case you actually need the result from f here's what you can do:

fun <T1, T2, R> combine(
        first: Iterable<T1>,
        second: Iterable<T2>,
        combiner: (T1, T2) -> R
): List<R> = first.flatMap { firstItem -> second.map { secondItem -> combiner(firstItem, secondItem) } }

@Test fun test() {
    val names = listOf("John", "Tom")
    val days = listOf(1, 2, 3)

    val result = combine(days, names, { day, name -> "${day}. ${name}" })

    @Suppress("DEPRECATION")
    kotlin.test.assert(result) {
        shouldBe(listOf(
                "1. John",
                "1. Tom",
                "2. John",
                "2. Tom",
                "3. John",
                "3. Tom"
        ))
    }
}

Note: it also works with println, in that case R is Unit.

Upvotes: 4

Brett Beatty
Brett Beatty

Reputation: 5983

What's wrong with a regular for loop?

for (name in names) for (day in days) f(name, day)

Upvotes: 5

Grzegorz Piwowarek
Grzegorz Piwowarek

Reputation: 13793

The forEach is the closest as it can get:

names.forEach {
    i -> days.forEach { j -> f(i, j)}
}

Example:

private fun f(i: String, j: Int) = println("$i,$j")

Result:

John,1
John,2
John,3
Tom,1
Tom,2
Tom,3

Upvotes: 6

Related Questions