Elye
Elye

Reputation: 60081

Is there a way to merge filter and map into single operation in Kotlin?

The below code will look for "=" and then split them. If there's no "=", filter them away first

myPairStr.asSequence()
        .filter { it.contains("=") }
        .map { it.split("=") }

However seeing that we have both

        .filter { it.contains("=") }
        .map { it.split("=") }

Wonder if there's a single operation that could combine the operation instead of doing it separately?

Upvotes: 11

Views: 6954

Answers (2)

Roland
Roland

Reputation: 23242

I see your point and under the hood split is also doing an indexOf-check to get the appropriate parts.

I do not know of any such function supporting both operations in a single one, even though such a function would basically just be similar to what we have already for the private fun split-implementation.

So if you really want both in one step (and require that functionality more often), you may want to implement your own splitOrNull-function, basically copying the current (private) split-implementation and adapting mainly 3 parts of it (the return type List<String>?, a condition if indexOf delivers a -1, we just return null; and some default values to make it easily usable (ignoreCase=false, limit=0); marked the changes with // added or // changed):

fun CharSequence.splitOrNull(delimiter: String, ignoreCase: Boolean = false, limit: Int = 0): List<String>? { // changed
    require(limit >= 0, { "Limit must be non-negative, but was $limit." })

    var currentOffset = 0
    var nextIndex = indexOf(delimiter, currentOffset, ignoreCase)
    if (nextIndex == -1 || limit == 1) {
        if (currentOffset == 0 && nextIndex == -1) // added
            return null                            // added
        return listOf(this.toString())
    }

    val isLimited = limit > 0
    val result = ArrayList<String>(if (isLimited) limit.coerceAtMost(10) else 10)
    do {
        result.add(substring(currentOffset, nextIndex))
        currentOffset = nextIndex + delimiter.length
        // Do not search for next occurrence if we're reaching limit
        if (isLimited && result.size == limit - 1) break
        nextIndex = indexOf(delimiter, currentOffset, ignoreCase)
    } while (nextIndex != -1)

    result.add(substring(currentOffset, length))
    return result
}

Having such a function in place you can then summarize both, the contains/indexOf and the split, into one call:

myPairStr.asSequence()
         .mapNotNull {
           it.splitOrNull("=") // or: it.splitOrNull("=", limit = 2)
         }

Otherwise your current approach is already good enough. A variation of it would just be to check the size of the split after splitting it (basically removing the need to write contains('=') and just checking the expected size, e.g.:

myPairStr.asSequence()
         .map { it.split('=') } 
         .filter { it.size > 1 }

If you want to split a $key=$value-formats, where value actually could contain additional =, you may want to use the following instead:

myPairStr.asSequence()
         .map { it.split('=', limit = 2) } 
         .filter { it.size > 1 }
//       .associate { (key, value) -> key to value }

Upvotes: 1

Abhay Agarwal
Abhay Agarwal

Reputation: 908

You can use mapNotNull instead of map.

myPairStr.asSequence().mapNotNull { it.split("=").takeIf { it.size >= 2 } }

The takeIf function will return null if the size of the list returned by split method is 1 i.e. if = is not present in the string. And mapNotNull will take only non null values and put them in the list(which is finally returned). In your case, this solution will work. In other scenarios, the implementation(to merge filter & map) may be different.

Upvotes: 10

Related Questions