diff --git a/libraries/stdlib/src/kotlin/text/Char.kt b/libraries/stdlib/src/kotlin/text/Char.kt index 2983be4740c..0150ef96083 100644 --- a/libraries/stdlib/src/kotlin/text/Char.kt +++ b/libraries/stdlib/src/kotlin/text/Char.kt @@ -39,3 +39,38 @@ public fun Char.equals(other: Char, ignoreCase: Boolean = false): Boolean { if (this.toLowerCase() === other.toLowerCase()) return true return false } + +/** + * Returns `true` if this character is a Unicode high-surrogate code unit (also known as leading-surrogate code unit). + */ +public fun Char.isHighSurrogate(): Boolean = this in MIN_HIGH_SURROGATE..MAX_HIGH_SURROGATE + +/** + * Returns `true` if this character is a Unicode low-surrogate code unit (also known as trailing-surrogate code unit). + */ +public fun Char.isLowSurrogate(): Boolean = this in MIN_LOW_SURROGATE..MAX_LOW_SURROGATE + +// TODO: make these constants public in Char.Companion after resolving #KT-7271 +/** + * The minimum value of a Unicode high-surrogate code unit + */ +private val MIN_HIGH_SURROGATE: Char + get() = '\uD800' + +/** + * The maximum value of a Unicode high-surrogate code unit + */ +private val MAX_HIGH_SURROGATE: Char + get() = '\uDBFF' + +/** + * The minimum value of a Unicode low-surrogate code unit + */ +private val MIN_LOW_SURROGATE: Char + get() = '\uDC00' + +/** + * The maximum value of a Unicode low-surrogate code unit + */ +private val MAX_LOW_SURROGATE: Char + get() = '\uDFFF' \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/text/CharJVM.kt b/libraries/stdlib/src/kotlin/text/CharJVM.kt index d9d6e8a1593..5bbb43a4b7c 100644 --- a/libraries/stdlib/src/kotlin/text/CharJVM.kt +++ b/libraries/stdlib/src/kotlin/text/CharJVM.kt @@ -26,10 +26,6 @@ public fun Char.isDefined(): Boolean = Character.isDefined(this) */ public fun Char.isDigit(): Boolean = Character.isDigit(this) -/** - * Returns `true` if this character is a Unicode high-surrogate code unit (also known as leading-surrogate code unit). - */ -public fun Char.isHighSurrogate(): Boolean = Character.isHighSurrogate(this) /** * Returns `true` if this character (Unicode code point) should be regarded as an ignorable diff --git a/libraries/stdlib/src/kotlin/text/Strings.kt b/libraries/stdlib/src/kotlin/text/Strings.kt index 182d7dbe934..4194d12937e 100644 --- a/libraries/stdlib/src/kotlin/text/Strings.kt +++ b/libraries/stdlib/src/kotlin/text/Strings.kt @@ -187,6 +187,12 @@ public fun String?.orEmpty(): String = this ?: "" public val String.indices: IntRange get() = 0..length() - 1 +/** + * Returns the index of the last character in the String or -1 if the String is empty + */ +public val String.lastIndex: Int + get() = this.length() - 1 + /** * Returns a character at the given index in a [CharSequence]. Allows to use the * index operator for working with character sequences: @@ -197,10 +203,13 @@ public val String.indices: IntRange public fun CharSequence.get(index: Int): Char = this.charAt(index) /** - * Returns the index of the last character in the String or -1 if the String is empty + * Returns `true` if this CharSequence has Unicode surrogate pair at the specified [index] */ -public val String.lastIndex: Int - get() = this.length() - 1 +public fun CharSequence.hasSurrogatePairAt(index: Int): Boolean { + return index in 0..length() - 2 + && this[index].isHighSurrogate() + && this[index + 1].isLowSurrogate() +} /** * Returns a subsequence obtained by taking the characters at the given [indices] in this sequence. @@ -508,6 +517,52 @@ public fun String.endsWith(char: Char, ignoreCase: Boolean): Boolean = // TODO: temporary overload to keep binary compatibility, remove after fixing markdown parser public fun String.endsWith(char: Char): Boolean = endsWith(char, ignoreCase = false) + +// common prefix and suffix + +/** + * Returns the longest string `prefix` such that this string and [other] string both start with this prefix, + * taking care not to split surrogate pairs. + * If this and [other] have no common prefix, returns the empty string. + + * @param ignoreCase `true` to ignore character case when matching a character. By default `false`. + */ +public fun CharSequence.commonPrefixWith(other: CharSequence, ignoreCase: Boolean = false): String { + val shortestLength = Math.min(this.length(), other.length()) + + var i = 0 + while (i < shortestLength && this[i].equals(other[i], ignoreCase = ignoreCase)) { + i++ + } + if (this.hasSurrogatePairAt(i - 1) || other.hasSurrogatePairAt(i - 1)) { + i-- + } + return subSequence(0, i).toString() +} + +/** + * Returns the longest string `suffix` such that this string and [other] string both end with this suffix, + * taking care not to split surrogate pairs. + * If this and [other] have no common suffix, returns the empty string. + + * @param ignoreCase `true` to ignore character case when matching a character. By default `false`. + */ +public fun CharSequence.commonSuffixWith(other: CharSequence, ignoreCase: Boolean = false): String { + val thisLength = this.length() + val otherLength = other.length() + val shortestLength = Math.min(thisLength, otherLength) + + var i = 0 + while (i < shortestLength && this[thisLength - i - 1].equals(other[otherLength - i - 1], ignoreCase = ignoreCase)) { + i++ + } + if (this.hasSurrogatePairAt(thisLength - i - 1) || other.hasSurrogatePairAt(otherLength - i - 1)) { + i--; + } + return subSequence(thisLength - i, thisLength).toString(); +} + + // indexOfAny() private fun String.findAnyOf(chars: CharArray, startIndex: Int, ignoreCase: Boolean, last: Boolean): Pair? { diff --git a/libraries/stdlib/src/kotlin/text/StringsJVM.kt b/libraries/stdlib/src/kotlin/text/StringsJVM.kt index 6191259eb18..fc12d0459d2 100644 --- a/libraries/stdlib/src/kotlin/text/StringsJVM.kt +++ b/libraries/stdlib/src/kotlin/text/StringsJVM.kt @@ -40,11 +40,12 @@ public fun String.equalsIgnoreCase(anotherString: String): Boolean = equals(anot * * @param ignoreCase `true` to ignore character case when comparing strings. By default `false`. */ -public fun String.equals(anotherString: String, ignoreCase: Boolean = false): Boolean = - if (!ignoreCase) +public fun String.equals(anotherString: String, ignoreCase: Boolean = false): Boolean { + return if (!ignoreCase) (this as java.lang.String).equals(anotherString) else (this as java.lang.String).equalsIgnoreCase(anotherString) +} /** * Returns a copy of this string with all occurrences of [oldChar] replaced with [newChar]. diff --git a/libraries/stdlib/test/text/StringTest.kt b/libraries/stdlib/test/text/StringTest.kt index 5a65fed8ecb..7aec0290308 100644 --- a/libraries/stdlib/test/text/StringTest.kt +++ b/libraries/stdlib/test/text/StringTest.kt @@ -70,6 +70,39 @@ class StringTest { assertFalse("".endsWith('a')) } + test fun commonPrefix() { + assertEquals("", "".commonPrefixWith("")) + assertEquals("", "any".commonPrefixWith("")) + assertEquals("", "".commonPrefixWith("any")) + assertEquals("", "some".commonPrefixWith("any")) + + assertEquals("an", "annual".commonPrefixWith("any")) + assertEquals("an", "annual".commonPrefixWith("Any", ignoreCase = true)) + assertEquals("", "annual".commonPrefixWith("Any", ignoreCase = false)) + // surrogate pairs + val dth54 = "\uD83C\uDC58" // domino tile horizontal 5-4 + val dth55 = "\uD83C\uDC59" // domino tile horizontal 5-5 + assertEquals("", dth54.commonPrefixWith(dth55)) + assertEquals(dth54, "$dth54$dth54".commonPrefixWith("$dth54$dth55")) + + } + + test fun commonSuffix() { + assertEquals("", "".commonSuffixWith("")) + assertEquals("", "any".commonSuffixWith("")) + assertEquals("", "".commonSuffixWith("any")) + assertEquals("", "some".commonSuffixWith("any")) + + assertEquals("ly", "yearly".commonSuffixWith("monthly")) + assertEquals("strö", "strö".commonSuffixWith("BISTRÖ", ignoreCase = true)) + assertEquals("", "yearly".commonSuffixWith("HARDLY", ignoreCase = false)) + // surrogate pairs + val dth54 = "\uD83C\uDC58" // domino tile horizontal 5-4 + val kimono = "\uD83D\uDC58" // kimono + assertEquals("", dth54.commonSuffixWith(kimono)) + assertEquals("$dth54", "d$dth54".commonSuffixWith("s$dth54")) + } + test fun capitalize() { assertEquals("A", "A".capitalize()) assertEquals("A", "a".capitalize())