Search code examples
listkotlintransformation

Split list when predicate is true


Does Kotlin provide a mutation function to split a list when a specific predicate is true?

In the following example the list should be split when the element is a .. The result should be of the type List<List<String>>.

// input list
val list = listOf(
    "This is", "the", "first sentence", ".",
    "And", "now there is", "a second", "one", ".",
    "Nice", "."
)

// the following should be the result of the transformation
listOf(
    listOf("This is", "the", "first sentence"),
    listOf("And", "now there is", "a second", "one"),
    listOf("Nice")
)

I need something like list.splitWhen { it == "." }


Solution

  • Does Kotlin provide a mutation function to split a list when a specific predicate is true?

    The closest one I have heard of is partition(), however I don't think it will work in your case.

    I have made and have briefly tested 3 higher order extension functions, which gives the same expected output.

    Solution 1: Straightforward approach

    inline fun List<String>.splitWhen(predicate: (String)->Boolean):List<List<String>> {
        val list = mutableListOf<MutableList<String>>()
        var needNewList = false
        forEach {
                string->
            if(!predicate(string)){
                if(needNewList||list.isEmpty()){
                    list.add(mutableListOf(string))
                    needNewList= false
                }
                else {
                    list.last().add(string)
                }
            }
            else {
                /* When a delimiter is found */
               needNewList = true
            }
        }
        return list
     }
    

    Solution 2: Pair based approach

    inline fun List<String>.splitWhen(predicate: (String)->Boolean):List<List<String>> {
        val list = mutableListOf<List<String>>()
        withIndex()
            .filter { indexedValue ->  predicate(indexedValue.value) || indexedValue.index==0 || indexedValue.index==size-1} // Just getting the delimiters with their index; Include 0 and last -- so to not ignore it while pairing later on
            .zipWithNext() // zip the IndexValue with the adjacent one so to later remove continuous delimiters; Example: Indices : 0,1,2,5,7 -> (0,1),(1,2),(2,5),(5,7)
            .filter { pair-> pair.first.index + 1 != pair.second.index } // Getting rid of continuous delimiters; Example: (".",".") will be removed, where "." is the delimiter
            .forEach{pair->
                val startIndex = if(predicate(pair.first.value)) pair.first.index+1 else pair.first.index // Trying to not consider delimiters
                val endIndex = if(!predicate(pair.second.value) && pair.second.index==size-1) pair.second.index+1 else pair.second.index // subList() endIndex is exclusive
                list.add(subList(startIndex,endIndex)) // Adding the relevant sub-list
            }
        return list
    }
    

    Solution 3: Check next value if delimiter found approach

    inline fun List<String>.splitWhen(predicate: (String)-> Boolean):List<List<String>> =
    foldIndexed(mutableListOf<MutableList<String>>(),{index, list, string->
        when {
            predicate(string) -> if(index<size-1 && !predicate(get(index+1))) list.add(mutableListOf()) // Adds  a new List within the output List; To prevent continuous delimiters -- !predicate(get(index+1))
            list.isNotEmpty() -> list.last().add(string) // Just adding it to lastly added sub-list, as the string is not a delimiter
            else -> list.add(mutableListOf(string)) // Happens for the first String
        }
        list})
    

    Simply call list.splitWhen{it=="delimiter"}. Solution 3 looks more syntactic sugar. Apart from it, you can do some performance test to check which one performs well.

    Note: I have done some brief tests which you can have a look via Kotlin Playground or via Github gist.