From 2805371bdcfa05bd2e72f86077f3a54518fd89ae Mon Sep 17 00:00:00 2001 From: Vsevolod Date: Sun, 14 Jan 2018 21:03:07 +0300 Subject: [PATCH] KT-16661 Provide fast-path for Strings#split with single delimiter Optimize single delimiter split when no matches is found: Return listOf() of single element instead of adding 'this' to result because underlying array in ArrayList is created lazily and by default its size is 16. Pre-size resulting list in split when limit is known. --- libraries/stdlib/src/kotlin/text/Strings.kt | 64 +++++++++++++++++++-- libraries/stdlib/test/text/StringTest.kt | 24 +++++++- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/libraries/stdlib/src/kotlin/text/Strings.kt b/libraries/stdlib/src/kotlin/text/Strings.kt index bb6a88ae784..54522ae1d0e 100644 --- a/libraries/stdlib/src/kotlin/text/Strings.kt +++ b/libraries/stdlib/src/kotlin/text/Strings.kt @@ -1163,8 +1163,16 @@ public fun CharSequence.splitToSequence(vararg delimiters: String, ignoreCase: B * the beginning to the end of this string, and matches at each position the first element in [delimiters] * that is equal to a delimiter in this instance at that position. */ -public fun CharSequence.split(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List = - rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) } +public fun CharSequence.split(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List { + if (delimiters.size == 1) { + val delimiter = delimiters[0] + if (!delimiter.isEmpty()) { + return split(delimiters[0], ignoreCase, limit) + } + } + + return rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) } +} /** * Splits this char sequence to a sequence of strings around occurrences of the specified [delimiters]. @@ -1183,8 +1191,56 @@ public fun CharSequence.splitToSequence(vararg delimiters: Char, ignoreCase: Boo * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`. * @param limit The maximum number of substrings to return. */ -public fun CharSequence.split(vararg delimiters: Char, ignoreCase: Boolean = false, limit: Int = 0): List = - rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) } +public fun CharSequence.split(vararg delimiters: Char, ignoreCase: Boolean = false, limit: Int = 0): List { + if (delimiters.size == 1) { + return split(delimiters[0].toString(), ignoreCase, limit) + } + + return rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) } +} + +/** + * Splits this char sequence to a list of strings around occurrences of the specified [delimiter]. + * This is specialized version of split which receives single non-empty delimiter and offers better performance + * + * @param delimiter String used as delimiter + * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`. + * @param limit The maximum number of substrings to return. + */ +private fun CharSequence.split(delimiter: String, ignoreCase: Boolean, limit: Int): List { + require(limit >= 0, { "Limit must be non-negative, but was $limit." }) + + var currentOffset = 0 + var nextIndex = indexOf(delimiter, currentOffset, ignoreCase) + val isLimited = limit > 0 + // This condition cannot be moved inside of ctor call because it will kill laziness of internal array allocation + val result = if (isLimited) ArrayList(limit.coerceAtMost(10)) else ArrayList() + + while (nextIndex != -1) { + if (!isLimited || result.size < limit - 1) { + result.add(substring(currentOffset, nextIndex)) + currentOffset = nextIndex + delimiter.length + nextIndex = indexOf(delimiter, currentOffset, ignoreCase) + } else { + result.add(substring(currentOffset, length)) + currentOffset = length + break + } + } + + if (currentOffset == 0) { + // If no matches found return single element list + // instead of triggering allocation of underlying array in 'result' + // Speedup is ~15% for small string + return listOf(this.toString()) + } + + if (!isLimited || result.size < limit) { + result.add(substring(currentOffset, length)) + } + + return result +} /** * Splits this char sequence around matches of the given regular expression. diff --git a/libraries/stdlib/test/text/StringTest.kt b/libraries/stdlib/test/text/StringTest.kt index 642afb3b192..2f9641053f3 100644 --- a/libraries/stdlib/test/text/StringTest.kt +++ b/libraries/stdlib/test/text/StringTest.kt @@ -527,7 +527,6 @@ class StringTest { assertEquals(listOf("test"), (+"test").split(*arrayOf()), "empty list of delimiters, none matched -> entire string returned") assertEquals(listOf("abc", "def", "123;456"), (+"abc;def,123;456").split(';', ',', limit = 3)) - assertEquals(listOf("abc", "def", "123", "456"), (+"abc
def
123
456").split("
", ignoreCase = true)) assertEquals(listOf("abc", "def", "123", "456"), (+"abc=-def==123=456").split("==", "=-", "=")) @@ -536,6 +535,20 @@ class StringTest { assertEquals(listOf("", "", "b", "b", "", ""), (+"abba").split("a", "")) } + @Test fun splitSingleDelimiter() = withOneCharSequenceArg { arg1 -> + operator fun String.unaryPlus(): CharSequence = arg1(this) + + assertEquals(listOf(""), (+"").split(";")) + + assertEquals(listOf("abc", "def", "123,456"), (+"abc,def,123,456").split(',', limit = 3)) + assertEquals(listOf("abc", "def", "123,456"), (+"abc,def,123,456").split(",", limit = 3)) + + assertEquals(listOf("abc", "def", "123", "456"), (+"abc
def
123
456").split("
", ignoreCase = true)) + assertEquals(listOf("abc", "def
123
456"), (+"abc
def
123
456").split("
", ignoreCase = false)) + + assertEquals(listOf("a", "b", "c"), (+"a*b*c").split("*")) + } + @Test fun splitToLines() = withOneCharSequenceArg { arg1 -> val string = arg1("first line\rsecond line\nthird line\r\nlast line") assertEquals(listOf("first line", "second line", "third line", "last line"), string.lines()) @@ -545,6 +558,15 @@ class StringTest { assertEquals(listOf(singleLine.toString()), singleLine.lines()) } + @Test fun splitIllegalLimit() = withOneCharSequenceArg("test string") { string -> + assertFailsWith { string.split(*arrayOf(), limit = -1) } + assertFailsWith { string.split(*charArrayOf(), limit = -2) } + assertFailsWith { string.split("", limit = -3) } + assertFailsWith { string.split('3', limit = -4) } + assertFailsWith { string.split("1", limit = -5) } + assertFailsWith { string.split('4', '1', limit = -6) } + assertFailsWith { string.split("5", "9", limit = -7) } + } @Test fun indexOfAnyChar() = withOneCharSequenceArg("abracadabra") { string -> val chars = charArrayOf('d', 'b')