diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/text/Strings.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/text/Strings.kt index 5bac0d8924a..bd0b8d2a296 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/text/Strings.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/text/Strings.kt @@ -35,6 +35,9 @@ internal actual external fun String.nativeLastIndexOf(str: String, fromIndex: In /** * Returns `true` if this string is equal to [other], optionally ignoring character case. * + * Two strings are considered to be equal if they have the same length and the same character at the same index. + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. + * * @param ignoreCase `true` to ignore character case when comparing strings. By default `false`. */ @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") @@ -475,6 +478,11 @@ internal fun compareToIgnoreCase(thiz: String, other: String): Int { 1 } +/** + * Compares two strings lexicographically, optionally ignoring case differences. + * + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. + */ @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun String.compareTo(other: String, ignoreCase: Boolean = false): Int { return if (!ignoreCase) this.compareTo(other) diff --git a/libraries/stdlib/common/src/kotlin/TextH.kt b/libraries/stdlib/common/src/kotlin/TextH.kt index f236eacb38b..94d3c4c6569 100644 --- a/libraries/stdlib/common/src/kotlin/TextH.kt +++ b/libraries/stdlib/common/src/kotlin/TextH.kt @@ -270,12 +270,17 @@ expect fun String.replaceFirst(oldValue: String, newValue: String, ignoreCase: B /** * Returns `true` if this string is equal to [other], optionally ignoring character case. * + * Two strings are considered to be equal if they have the same length and the same character at the same index. + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. + * * @param ignoreCase `true` to ignore character case when comparing strings. By default `false`. */ expect fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean /** * Compares two strings lexicographically, optionally ignoring case differences. + * + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. */ @SinceKotlin("1.2") expect fun String.compareTo(other: String, ignoreCase: Boolean = false): Int diff --git a/libraries/stdlib/js/src/kotlin/text/string.kt b/libraries/stdlib/js/src/kotlin/text/string.kt index 5026312024e..aa8c7c0d06b 100644 --- a/libraries/stdlib/js/src/kotlin/text/string.kt +++ b/libraries/stdlib/js/src/kotlin/text/string.kt @@ -244,6 +244,11 @@ public inline fun String.match(regex: String): Array? = asDynamic().matc @kotlin.internal.InlineOnly internal inline fun String.nativeReplace(pattern: RegExp, replacement: String): String = asDynamic().replace(pattern, replacement) +/** + * Compares two strings lexicographically, optionally ignoring case differences. + * + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. + */ @SinceKotlin("1.2") @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun String.compareTo(other: String, ignoreCase: Boolean = false): Int { @@ -252,24 +257,23 @@ public actual fun String.compareTo(other: String, ignoreCase: Boolean = false): val n2 = other.length val min = minOf(n1, n2) if (min == 0) return n1 - n2 - var start = 0 - while (true) { - val end = minOf(start + 16, min) - var s1 = this.substring(start, end) - var s2 = other.substring(start, end) - if (s1 != s2) { - s1 = s1.uppercase() - s2 = s2.uppercase() - if (s1 != s2) { - s1 = s1.lowercase() - s2 = s2.lowercase() - if (s1 != s2) { - return s1.compareTo(s2) + for (index in 0 until min) { + var thisChar = this[index] + var otherChar = other[index] + + if (thisChar != otherChar) { + thisChar = thisChar.uppercaseChar() + otherChar = otherChar.uppercaseChar() + + if (thisChar != otherChar) { + thisChar = thisChar.lowercaseChar() + otherChar = otherChar.lowercaseChar() + + if (thisChar != otherChar) { + return thisChar.compareTo(otherChar) } } } - if (end == min) break - start = end } return n1 - n2 } else { diff --git a/libraries/stdlib/js/src/kotlin/text/stringsCode.kt b/libraries/stdlib/js/src/kotlin/text/stringsCode.kt index ffcfc8fe0df..4ee4535738e 100644 --- a/libraries/stdlib/js/src/kotlin/text/stringsCode.kt +++ b/libraries/stdlib/js/src/kotlin/text/stringsCode.kt @@ -61,19 +61,33 @@ public fun String.matches(regex: String): Boolean { */ public actual fun CharSequence.isBlank(): Boolean = length == 0 || indices.all { this[it].isWhitespace() } +/** + * Returns `true` if this string is equal to [other], optionally ignoring character case. + * + * Two strings are considered to be equal if they have the same length and the same character at the same index. + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. + * + * @param ignoreCase `true` to ignore character case when comparing strings. By default `false`. + */ @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") -public actual fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean = - when { - this == null -> other == null - !ignoreCase -> this == other - other == null -> false - else -> { - val thisLower = this.lowercase() - val otherLower = other.lowercase() - thisLower == otherLower || (thisLower.uppercase() == otherLower.uppercase()) +public actual fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean { + if (this == null) return other == null + if (other == null) return false + if (!ignoreCase) return this == other + + if (this.length != other.length) return false + + for (index in 0 until this.length) { + val thisChar = this[index] + val otherChar = other[index] + if (!thisChar.equals(otherChar, ignoreCase)) { + return false } } + return true +} + @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun CharSequence.regionMatches(thisOffset: Int, other: CharSequence, otherOffset: Int, length: Int, ignoreCase: Boolean = false): Boolean = diff --git a/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt b/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt index 173a4a5f84a..81abef23cc6 100644 --- a/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt +++ b/libraries/stdlib/jvm/src/kotlin/text/StringsJVM.kt @@ -44,6 +44,9 @@ internal actual inline fun String.nativeLastIndexOf(str: String, fromIndex: Int) /** * Returns `true` if this string is equal to [other], optionally ignoring character case. * + * Two strings are considered to be equal if they have the same length and the same character at the same index. + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. + * * @param ignoreCase `true` to ignore character case when comparing strings. By default `false`. */ @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") @@ -540,6 +543,8 @@ public inline fun String.codePointCount(beginIndex: Int, endIndex: Int): Int = /** * Compares two strings lexicographically, optionally ignoring case differences. + * + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. */ @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun String.compareTo(other: String, ignoreCase: Boolean = false): Int { diff --git a/libraries/stdlib/src/kotlin/text/Char.kt b/libraries/stdlib/src/kotlin/text/Char.kt index d2553a17b0f..88a5338b97a 100644 --- a/libraries/stdlib/src/kotlin/text/Char.kt +++ b/libraries/stdlib/src/kotlin/text/Char.kt @@ -225,13 +225,9 @@ public inline operator fun Char.plus(other: String): String = this.toString() + /** * Returns `true` if this character is equal to the [other] character, optionally ignoring character case. * + * Two characters are considered equal ignoring case if `Char.uppercaseChar().lowercaseChar()` on each character produces the same result. + * * @param ignoreCase `true` to ignore character case when comparing characters. By default `false`. - * - * Two characters are considered the same ignoring case if at least one of the following is `true`: - * - The two characters are the same (as compared by the == operator) - * - Applying the method [uppercaseChar] to each character produces the same result - * - Applying the method [lowercaseChar] to each character produces the same result - * * @sample samples.text.Chars.equals */ public fun Char.equals(other: Char, ignoreCase: Boolean = false): Boolean { diff --git a/libraries/stdlib/test/text/StringTest.kt b/libraries/stdlib/test/text/StringTest.kt index a94ec6b95cb..5e8658f2fb6 100644 --- a/libraries/stdlib/test/text/StringTest.kt +++ b/libraries/stdlib/test/text/StringTest.kt @@ -841,6 +841,11 @@ class StringTest { assertFalse("sample".equals(null, ignoreCase = true)) assertTrue(null.equals(null, ignoreCase = true)) assertTrue(null.equals(null, ignoreCase = false)) + + // Tests that characters are compared one by one + val s1 = "\uFB00" // uppercase() == "FF" + val s2 = "\u0066\u0066" // "ff" + assertFalse(s1.equals(s2, ignoreCase = true)) } @Test fun compareToIgnoreCase() { @@ -898,6 +903,11 @@ class StringTest { } } } + + // Tests that characters are compared one by one + val s1 = "\uFB00" // uppercase().lowercase() == "ff" + val s2 = "\u0067" // "g" + assertCompareResult(GT, s1, s2, ignoreCase = true) } diff --git a/libraries/stdlib/wasm/src/kotlin/Text.kt b/libraries/stdlib/wasm/src/kotlin/Text.kt index d89f4619979..d96889eccef 100644 --- a/libraries/stdlib/wasm/src/kotlin/Text.kt +++ b/libraries/stdlib/wasm/src/kotlin/Text.kt @@ -429,12 +429,17 @@ actual fun String.replaceFirst(oldValue: String, newValue: String, ignoreCase: B /** * Returns `true` if this string is equal to [other], optionally ignoring character case. * + * Two strings are considered to be equal if they have the same length and the same character at the same index. + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. + * * @param ignoreCase `true` to ignore character case when comparing strings. By default `false`. */ actual fun String?.equals(other: String?, ignoreCase: Boolean): Boolean = TODO("Wasm stdlib: Text") /** * Compares two strings lexicographically, optionally ignoring case differences. + * + * If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared. */ @SinceKotlin("1.2") actual fun String.compareTo(other: String, ignoreCase: Boolean): Int = TODO("Wasm stdlib: Text")