9

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?

2
  • at least you can use list.dropLast(1) instead of list.remove(list.get(list.size-1)) Commented Jun 4, 2020 at 8:22
  • Just remember that readability > conciseness Commented Jun 4, 2020 at 8:47

7 Answers 7

15

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]
[]
[]
Sign up to request clarification or add additional context in comments.

Comments

9

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)")

3 Comments

@IR42 That means exactly the same. (They both return the EmptyList singleton object.)
If the substring contains Parentheses then it returns 0 occurrences.
Make sure to escape some of the characters. In my case I had to escape $ sign by .replace("$","\\$") for this to work
4

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)
        }
    }
}

Comments

4

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()

Comments

3

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.

3 Comments

Each call substring(index + substr.length) will produce new string. That's not efficient, especially on large strings
I'd love to see your benchmarks ;) this question isn't about micro-optimisations people.
@IR42 that's a nice improvement. I will update the answer.
3

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()

1 Comment

PS: the @JvmName("indexesOfNullable") annotation is because the JVM method signatures clash. To see the full message remove the annotation in IntelliJ IDEA.
1

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()
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.