Get indexes of substrings contained in a string in Kotlin-way

Viewed 7546

I want to implement a function that will return the indexes of the substrings in the specified string. For now i did it in Java-style:

public fun String?.indexesOf(substr: String, ignoreCase: Boolean = true): List<Int> {
    var list = mutableListOf<Int>()
    if (substr.isNullOrBlank()) return list
    var count = 0;
    this?.split(substr, ignoreCase = ignoreCase)?.forEach {
        count += it.length
        list.add(count)
        count += substr.length
    }
    list.remove(list.get(list.size-1))
    return list
}

But I don't think this is a kotlin-way solution. Its most looks like typical java program but written in kotlin. How can this be implemented more elegantly using kotlin?

7 Answers

what would i do is the following:

fun ignoreCaseOpt(ignoreCase: Boolean) = 
    if (ignoreCase) setOf(RegexOption.IGNORE_CASE) else emptySet()

fun String?.indexesOf(pat: String, ignoreCase: Boolean = true): List<Int> =
    pat.toRegex(ignoreCaseOpt(ignoreCase))
        .findAll(this?: "")
        .map { it.range.first }
        .toList()

// check:
println("xabcaBd".indexesOf("ab", true))
println("xabcaBd".indexesOf("ab", false))
println("xabcaBd".indexesOf("abx", true))

val s: String? = null
println(s.indexesOf("aaa"))

// output:
[1, 4]
[1]
[]
[]

You could condense it down to something like this:

public fun String?.indexesOf(substr: String, ignoreCase: Boolean = true): List<Int> {
    return this?.let { 
        val regex = if (ignoreCase) Regex(substr, RegexOption.IGNORE_CASE) else Regex(substr)
        regex.findAll(this).map { it.range.start }.toList()
    } ?: emptyList()
}

Whether that's more efficient is a different matter. You'd have to test that.


If you wanted "aaa".indexesOf("aa") to return [0, 1] rather than just [0], you should be able to do that by modifying the regex to use positive lookahead, i.e.:

val regex = if (ignoreCase) Regex("(?=$substr)", RegexOption.IGNORE_CASE) else Regex("(?=$substr)")

Correct way is to use String.indexOf(), since splitting would ignore some substring occurrences.

For example with input "aaaa" and substr "aaa" ("aaaa".indexesOf("aaa")) result should be [0, 1] but your solution (using split) will result to [0]

public fun String?.indexesOf(substr: String, ignoreCase: Boolean = true): List<Int> {
    val list = mutableListOf<Int>()
    if (this == null || substr.isBlank()) return list

    var i = -1
    while(true) {
        i = indexOf(substr, i + 1, ignoreCase)
        when (i) {
            -1 -> return list
            else -> list.add(i)
        }
    }
}

Here's a tail-recursive example that doesn't hold any mutable state:

fun String?.indexesOf(substr: String, ignoreCase: Boolean = true): List<Int> {
    tailrec fun String.collectIndexesOf(offset: Int = 0, indexes: List<Int> = emptyList()): List<Int> =
        when (val index = indexOf(substr, offset, ignoreCase)) {
            -1 -> indexes
            else -> collectIndexesOf(index + substr.length, indexes + index)
        }

    return when (this) {
        null -> emptyList()
        else -> collectIndexesOf()
    }
}

"abcABCbcaabcabcaaabc".indexesOf("ddd")
// []
"abcABCbcaabcabcaaabc".indexesOf("abc", ignoreCase = false)
// [0, 9, 12, 17]
"abcABCbcaabcabcaaabc".indexesOf("abc", ignoreCase = true)
// [0, 3, 9, 12, 17]
null.indexesOf("abc", ignoreCase = true)
// []

It will find the first index of the substring, and recursively continue shortening it to find the next occurrence.

Try this using the indexOf function

fun String?.indexesOf(substr: String, ignoreCase: Boolean = false): List<Int> {
    return this?.let {
        val indexes = mutableListOf<Int>()
        var startIndex = 0
        while(startIndex in 0 until length){
            val index = this.indexOf(substr, startIndex, ignoreCase)
            startIndex = if(index != -1){
                indexes.add(index)
                index + substr.length
            } else{
                index
            }
        }
        return indexes
    } ?: emptyList()
}

I really like @leetwinski's and @Michael's answers.

There's so many possibilities with Kotlin, it's amazing :)

Another possible solution based on the above:

fun String.indexesOf(substr: String, ignoreCase: Boolean = true) : List<Int> =
    (if (ignoreCase) Regex(substr, RegexOption.IGNORE_CASE) else Regex(substr))
        .findAll(this).map { it.range.first }.toList()

@JvmName("indexesOfNullable")
fun String?.indexesOf(substr: String, ignoreCase: Boolean = true) = this?.indexesOf(substr, ignoreCase) ?: emptyList()

This should be a comment leetwinski's anseer , but SO won't let me write comments.

It's a great solution but be aware that if your query string contains any special characters that have special meaning in regex, that may give you incorrect results or even a PatternSyntaxException and crash your app.

So, if you want to look for a literal match, you have to use escape

So the code will

fun ignoreCaseOpt(ignoreCase: Boolean) =
        if (ignoreCase) setOf(RegexOption.IGNORE_CASE) else emptySet()

fun String?.indexesOf(query: String, ignoreCase: Boolean = true): List<Int> =
    Regex.escape(query)       // to disable any special meaning of query's characters
        .toRegex(ignoreCaseOpt(ignoreCase))
        .findAll(this?: "")
        .map { it.range.first }
        .toList()
Related