From ed57bcb3b1c407635ae2e6072dfb0633df1314bc Mon Sep 17 00:00:00 2001 From: Abduqodiri Qurbonzoda Date: Mon, 21 Sep 2020 02:21:23 +0300 Subject: [PATCH] Commonize and generalize JVM-only String.contentEquals #KT-42840 --- libraries/stdlib/api/js-v1/kotlin.text.kt | 6 +++ libraries/stdlib/api/js/kotlin.text.kt | 6 +++ libraries/stdlib/js/src/kotlin/text/string.kt | 24 +++++++++ .../stdlib/jvm/src/kotlin/text/StringsJVM.kt | 39 +++++++++++++- .../samples/test/samples/text/strings.kt | 13 +++++ libraries/stdlib/src/kotlin/text/Strings.kt | 53 +++++++++++++++++++ libraries/stdlib/test/text/StringTest.kt | 38 +++++++++++++ libraries/stdlib/wasm/src/kotlin/Text.kt | 19 +++++++ .../kotlin-stdlib-runtime-merged.txt | 2 + 9 files changed, 198 insertions(+), 2 deletions(-) diff --git a/libraries/stdlib/api/js-v1/kotlin.text.kt b/libraries/stdlib/api/js-v1/kotlin.text.kt index 96e9a2687dc..eb00efa3bbf 100644 --- a/libraries/stdlib/api/js-v1/kotlin.text.kt +++ b/libraries/stdlib/api/js-v1/kotlin.text.kt @@ -162,6 +162,12 @@ public operator fun kotlin.CharSequence.contains(other: kotlin.CharSequence, ign @kotlin.internal.InlineOnly public inline operator fun kotlin.CharSequence.contains(regex: kotlin.text.Regex): kotlin.Boolean +@kotlin.SinceKotlin(version = "1.5") +public infix fun kotlin.CharSequence?.contentEquals(other: kotlin.CharSequence?): kotlin.Boolean + +@kotlin.SinceKotlin(version = "1.5") +public fun kotlin.CharSequence?.contentEquals(other: kotlin.CharSequence?, ignoreCase: kotlin.Boolean): kotlin.Boolean + @kotlin.internal.InlineOnly public inline fun kotlin.CharSequence.count(): kotlin.Int diff --git a/libraries/stdlib/api/js/kotlin.text.kt b/libraries/stdlib/api/js/kotlin.text.kt index 96e9a2687dc..eb00efa3bbf 100644 --- a/libraries/stdlib/api/js/kotlin.text.kt +++ b/libraries/stdlib/api/js/kotlin.text.kt @@ -162,6 +162,12 @@ public operator fun kotlin.CharSequence.contains(other: kotlin.CharSequence, ign @kotlin.internal.InlineOnly public inline operator fun kotlin.CharSequence.contains(regex: kotlin.text.Regex): kotlin.Boolean +@kotlin.SinceKotlin(version = "1.5") +public infix fun kotlin.CharSequence?.contentEquals(other: kotlin.CharSequence?): kotlin.Boolean + +@kotlin.SinceKotlin(version = "1.5") +public fun kotlin.CharSequence?.contentEquals(other: kotlin.CharSequence?, ignoreCase: kotlin.Boolean): kotlin.Boolean + @kotlin.internal.InlineOnly public inline fun kotlin.CharSequence.count(): kotlin.Int diff --git a/libraries/stdlib/js/src/kotlin/text/string.kt b/libraries/stdlib/js/src/kotlin/text/string.kt index 80ca2d47ae5..a9d0b63ea8e 100644 --- a/libraries/stdlib/js/src/kotlin/text/string.kt +++ b/libraries/stdlib/js/src/kotlin/text/string.kt @@ -268,6 +268,30 @@ public actual fun String.compareTo(other: String, ignoreCase: Boolean = false): } } +/** + * Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], + * i.e. both char sequences contain the same number of the same characters in the same order. + * + * @sample samples.text.Strings.contentEquals + */ +@SinceKotlin("1.5") +public actual infix fun CharSequence?.contentEquals(other: CharSequence?): Boolean = contentEqualsImpl(other) + +/** + * Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], optionally ignoring case difference. + * + * @param ignoreCase `true` to ignore character case when comparing contents. + * + * @sample samples.text.Strings.contentEquals + */ +@SinceKotlin("1.5") +public actual fun CharSequence?.contentEquals(other: CharSequence?, ignoreCase: Boolean): Boolean { + return if (ignoreCase) + this.contentEqualsIgnoreCaseImpl(other) + else + this.contentEqualsImpl(other) +} + private val STRING_CASE_INSENSITIVE_ORDER = Comparator { a, b -> a.compareTo(b, ignoreCase = true) } diff --git a/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt b/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt index 65cd26b38a4..2e4c0faaaf0 100644 --- a/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt +++ b/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt @@ -544,8 +544,8 @@ public actual fun String.compareTo(other: String, ignoreCase: Boolean = false): /** * Returns `true` if this string is equal to the contents of the specified [CharSequence], `false` otherwise. * - * Unlike the overload that accepts an argument of type [StringBuffer], - * this function does not compare this string and the specified [CharSequence] in a synchronized block. + * Note that if the [CharSequence] argument is a [StringBuffer] then the comparison may be performed in a synchronized block + * that acquires that [StringBuffer]'s monitor. */ @kotlin.internal.InlineOnly public inline fun String.contentEquals(charSequence: CharSequence): Boolean = (this as java.lang.String).contentEquals(charSequence) @@ -559,6 +559,41 @@ public inline fun String.contentEquals(charSequence: CharSequence): Boolean = (t @kotlin.internal.InlineOnly public inline fun String.contentEquals(stringBuilder: StringBuffer): Boolean = (this as java.lang.String).contentEquals(stringBuilder) +/** + * Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], + * i.e. both char sequences contain the same number of the same characters in the same order. + * + * If this [CharSequence] is a [String] and [other] is not `null` + * then this function behaves the same as [String.contentEquals]. + * + * @sample samples.text.Strings.contentEquals + */ +@SinceKotlin("1.5") +public actual infix fun CharSequence?.contentEquals(other: CharSequence?): Boolean { + return if (this is String && other != null) + contentEquals(other) + else + contentEqualsImpl(other) +} + +/** + * Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], optionally ignoring case difference. + * + * If this [CharSequence] is a [String], [other] is not `null` and [ignoreCase] is `false` + * then this function behaves the same as [String.contentEquals]. + * + * @param ignoreCase `true` to ignore character case when comparing contents. + * + * @sample samples.text.Strings.contentEquals + */ +@SinceKotlin("1.5") +public actual fun CharSequence?.contentEquals(other: CharSequence?, ignoreCase: Boolean): Boolean { + return if (ignoreCase) + contentEqualsIgnoreCaseImpl(other) + else + contentEquals(other) +} + /** * Returns a canonical representation for this string object. */ diff --git a/libraries/stdlib/samples/test/samples/text/strings.kt b/libraries/stdlib/samples/test/samples/text/strings.kt index 70379dfc58e..f4082610e2b 100644 --- a/libraries/stdlib/samples/test/samples/text/strings.kt +++ b/libraries/stdlib/samples/test/samples/text/strings.kt @@ -454,4 +454,17 @@ class Strings { assertPrints(inputString0.replace('s', 'z'), "Mizzizzippi") assertPrints(inputString1.replace("data", "information"), "Insufficient information for meaningful answer.") } + + @Sample + fun contentEquals() { + val stringBuilder = StringBuilder() + stringBuilder.append("Kot").append("lin") + assertPrints(stringBuilder, "Kotlin") + assertTrue(stringBuilder contentEquals "Kotlin") + + stringBuilder.setCharAt(0, 'k') + assertPrints(stringBuilder, "kotlin") + assertFalse("Kotlin".contentEquals(stringBuilder)) + assertTrue("Kotlin".contentEquals(stringBuilder, ignoreCase = true)) + } } diff --git a/libraries/stdlib/src/kotlin/text/Strings.kt b/libraries/stdlib/src/kotlin/text/Strings.kt index a0d00a730e3..9bb4f58871a 100644 --- a/libraries/stdlib/src/kotlin/text/Strings.kt +++ b/libraries/stdlib/src/kotlin/text/Strings.kt @@ -1378,3 +1378,56 @@ public fun CharSequence.lineSequence(): Sequence = splitToSequence("\r\n * The lines returned do not include terminating line separators. */ public fun CharSequence.lines(): List = lineSequence().toList() + +/** + * Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], + * i.e. both char sequences contain the same number of the same characters in the same order. + * + * @sample samples.text.Strings.contentEquals + */ +@SinceKotlin("1.5") +public expect infix fun CharSequence?.contentEquals(other: CharSequence?): Boolean + +/** + * Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], optionally ignoring case difference. + * + * @param ignoreCase `true` to ignore character case when comparing contents. + * + * @sample samples.text.Strings.contentEquals + */ +@SinceKotlin("1.5") +public expect fun CharSequence?.contentEquals(other: CharSequence?, ignoreCase: Boolean): Boolean + +internal fun CharSequence?.contentEqualsIgnoreCaseImpl(other: CharSequence?): Boolean { + if (this is String && other is String) { + return this.equals(other, ignoreCase = true) + } + + if (this === other) return true + if (this == null || other == null || this.length != other.length) return false + + for (i in 0 until length) { + if (!this[i].equals(other[i], ignoreCase = true)) { + return false + } + } + + return true +} + +internal fun CharSequence?.contentEqualsImpl(other: CharSequence?): Boolean { + if (this is String && other is String) { + return this == other + } + + if (this === other) return true + if (this == null || other == null || this.length != other.length) return false + + for (i in 0 until length) { + if (this[i] != other[i]) { + return false + } + } + + return true +} \ No newline at end of file diff --git a/libraries/stdlib/test/text/StringTest.kt b/libraries/stdlib/test/text/StringTest.kt index 06f2dca962f..21b31deca84 100644 --- a/libraries/stdlib/test/text/StringTest.kt +++ b/libraries/stdlib/test/text/StringTest.kt @@ -1741,4 +1741,42 @@ ${" "} // Special Casing assertEquals("\u0399\u0308\u0301\u0053\u0053", "\u0390\u00DF".uppercase()) } + + @Test + fun contentEquals() = withTwoCharSequenceArgs { arg1, arg2 -> + infix fun String?.contentEquals(other: String?): Boolean { + return this?.let { arg1(it) } contentEquals other?.let { arg2(it) } + } + + assertTrue("" contentEquals "") + assertTrue("1" contentEquals "1") + assertFalse("12" contentEquals "1") + assertFalse("1" contentEquals "12") + + assertTrue("sample" contentEquals "sample") + assertFalse("Sample" contentEquals "sample") + assertFalse("sample" contentEquals "Sample") + assertFalse("sample" contentEquals null) + assertFalse(null contentEquals "sample") + assertTrue(null contentEquals null) + } + + @Test + fun contentEqualsIgnoreCase() = withTwoCharSequenceArgs { arg1, arg2 -> + fun String.contentEquals(other: String, ignoreCase: Boolean): Boolean { + return arg1(this).contentEquals(arg2(other), ignoreCase) + } + + assertTrue("".contentEquals("", ignoreCase = false)) + assertTrue("".contentEquals("", ignoreCase = true)) + assertTrue("1".contentEquals("1", ignoreCase = false)) + assertTrue("1".contentEquals("1", ignoreCase = true)) + + assertFalse("sample".contentEquals("Sample", ignoreCase = false)) + assertTrue("sample".contentEquals("Sample", ignoreCase = true)) + assertFalse("sample".contentEquals(null, ignoreCase = false)) + assertFalse("sample".contentEquals(null, ignoreCase = true)) + assertTrue(null.contentEquals(null, ignoreCase = true)) + assertTrue(null.contentEquals(null, ignoreCase = false)) + } } diff --git a/libraries/stdlib/wasm/src/kotlin/Text.kt b/libraries/stdlib/wasm/src/kotlin/Text.kt index 7a38fcee45e..e7c3228203b 100644 --- a/libraries/stdlib/wasm/src/kotlin/Text.kt +++ b/libraries/stdlib/wasm/src/kotlin/Text.kt @@ -425,6 +425,25 @@ actual fun String?.equals(other: String?, ignoreCase: Boolean): Boolean = TODO(" @SinceKotlin("1.2") actual fun String.compareTo(other: String, ignoreCase: Boolean): Int = TODO("Wasm stdlib: Text") +/** + * Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], + * i.e. both char sequences contain the same number of the same characters in the same order. + * + * @sample samples.text.Strings.contentEquals + */ +@SinceKotlin("1.5") +public actual infix fun CharSequence?.contentEquals(other: CharSequence?): Boolean = TODO("Wasm stdlib: Text") + +/** + * Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], optionally ignoring case difference. + * + * @param ignoreCase `true` to ignore character case when comparing contents. + * + * @sample samples.text.Strings.contentEquals + */ +@SinceKotlin("1.5") +public actual fun CharSequence?.contentEquals(other: CharSequence?, ignoreCase: Boolean): Boolean = TODO("Wasm stdlib: Text") + public actual fun String.startsWith(prefix: String, ignoreCase: Boolean): Boolean = TODO("Wasm stdlib: Text") public actual fun String.startsWith(prefix: String, startIndex: Int, ignoreCase: Boolean): Boolean = TODO("Wasm stdlib: Text") 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 2499ffd1cc3..4dfefc54ef1 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 @@ -5261,6 +5261,8 @@ public final class kotlin/text/StringsKt { 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 public static synthetic fun contains$default (Ljava/lang/CharSequence;Ljava/lang/CharSequence;ZILjava/lang/Object;)Z + public static final fun contentEquals (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Z + public static final fun contentEquals (Ljava/lang/CharSequence;Ljava/lang/CharSequence;Z)Z public static final fun count (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)I public static final fun decapitalize (Ljava/lang/String;)Ljava/lang/String; public static final fun decapitalize (Ljava/lang/String;Ljava/util/Locale;)Ljava/lang/String;