I have a `List<String>` that i'd like to spl...
# announcements
l
I have a
List<String>
that i'd like to split into a
List<List<String>>
by indicating a delimiter
String
, similar to how i can split a
String
into a
List<String>
by indicating a delimiter
Char
using
.split()
. unfortunately lists dont have a
.split()
function, so what would be the best way to implement it ? looking for a functional approach if possible
e
stringlist.map{ it.split('char') }
👆 1
Try to read some docs about list.map and list.flatMap
l
that is not doing what i want
this splits the strings themselves instead of the list
listOf(1,2,3,4,5).split(3) == listOf(listOf(1,2), listOf(4,5)
is what i want (and i just realized it doesnt need to be strings, any type works)
e
Oh okay
e
I use this for Advent of Code: link. If the
selector
returns
true
, that string is considered the delimiter.
💯 1
It's not the most performant solution, but it prevents having to repeatedly add to the list after a loop. It'd get the job done, but I don't like that. Anyway, such a solution would be something like:
Copy code
fun <T> List<T>.chunkedBy(selector: (T) -> Boolean): List<List<T>> {
    val chunks = mutableListOf(mutableListOf<T>())
    val chunk = mutableListOf<T>()
    for (item in this) {
        if (selector(item)) {
            chunks.add(chunk)
            chunk.clear()
        } else {
            chunk.add(item)
        }
    }
    chunks.add(chunk) // add the last chunk that wasn't separated
    return chunks
}
Or something like that. But I'm not sure what would happen if you had multiple delimeters at the end, for example. Not sure what my code would do either, to be honest. Add multiple empty lists? Could add
filter { it.isNotEmpty() }
.
l
it does what i need, but seems pretty hacky tbh. guess functional isnt the ideal approach here after all
n
Why does it seem hacky?
A lot of the functional tools we use are internally implemented using mutation and are a bit messy
That's part of the point to abstract it out
I'd implement it using sequence though
v
a little more functional (and a lot less efficient, I guess) approach:
Copy code
fun <T> List<T>.split(separator: T): List<List<T>> = fold(emptyList()) { acc, element ->
    when {
        element == separator -> acc.plusElement(emptyList())
        acc.isNotEmpty() -> acc.dropLast(1).plusElement(acc.last().plusElement(element))
        else -> listOf(listOf(element))
    }
}
s
Similarly:
Copy code
fun <A> List<A>.splitWhen(f: (A) -> Boolean): List<List<A>> =
    if (isEmpty()) emptyList()
    else listOf(takeWhile { !f(it) }) + dropWhile { !f(it) }.drop(1).splitWhen(f)

listOf(1, 2, 3, 4, 5).splitWhen { it == 3 }
You'll need to think about how you want to handle neighbouring separators though, e.g.
Copy code
listOf(1, 2, 3, 3, 4, 5).splitWhen { it == 3 }
Do you want that to evaluate to
listOf(listOf(1, 2), emptyList(), listOf(4, 5))
or
listOf(listOf(1, 2), listOf(4, 5))
?
n
should mark that with tailrec if it allows you
Without tailrec probably don't want to implement that way
s
You'd need a separate inner function to tailrec it because the recursive call isn't in tail position.
e.g.
Copy code
fun <A> List<A>.splitWhen(f: (A) -> Boolean): List<List<A>> {
    tailrec fun go(rest: List<A>, result: List<List<A>>): List<List<A>> =
        if (rest.isEmpty()) result
        else go(
            rest.dropWhile { !f(it) }.drop(1),
            result + listOf(rest.takeWhile { !f(it) })
        )

    return go(this, emptyList())
}
n
Yeah I wasn't sure the restrictions on it
At any rate without that you have potentially linear recursion depth with the problem size, likely inefficient and also maybe risking a stack overflow or hitting some kind of limit (not sure what happens on JVM)
Using sequences; mine is hardcoded to use empty strings (it's designed directly for use in AOC as I see this pattern of grouped inputs keep coming up)
Copy code
fun Sequence<String>.chunkedByEmpty(): Sequence<List<String>> = sequence {
    var currentList = mutableListOf<String>()
    for (x in this@chunkedByEmpty) {
        if (x != "") {
            currentList.add(x)
            continue
        }
        yield(currentList)
        currentList = mutableListOf<String>()
    }
    yield(currentList)
}

inline fun <R> File.useChunkedLines(transform: (Sequence<List<String>>) -> R) =
    useLines { transform(it.chunkedByEmpty()) }