From 81d2d3cb6a0942b8d323928de4ef41a1a4bc3123 Mon Sep 17 00:00:00 2001 From: Abduqodiri Qurbonzoda Date: Thu, 11 Apr 2019 02:57:33 +0300 Subject: [PATCH] Implement String to/from CharArray conversion (KT-29265) --- libraries/stdlib/common/build.gradle | 3 +- libraries/stdlib/common/src/kotlin/TextH.kt | 25 +++++++++++ libraries/stdlib/jdk7/build.gradle | 7 ++- libraries/stdlib/jdk8/build.gradle | 7 ++- libraries/stdlib/js-ir/build.gradle | 3 +- libraries/stdlib/js-v1/build.gradle | 3 +- libraries/stdlib/js/src/kotlin/text/string.kt | 38 ++++++++++++++++ libraries/stdlib/jvm/build.gradle | 3 +- .../stdlib/jvm/src/kotlin/text/StringsJVM.kt | 36 ++++++++++++++- .../src/kotlin/collections/AbstractList.kt | 9 ++++ libraries/stdlib/test/text/StringTest.kt | 45 ++++++++++++++++--- .../kotlin-stdlib-runtime-merged.txt | 4 ++ 12 files changed, 169 insertions(+), 14 deletions(-) diff --git a/libraries/stdlib/common/build.gradle b/libraries/stdlib/common/build.gradle index 1505b5d28ef..76841ecff7b 100644 --- a/libraries/stdlib/common/build.gradle +++ b/libraries/stdlib/common/build.gradle @@ -80,7 +80,8 @@ compileCoroutinesExperimentalKotlinCommon { compileTestKotlinCommon { kotlinOptions { freeCompilerArgs += [ - "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes" + "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", + "-Xuse-experimental=kotlin.ExperimentalStdlibApi" ] } } diff --git a/libraries/stdlib/common/src/kotlin/TextH.kt b/libraries/stdlib/common/src/kotlin/TextH.kt index 986ca04f0b0..d787fe943a9 100644 --- a/libraries/stdlib/common/src/kotlin/TextH.kt +++ b/libraries/stdlib/common/src/kotlin/TextH.kt @@ -109,6 +109,31 @@ public expect fun String(chars: CharArray): String @SinceKotlin("1.2") public expect fun String(chars: CharArray, offset: Int, length: Int): String +/** + * Concatenates characters in this [CharArray] or its subrange into a String. + * + * @param startIndex the beginning (inclusive) of the subrange of characters, 0 by default. + * @param endIndex the end (exclusive) of the subrange of characters, size of this array by default. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the size of this array. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public expect fun CharArray.concatToString(startIndex: Int = 0, endIndex: Int = this.size): String + +/** + * Returns a [CharArray] containing characters of this string or its substring. + * + * @param startIndex the beginning (inclusive) of the substring, 0 by default. + * @param endIndex the end (exclusive) of the substring, length of this string by default. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the length of this string. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + */ +@SinceKotlin("1.3") +@ExperimentalStdlibApi +public expect fun String.toCharArray(startIndex: Int = 0, endIndex: Int = this.length): CharArray internal expect fun String.nativeIndexOf(str: String, fromIndex: Int): Int internal expect fun String.nativeLastIndexOf(str: String, fromIndex: Int): Int diff --git a/libraries/stdlib/jdk7/build.gradle b/libraries/stdlib/jdk7/build.gradle index 375bf912581..bbf0afa632b 100644 --- a/libraries/stdlib/jdk7/build.gradle +++ b/libraries/stdlib/jdk7/build.gradle @@ -91,7 +91,12 @@ compileKotlin { } compileTestKotlin { - kotlinOptions.freeCompilerArgs = ["-Xallow-kotlin-package", "-Xmulti-platform", "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes"] + kotlinOptions.freeCompilerArgs = [ + "-Xallow-kotlin-package", + "-Xmulti-platform", + "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", + "-Xuse-experimental=kotlin.ExperimentalStdlibApi" + ] } compileJava9Sources(project, 'kotlin.stdlib.jdk7') diff --git a/libraries/stdlib/jdk8/build.gradle b/libraries/stdlib/jdk8/build.gradle index ffce52a0946..b5f59544fc6 100644 --- a/libraries/stdlib/jdk8/build.gradle +++ b/libraries/stdlib/jdk8/build.gradle @@ -85,7 +85,12 @@ compileKotlin { } compileTestKotlin { - kotlinOptions.freeCompilerArgs = ["-Xallow-kotlin-package", "-Xmulti-platform", "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes"] + kotlinOptions.freeCompilerArgs = [ + "-Xallow-kotlin-package", + "-Xmulti-platform", + "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", + "-Xuse-experimental=kotlin.ExperimentalStdlibApi" + ] } compileJava9Sources(project, 'kotlin.stdlib.jdk8') diff --git a/libraries/stdlib/js-ir/build.gradle b/libraries/stdlib/js-ir/build.gradle index 17a13f5d592..bb2328331e6 100644 --- a/libraries/stdlib/js-ir/build.gradle +++ b/libraries/stdlib/js-ir/build.gradle @@ -135,7 +135,8 @@ compileTestKotlin2Js { "-Xir", "-Xir-produce-only=js", "-verbose", - "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes" + "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", + "-Xuse-experimental=kotlin.ExperimentalStdlibApi" ] } } diff --git a/libraries/stdlib/js-v1/build.gradle b/libraries/stdlib/js-v1/build.gradle index ca93cfcc6fe..527b171b713 100644 --- a/libraries/stdlib/js-v1/build.gradle +++ b/libraries/stdlib/js-v1/build.gradle @@ -193,7 +193,8 @@ compileTestKotlin2Js { kotlinOptions { moduleKind = "umd" freeCompilerArgs += [ - "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes" + "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", + "-Xuse-experimental=kotlin.ExperimentalStdlibApi" ] } } diff --git a/libraries/stdlib/js/src/kotlin/text/string.kt b/libraries/stdlib/js/src/kotlin/text/string.kt index cb7e4d85168..42c12933951 100644 --- a/libraries/stdlib/js/src/kotlin/text/string.kt +++ b/libraries/stdlib/js/src/kotlin/text/string.kt @@ -36,6 +36,44 @@ public actual fun String(chars: CharArray, offset: Int, length: Int): String { return result } +/** + * Concatenates characters in this [CharArray] or its subrange into a String. + * + * @param startIndex the beginning (inclusive) of the subrange of characters, 0 by default. + * @param endIndex the end (exclusive) of the subrange of characters, size of this array by default. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the size of this array. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + */ +@SinceKotlin("1.3") +@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") +@ExperimentalStdlibApi +public actual fun CharArray.concatToString(startIndex: Int = 0, endIndex: Int = this.size): String { + AbstractList.checkBoundsIndexes(startIndex, endIndex, this.size) + var result = "" + for (index in startIndex until endIndex) { + result += this[index] + } + return result +} + +/** + * Returns a [CharArray] containing characters of this string or its substring. + * + * @param startIndex the beginning (inclusive) of the substring, 0 by default. + * @param endIndex the end (exclusive) of the substring, length of this string by default. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the length of this string. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + */ +@SinceKotlin("1.3") +@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") +@ExperimentalStdlibApi +public actual fun String.toCharArray(startIndex: Int = 0, endIndex: Int = this.length): CharArray { + AbstractList.checkBoundsIndexes(startIndex, endIndex, length) + return CharArray(endIndex - startIndex) { get(startIndex + it) } +} + /** * Returns a copy of this string converted to upper case using the rules of the default locale. * diff --git a/libraries/stdlib/jvm/build.gradle b/libraries/stdlib/jvm/build.gradle index 9e97f6fb218..0f0652a2754 100644 --- a/libraries/stdlib/jvm/build.gradle +++ b/libraries/stdlib/jvm/build.gradle @@ -249,7 +249,8 @@ check.dependsOn(coroutinesExperimentalMigrationTest) compileTestKotlin { kotlinOptions { freeCompilerArgs += [ - "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes" + "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", + "-Xuse-experimental=kotlin.ExperimentalStdlibApi" ] } } diff --git a/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt b/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt index 4dc6447b10b..41d66215588 100644 --- a/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt +++ b/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt @@ -10,7 +10,7 @@ package kotlin.text import java.nio.charset.Charset -import java.util.* +import java.util.Locale import java.util.regex.Pattern @@ -108,6 +108,40 @@ public actual inline fun String.toUpperCase(): String = (this as java.lang.Strin @kotlin.internal.InlineOnly public actual inline fun String.toLowerCase(): String = (this as java.lang.String).toLowerCase() +/** + * Concatenates characters in this [CharArray] or its subrange into a String. + * + * @param startIndex the beginning (inclusive) of the subrange of characters, 0 by default. + * @param endIndex the end (exclusive) of the subrange of characters, size of this array by default. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the size of this array. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + */ +@SinceKotlin("1.3") +@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") +@ExperimentalStdlibApi +public actual fun CharArray.concatToString(startIndex: Int = 0, endIndex: Int = this.size): String { + AbstractList.checkBoundsIndexes(startIndex, endIndex, this.size) + return String(this, startIndex, endIndex - startIndex) +} + +/** + * Returns a [CharArray] containing characters of this string or its substring. + * + * @param startIndex the beginning (inclusive) of the substring, 0 by default. + * @param endIndex the end (exclusive) of the substring, length of this string by default. + * + * @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the length of this string. + * @throws IllegalArgumentException if [startIndex] is greater than [endIndex]. + */ +@SinceKotlin("1.3") +@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") +@ExperimentalStdlibApi +public actual fun String.toCharArray(startIndex: Int = 0, endIndex: Int = this.length): CharArray { + AbstractList.checkBoundsIndexes(startIndex, endIndex, length) + return toCharArray(CharArray(endIndex - startIndex), 0, startIndex, endIndex) +} + /** * Returns a new character array containing the characters from this string. */ diff --git a/libraries/stdlib/src/kotlin/collections/AbstractList.kt b/libraries/stdlib/src/kotlin/collections/AbstractList.kt index 7ef07a94fd6..818abfeff20 100644 --- a/libraries/stdlib/src/kotlin/collections/AbstractList.kt +++ b/libraries/stdlib/src/kotlin/collections/AbstractList.kt @@ -123,6 +123,15 @@ public abstract class AbstractList protected constructor() : AbstractColl } } + internal fun checkBoundsIndexes(startIndex: Int, endIndex: Int, size: Int) { + if (startIndex < 0 || endIndex > size) { + throw IndexOutOfBoundsException("startIndex: $startIndex, endIndex: $endIndex, size: $size") + } + if (startIndex > endIndex) { + throw IllegalArgumentException("startIndex: $startIndex > endIndex: $endIndex") + } + } + internal fun orderedHashCode(c: Collection<*>): Int { var hashCode = 1 for (e in c) { diff --git a/libraries/stdlib/test/text/StringTest.kt b/libraries/stdlib/test/text/StringTest.kt index 8b6df37e543..f05639ae48e 100644 --- a/libraries/stdlib/test/text/StringTest.kt +++ b/libraries/stdlib/test/text/StringTest.kt @@ -45,36 +45,52 @@ fun Char.isAsciiUpperCase() = this in 'A'..'Z' class StringTest { + private fun testStringFromChars(expected: String, chars: CharArray, offset: Int, length: Int) { + assertEquals(expected, String(chars, offset, length)) + assertEquals(expected, chars.concatToString(startIndex = offset, endIndex = offset + length)) + } + + private fun testStringFromChars(expected: String, chars: CharArray) { + assertEquals(expected, String(chars)) + assertEquals(expected, chars.concatToString()) + } + @Test fun stringFromCharArrayFullSlice() { val chars: CharArray = charArrayOf('K', 'o', 't', 'l', 'i', 'n') - assertEquals("Kotlin", String(chars, 0, chars.size)) + testStringFromChars("Kotlin", chars, 0, chars.size) } @Test fun stringFromCharArraySlice() { val chars: CharArray = charArrayOf('K', 'o', 't', 'l', 'i', 'n', ' ', 'r', 'u', 'l', 'e', 's') - assertEquals("rule", String(chars, 7, 4)) + testStringFromChars("rule", chars, 7, 4) val longChars = CharArray(200_000) { 'k' } - val longString = String(longChars, 1000, 190_000) + val longString = String(longChars, offset = 1000, length = 190_000) + val longConcatString = longChars.concatToString(startIndex = 1000, endIndex = 191_000) assertEquals(190_000, longString.length) + assertEquals(190_000, longConcatString.length) assertTrue(longString.all { it == 'k' }) + assertTrue(longConcatString.all { it == 'k' }) } @Test fun stringFromCharArray() { val chars: CharArray = charArrayOf('K', 'o', 't', 'l', 'i', 'n') - assertEquals("Kotlin", String(chars)) + testStringFromChars("Kotlin", chars) val longChars = CharArray(200_000) { 'k' } val longString = String(longChars) + val longConcatString = longChars.concatToString() assertEquals(200_000, longString.length) + assertEquals(200_000, longConcatString.length) assertTrue(longString.all { it == 'k' }) + assertTrue(longConcatString.all { it == 'k' }) } @Test fun stringFromCharArrayUnicodeSurrogatePairs() { val chars: CharArray = charArrayOf('Ц', '月', '語', '\u016C', '\u138D', '\uD83C', '\uDC3A') - assertEquals("Ц月語Ŭᎍ🀺", String(chars)) - assertEquals("月", String(chars, 1, 1)) - assertEquals("Ŭᎍ🀺", String(chars, 3, 4)) + testStringFromChars("Ц月語Ŭᎍ🀺", chars) + testStringFromChars("月", chars, 1, 1) + testStringFromChars("Ŭᎍ🀺", chars, 3, 4) } @Test fun stringFromCharArrayOutOfBounds() { @@ -82,11 +98,26 @@ class StringTest { assertFailsWith { String(chars, -1, 1) } assertFailsWith { String(chars, 1, -1) } assertFailsWith { String(chars, chars.size - 1, 2) } + + assertFailsWith { chars.concatToString(-1, 1) } + assertFailsWith { chars.concatToString(1, -1) } + assertFailsWith { chars.concatToString(chars.size - 1, 2) } } test(CharArray(16) { 'k' }) test(CharArray(160_000) { 'k' }) } + @Test + fun toCharArray() { + val s = "hello" + assertArrayContentEquals(charArrayOf('h', 'e', 'l', 'l', 'o'), s.toCharArray()) + assertArrayContentEquals(charArrayOf('e', 'l'), s.toCharArray(1, 3)) + + assertFailsWith { s.toCharArray(-1) } + assertFailsWith { s.toCharArray(0, 6) } + assertFailsWith { s.toCharArray(3, 1) } + } + @Test fun isEmptyAndBlank() = withOneCharSequenceArg { arg1 -> class Case(val value: String?, val isNull: Boolean = false, val isEmpty: Boolean = false, val isBlank: Boolean = false) diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index e31f9bc45d0..a295b2ea792 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -4906,6 +4906,8 @@ public final class kotlin/text/StringsKt { public static synthetic fun commonSuffixWith$default (Ljava/lang/CharSequence;Ljava/lang/CharSequence;ZILjava/lang/Object;)Ljava/lang/String; public static final fun compareTo (Ljava/lang/String;Ljava/lang/String;Z)I public static synthetic fun compareTo$default (Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)I + public static final fun concatToString ([CII)Ljava/lang/String; + public static synthetic fun concatToString$default ([CIIILjava/lang/Object;)Ljava/lang/String; public static final fun contains (Ljava/lang/CharSequence;CZ)Z public static final fun contains (Ljava/lang/CharSequence;Ljava/lang/CharSequence;Z)Z public static synthetic fun contains$default (Ljava/lang/CharSequence;CZILjava/lang/Object;)Z @@ -5131,6 +5133,8 @@ public final class kotlin/text/StringsKt { public static final fun toBigIntegerOrNull (Ljava/lang/String;I)Ljava/math/BigInteger; public static final fun toByteOrNull (Ljava/lang/String;)Ljava/lang/Byte; public static final fun toByteOrNull (Ljava/lang/String;I)Ljava/lang/Byte; + public static final fun toCharArray (Ljava/lang/String;II)[C + public static synthetic fun toCharArray$default (Ljava/lang/String;IIILjava/lang/Object;)[C public static final fun toCollection (Ljava/lang/CharSequence;Ljava/util/Collection;)Ljava/util/Collection; public static final fun toDoubleOrNull (Ljava/lang/String;)Ljava/lang/Double; public static final fun toFloatOrNull (Ljava/lang/String;)Ljava/lang/Float;