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 ce66209b935..f612e393f88 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/text/Strings.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/text/Strings.kt @@ -63,13 +63,16 @@ public actual fun String.replace(oldChar: Char, newChar: Char, ignoreCase: Boole private external fun String.replace(oldChar: Char, newChar: Char): String private fun String.replaceIgnoreCase(oldChar: Char, newChar: Char): String { - val charArray = CharArray(length) - val oldCharLower = oldChar.lowercaseChar() + val charArray = this.toCharArray() + val oldCharUpper = oldChar.uppercaseChar() + val oldCharLower = oldCharUpper.lowercaseChar() for (index in 0 until length) { val thisChar = this[index] - val thisCharLower = thisChar.lowercaseChar() - charArray[index] = if (thisCharLower == oldCharLower) newChar else thisChar + if (thisChar != oldChar && thisChar.uppercaseChar().let { it != oldCharUpper && it.lowercaseChar() != oldCharLower }) { + continue + } + charArray[index] = newChar } return charArray.concatToString() @@ -186,9 +189,9 @@ private external fun String.unsafeRangeEquals(thisOffset: Int, other: String, ot // Bounds must be checked before calling this method private fun String.unsafeRangeEqualsIgnoreCase(thisOffset: Int, other: String, otherOffset: Int, length: Int): Boolean { for (index in 0 until length) { - val thisCharLower = this[thisOffset + index].lowercaseChar() - val otherCharLower = other[otherOffset + index].lowercaseChar() - if (thisCharLower != otherCharLower) { + val thisCharUpper = this[thisOffset + index].uppercaseChar() + val otherCharUpper = other[otherOffset + index].uppercaseChar() + if (thisCharUpper != otherCharUpper && thisCharUpper.lowercaseChar() != otherCharUpper.lowercaseChar()) { return false } } @@ -429,8 +432,8 @@ internal fun compareToIgnoreCase(thiz: String, other: String): Int { val length = minOf(thiz.length, other.length) for (index in 0 until length) { - val thisLowerChar = thiz[index].lowercaseChar() - val otherLowerChar = other[index].lowercaseChar() + val thisLowerChar = thiz[index].uppercaseChar().lowercaseChar() + val otherLowerChar = other[index].uppercaseChar().lowercaseChar() if (thisLowerChar != otherLowerChar) { return if (thisLowerChar < otherLowerChar) -1 else 1 } diff --git a/libraries/stdlib/js/src/kotlin/text/regex.kt b/libraries/stdlib/js/src/kotlin/text/regex.kt index 03106f15428..c5cd563217b 100644 --- a/libraries/stdlib/js/src/kotlin/text/regex.kt +++ b/libraries/stdlib/js/src/kotlin/text/regex.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ @@ -51,7 +51,7 @@ public actual class Regex actual constructor(pattern: String, options: Set = options.toSet() - private val nativePattern: RegExp = RegExp(pattern, options.map { it.value }.joinToString(separator = "") + "g") + private val nativePattern: RegExp = RegExp(pattern, options.joinToString(separator = "", prefix = "gu") { it.value }) /** Indicates whether the regular expression matches the entire [input]. */ public actual infix fun matches(input: CharSequence): Boolean { @@ -200,7 +200,7 @@ public actual class Regex actual constructor(pattern: String, options: Set other == null + !ignoreCase -> this == other + other == null -> false + else -> { + val thisLower = this.lowercase() + val otherLower = other.lowercase() + thisLower == otherLower || (thisLower.uppercase() == otherLower.uppercase()) + } + } @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") @@ -125,16 +129,16 @@ public actual fun CharSequence.repeat(n: Int): String { @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun String.replace(oldValue: String, newValue: String, ignoreCase: Boolean = false): String = - nativeReplace(RegExp(Regex.escape(oldValue), if (ignoreCase) "gi" else "g"), Regex.escapeReplacement(newValue)) + nativeReplace(RegExp(Regex.escape(oldValue), if (ignoreCase) "gui" else "gu"), Regex.escapeReplacement(newValue)) @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun String.replace(oldChar: Char, newChar: Char, ignoreCase: Boolean = false): String = - nativeReplace(RegExp(Regex.escape(oldChar.toString()), if (ignoreCase) "gi" else "g"), newChar.toString()) + nativeReplace(RegExp(Regex.escape(oldChar.toString()), if (ignoreCase) "gui" else "gu"), newChar.toString()) @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun String.replaceFirst(oldValue: String, newValue: String, ignoreCase: Boolean = false): String = - nativeReplace(RegExp(Regex.escape(oldValue), if (ignoreCase) "i" else ""), Regex.escapeReplacement(newValue)) + nativeReplace(RegExp(Regex.escape(oldValue), if (ignoreCase) "ui" else "u"), Regex.escapeReplacement(newValue)) @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun String.replaceFirst(oldChar: Char, newChar: Char, ignoreCase: Boolean = false): String = - nativeReplace(RegExp(Regex.escape(oldChar.toString()), if (ignoreCase) "i" else ""), newChar.toString()) + nativeReplace(RegExp(Regex.escape(oldChar.toString()), if (ignoreCase) "ui" else "u"), newChar.toString()) diff --git a/libraries/stdlib/src/kotlin/text/Char.kt b/libraries/stdlib/src/kotlin/text/Char.kt index 7d230e97bf6..5510b21b355 100644 --- a/libraries/stdlib/src/kotlin/text/Char.kt +++ b/libraries/stdlib/src/kotlin/text/Char.kt @@ -230,9 +230,10 @@ public fun Char.equals(other: Char, ignoreCase: Boolean = false): Boolean { if (this == other) return true if (!ignoreCase) return false - if (this.uppercaseChar() == other.uppercaseChar()) return true - if (this.lowercaseChar() == other.lowercaseChar()) return true - return false + val thisUpper = this.uppercaseChar() + val otherUpper = other.uppercaseChar() + + return thisUpper == otherUpper || thisUpper.lowercaseChar() == otherUpper.lowercaseChar() } /** diff --git a/libraries/stdlib/test/text/CharTest.kt b/libraries/stdlib/test/text/CharTest.kt index 4ab09cc5d8e..e927b2b50f9 100644 --- a/libraries/stdlib/test/text/CharTest.kt +++ b/libraries/stdlib/test/text/CharTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ @@ -9,6 +9,16 @@ import kotlin.test.* class CharTest { + companion object { + val equalIgnoreCaseGroups = listOf( + "Aa", "Zz", "üÜ", "öÖ", "äÄ", + "KkK", "Ssſ", "µΜμ", "ÅåÅ", + "DŽDždž", "LJLjlj", "NJNjnj", "DZDzdz", "ͅΙιι", "Ββϐ", "Εεϵ", + "Κκϰ", "Ππϖ", "Ρρϱ", "Σςσ", "Φφϕ", "ΩωΩ", "Ṡṡẛ", + "Θθϑϴ", "Iiİı", + ) + } + @Test fun charFromIntCode() { val codes = listOf(48, 65, 122, 946, '+'.code, 'Ö'.code, 0xFFFC) @@ -145,6 +155,20 @@ class CharTest { testFails(100, radix = 110) } + @Test + fun equalsIgnoreCase() { + val nonEqual = equalIgnoreCaseGroups.flatMap { allEqualChars -> + allEqualChars.flatMap { c1 -> allEqualChars.mapNotNull { c2 -> + if (!c1.equals(c2, ignoreCase = true)) "$c1 != $c2" else null + } + } + } + if (nonEqual.isNotEmpty()) { + fail("Expected chars to be equal ignoring case:\n${nonEqual.joinToString("\n")}") + } + } + + private fun charToCategory() = mapOf( '\u0378' to "Cn", 'A' to "Lu", // \u0041 diff --git a/libraries/stdlib/test/text/StringTest.kt b/libraries/stdlib/test/text/StringTest.kt index 3b96b99b1f1..26a1e463473 100644 --- a/libraries/stdlib/test/text/StringTest.kt +++ b/libraries/stdlib/test/text/StringTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ @@ -847,7 +847,7 @@ class StringTest { val result = v1.compareTo(v2, ignoreCase = ignoreCase).sign assertEquals(expectedResult, result, "Comparing '$v1' with '$v2', ignoreCase = $ignoreCase") if (expectedResult == 0) - assertTrue(v1.equals(v2, ignoreCase = ignoreCase)) + assertTrue(v1.equals(v2, ignoreCase = ignoreCase), "'$v1'=='$v2' should be consistent with ordering, ignoreCase = $ignoreCase") if (!ignoreCase) assertEquals(v1.compareTo(v2).sign, result) } @@ -886,6 +886,16 @@ class StringTest { assertCompareResult(EQ, item1, item2, ignoreCase = true) } } + + CharTest.equalIgnoreCaseGroups + .filterNot { "i" in it } + .forEach { equalGroup -> + for (char1 in equalGroup) { + for (char2 in equalGroup) { + assertCompareResult(EQ, char1.toString(), char2.toString(), ignoreCase = true) + } + } + } } @@ -915,12 +925,14 @@ class StringTest { assertEquals(expectAllReplaced, chars.replace(c, '_', ignoreCase = true), "$message, ignoreCase") assertEquals(expectOneReplaced, chars.replace(c.toString(), "_"), "$message, as string") assertEquals(expectAllReplaced, chars.replace(c.toString(), "_", ignoreCase = true), "$message, as string, ignoreCase") + assertEquals(expectOneReplaced, chars.replace(Regex(Regex.escape(c.toString())), "_"), "$message, as regex") + assertEquals(expectAllReplaced, chars.replace(Regex(Regex.escape(c.toString()), RegexOption.IGNORE_CASE), "_"), "$message, as regex, ignoreCase") } } - testIgnoreCase("üÜ") - testIgnoreCase("öÖ") - testIgnoreCase("äÄ") + CharTest.equalIgnoreCaseGroups + .filterNot { "i" in it } // not supported by JS + .forEach { testIgnoreCase(it) } } @Test fun replaceFirst() {