diff --git a/libraries/stdlib/src/kotlin/text/Char.kt b/libraries/stdlib/src/kotlin/text/Char.kt index 939ce2685f9..2983be4740c 100644 --- a/libraries/stdlib/src/kotlin/text/Char.kt +++ b/libraries/stdlib/src/kotlin/text/Char.kt @@ -20,3 +20,22 @@ package kotlin * Concatenates this Char and a String */ public fun Char.plus(string: String) : String = this.toString() + string + +/** + * Returns `true` if this character is equal to the [other] character, optionally ignoring character case. + * + * @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 [toUpperCase] to each character produces the same result + * - Applying the method [toLowerCase] to each character produces the same result + */ +public fun Char.equals(other: Char, ignoreCase: Boolean = false): Boolean { + if (this === other) return true + if (!ignoreCase) return false + + if (this.toUpperCase() === other.toUpperCase()) return true + if (this.toLowerCase() === other.toLowerCase()) return true + return false +} diff --git a/libraries/stdlib/src/kotlin/text/CharJVM.kt b/libraries/stdlib/src/kotlin/text/CharJVM.kt index 9936dd98f71..d9d6e8a1593 100644 --- a/libraries/stdlib/src/kotlin/text/CharJVM.kt +++ b/libraries/stdlib/src/kotlin/text/CharJVM.kt @@ -73,3 +73,13 @@ public fun Char.isUpperCase(): Boolean = Character.isUpperCase(this) * Returns `true` if this character is lower case. */ public fun Char.isLowerCase(): Boolean = Character.isLowerCase(this) + +/** + * Converts this character to uppercase. + */ +public fun Char.toUpperCase(): Char = Character.toUpperCase(this) + +/** + * Converts this character to lowercase. + */ +public fun Char.toLowerCase(): Char = Character.toLowerCase(this) diff --git a/libraries/stdlib/src/kotlin/text/Strings.kt b/libraries/stdlib/src/kotlin/text/Strings.kt index 5afe40048b9..ad13569ea87 100644 --- a/libraries/stdlib/src/kotlin/text/Strings.kt +++ b/libraries/stdlib/src/kotlin/text/Strings.kt @@ -1,5 +1,7 @@ package kotlin +import java.util.NoSuchElementException + /** Returns the string with leading and trailing text matching the given string removed */ deprecated("Use removeEnclosing(text, text) or removePrefix(text).removeSuffix(text)") public fun String.trim(text: String): String = removePrefix(text).removeSuffix(text) @@ -462,3 +464,331 @@ public fun String.replaceBeforeLast(delimiter: String, replacement: String, miss val index = lastIndexOf(delimiter) return if (index == -1) missingDelimiterValue else replaceRange(0, index, replacement) } + +/** + * Returns `true` if this string starts with the specified character. + */ +public fun String.startsWith(char: Char, ignoreCase: Boolean = false): Boolean = + this.length() > 0 && this[0].equals(char, ignoreCase) + +/** + * Returns `true` if this string ends with the specified character. + */ +public fun String.endsWith(char: Char, ignoreCase: Boolean = false): Boolean = + this.length() > 0 && this[lastIndex].equals(char, ignoreCase) + + +// indexOfAny() + + +private fun String.indexOfAny(chars: CharArray, startIndex: Int, ignoreCase: Boolean, last: Boolean): Pair? { + if (!ignoreCase && chars.size() == 1) { + val char = chars.single() + val index = if (!last) nativeIndexOf(char, startIndex) else nativeLastIndexOf(char, startIndex) + return if (index < 0) null else index to char + } + + val indices = if (!last) Math.max(startIndex, 0)..lastIndex else Math.min(startIndex, lastIndex) downTo 0 + for (index in indices) { + val charAtIndex = get(index) + val matchingCharIndex = chars.indexOfFirst { it.equals(charAtIndex, ignoreCase) } + if (matchingCharIndex >= 0) + return index to chars[matchingCharIndex] + } + + return null +} + +/** + * Finds the first occurrence of any of the specified [chars] in this string, starting from the specified [startIndex] and + * optionally ignoring the case. + * + * @param ignoreCase `true` to ignore character case when matching a character. By default `false`. + * @returns A pair of an index of the first occurrence of matched character from [chars] and the character matched or `null` if none of [chars] are found. + * + */ +public fun String.indexOfAny(chars: CharArray, startIndex: Int = 0, ignoreCase: Boolean = false): Pair? = + indexOfAny(chars, startIndex, ignoreCase, last = false) + +/** + * Finds the last occurrence of any of the specified [chars] in this string, starting from the specified [startIndex] and + * optionally ignoring the case. + * + * @param startIndex The index of character to start searching at. The search proceeds backward toward the beginning of the string. + * @param ignoreCase `true` to ignore character case when matching a character. By default `false`. + * @returns A pair of an index of the last occurrence of matched character from [chars] and the character matched or `null` if none of [chars] are found. + * + */ +public fun String.lastIndexOfAny(chars: CharArray, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Pair? = + indexOfAny(chars, startIndex, ignoreCase, last = true) + + + +private fun String.indexOfAny(strings: Collection, startIndex: Int, ignoreCase: Boolean, last: Boolean): Pair? { + if (!ignoreCase && strings.size() == 1) { + val string = strings.single() + val index = if (!last) nativeIndexOf(string, startIndex) else nativeLastIndexOf(string, startIndex) + return if (index < 0) null else index to string + } + + val indices = if (!last) Math.max(startIndex, 0)..lastIndex else Math.min(startIndex, lastIndex) downTo 0 + for (index in indices) { + val matchingString = strings.firstOrNull { it.regionMatches(0, this, index, it.length(), ignoreCase) } + if (matchingString != null) + return index to matchingString + } + + return null +} + +/** + * Finds the first occurrence of any of the specified [strings] in this string, starting from the specified [startIndex] and + * optionally ignoring the case. + * + * @param ignoreCase `true` to ignore character case when matching a string. By default `false`. + * @returns A pair of an index of the first occurrence of matched string from [stromgs] and the string matched or `null` if none of [strings] are found. + * + * To avoid ambiguous results when strings in [strings] have characters in common, this method proceeds from + * the beginning to the end of this string, and finds at each position the first element in [strings] + * that matches this string at that position. + */ +public fun String.indexOfAny(strings: Collection, startIndex: Int = 0, ignoreCase: Boolean = false): Pair? = + indexOfAny(strings, startIndex, ignoreCase, last = false) + +/** + * Finds the last occurrence of any of the specified [strings] in this string, starting from the specified [startIndex] and + * optionally ignoring the case. + * + * @param startIndex The index of character to start searching at. The search proceeds backward toward the beginning of the string. + * @param ignoreCase `true` to ignore character case when matching a string. By default `false`. + * @returns A pair of an index of the last occurrence of matched string from [strings] and the string matched or `null` if none of [strings] are found. + * + * To avoid ambiguous results when strings in [strings] have characters in common, this method proceeds from + * the end toward the beginning of this string, and finds at each position the first element in [strings] + * that matches this string at that position. + */ +public fun String.lastIndexOfAny(strings: Collection, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Pair? = + indexOfAny(strings, startIndex, ignoreCase, last = true) + + +// indexOf + +/** + * Returns the index within this string of the first occurrence of the specified character, starting from the specified [startIndex]. + * + * @param ignoreCase `true` to ignore character case when matching a character. By default `false`. + * @returns An index of the first occurrence of [char] or -1 if none is found. + */ +public fun String.indexOf(char: Char, startIndex: Int = 0, ignoreCase: Boolean = false): Int { + return if (ignoreCase) + indexOfAny(charArray(char), startIndex, ignoreCase)?.first ?: -1 + else + nativeIndexOf(char, startIndex) +} + +/** + * Returns the index within this string of the first occurrence of the specified [string], starting from the specified [startIndex]. + * + * @param ignoreCase `true` to ignore character case when matching a string. By default `false`. + * @returns An index of the first occurrence of [string] or -1 if none is found. + */ +public fun String.indexOf(string: String, startIndex: Int = 0, ignoreCase: Boolean = false): Int { + return if (ignoreCase) + indexOfAny(listOf(string), startIndex, ignoreCase)?.first ?: -1 + else + nativeIndexOf(string, startIndex) +} + +/** + * Returns the index within this string of the last occurrence of the specified character, starting from the specified [startIndex]. + * + * @param startIndex The index of character to start searching at. The search proceeds backward toward the beginning of the string. + * @param ignoreCase `true` to ignore character case when matching a character. By default `false`. + * @returns An index of the first occurrence of [char] or -1 if none is found. + */ +public fun String.lastIndexOf(char: Char, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Int { + return if (ignoreCase) + lastIndexOfAny(charArray(char), startIndex, ignoreCase)?.first ?: -1 + else + nativeLastIndexOf(char, startIndex) +} + +/** + * Returns the index within this string of the last occurrence of the specified [string], starting from the specified [startIndex]. + * + * @param startIndex The index of character to start searching at. The search proceeds backward toward the beginning of the string. + * @param ignoreCase `true` to ignore character case when matching a string. By default `false`. + * @returns An index of the first occurrence of [string] or -1 if none is found. + */ +public fun String.lastIndexOf(string: String, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Int { + return if (ignoreCase) + lastIndexOfAny(listOf(string), startIndex, ignoreCase)?.first ?: -1 + else + nativeLastIndexOf(string, startIndex) +} + +/** + * Returns `true` if this string contains the specified sequence of characters as a substring. + * + * @param ignoreCase `true` to ignore character case when comparing strings. By default `false`. + */ +public fun String.contains(seq: CharSequence, ignoreCase: Boolean = false): Boolean = + indexOf(seq.toString(), ignoreCase = ignoreCase) >= 0 + + +// rangesDelimitedBy + + +private class DelimitedRangesSequence(private val string: String, private val startIndex: Int, private val limit: Int, private val getNextMatch: String.(Int) -> Pair?): Sequence { + + override fun iterator(): Iterator = object : Iterator { + var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue + var currentStartIndex: Int = Math.min(Math.max(startIndex, 0), string.length()) + var nextItem: IntRange? = null + var counter: Int = 0 + + private fun calcNext() { + if (currentStartIndex < 0) { + nextState = 0 + nextItem = null + } + else { + if (limit > 0 && ++counter >= limit) { + nextItem = currentStartIndex..string.lastIndex + currentStartIndex = -1 + } + else { + val match = string.getNextMatch(currentStartIndex) + if (match == null) { + nextItem = currentStartIndex..string.lastIndex + currentStartIndex = -1 + } + else { + val (index,length) = match + nextItem = currentStartIndex..index-1 + currentStartIndex = index + length + } + } + nextState = 1 + } + } + + override fun next(): IntRange { + if (nextState == -1) + calcNext() + if (nextState == 0) + throw NoSuchElementException() + val result = nextItem as IntRange + // Clean next to avoid keeping reference on yielded instance + nextItem = null + nextState = -1 + return result + } + + override fun hasNext(): Boolean { + if (nextState == -1) + calcNext() + return nextState == 1 + } + } +} + +/** + * Returns a sequence of index ranges of substrings in this string around occurrences of the specified [delimiters]. + * + * @param delimiters One or more characters to be used as delimiters. + * @param startIndex The index to start searching delimiters from. + * No range having its start value less than [startIndex] is returned. + * [startIndex] is coerced to be non-negative and not greater than length of this string. + * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`. + * @param limit The maximum number of substrings to return. Zero by default means no limit is set. + */ +private fun String.rangesDelimitedBy(vararg delimiters: Char, startIndex: Int = 0, ignoreCase: Boolean = false, limit: Int = 0): Sequence { + require(limit >= 0, { "Limit must be non-negative, but was $limit" }) + + return DelimitedRangesSequence(this, startIndex, limit, { startIndex -> indexOfAny(delimiters, startIndex, ignoreCase = ignoreCase)?.let { Pair(it.first, 1) }}) +} + + +/** + * Returns a sequence of index ranges of substrings in this string around occurrences of the specified [delimiters]. + * + * @param delimiters One or more strings to be used as delimiters. + * @param startIndex The index to start searching delimiters from. + * No range having its start value less than [startIndex] is returned. + * [startIndex] is coerced to be non-negative and not greater than length of this string. + * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`. + * @param limit The maximum number of substrings to return. Zero by default means no limit is set. + * + * To avoid ambiguous results when strings in [delimiters] have characters in common, this method proceeds from + * the beginning to the end of this string, and finds at each position the first element in [delimiters] + * that matches this string at that position. + */ +private fun String.rangesDelimitedBy(vararg delimiters: String, startIndex: Int = 0, ignoreCase: Boolean = false, limit: Int = 0): Sequence { + require(limit >= 0, { "Limit must be non-negative, but was $limit" } ) + val delimitersList = delimiters.asList() + + return DelimitedRangesSequence(this, startIndex, limit, { startIndex -> indexOfAny(delimitersList, startIndex, ignoreCase = ignoreCase)?.let { Pair(it.first, it.second.length() )} }) + +} + + +// split + +/** + * Splits this string to a sequence of strings around occurrences of the specified [delimiters]. + * + * @param delimiters One or more strings to be used as delimiters. + * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`. + * @param limit The maximum number of substrings to return. Zero by default means no limit is set. + * + * To avoid ambiguous results when strings in [delimiters] have characters in common, this method proceeds from + * the beginning to the end of this string, and finds at each position the first element in [delimiters] + * that matches this string at that position. + */ +public fun String.splitToSequence(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): Sequence = + rangesDelimitedBy(*delimiters, ignoreCase = ignoreCase, limit = limit) map { substring(it) } + +/** + * Splits this string to a list of strings around occurrences of the specified [delimiters]. + * + * @param delimiters One or more strings to be used as delimiters. + * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`. + * @param limit The maximum number of substrings to return. Zero by default means no limit is set. + * + * To avoid ambiguous results when strings in [delimiters] have characters in common, this method proceeds from + * the beginning to the end of this string, and matches at each position the first element in [delimiters] + * that is equal to a delimiter in this instance at that position. + */ +public fun String.split(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List = + splitToSequence(*delimiters, ignoreCase = ignoreCase, limit = limit).toList() + +/** + * Splits this string to a sequence of strings around occurrences of the specified [delimiters]. + * + * @param delimiters One or more characters to be used as delimiters. + * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`. + * @param limit The maximum number of substrings to return. + */ +public fun String.splitToSequence(vararg delimiters: Char, ignoreCase: Boolean = false, limit: Int = 0): Sequence = + rangesDelimitedBy(*delimiters, ignoreCase = ignoreCase, limit = limit) map { substring(it) } + +/** + * Splits this string to a list of strings around occurrences of the specified [delimiters]. + * + * @param delimiters One or more characters to be used as delimiters. + * @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`. + * @param limit The maximum number of substrings to return. + */ +public fun String.split(vararg delimiters: Char, ignoreCase: Boolean = false, limit: Int = 0): List = + splitToSequence(*delimiters, ignoreCase = ignoreCase, limit = limit).toList() + +/** + * Splits this string to a sequence of lines delimited by any of the following character sequences: CRLF, LF or CR. + */ +public fun String.lineSequence(): Sequence = splitToSequence("\r\n", "\n", "\r") + +/** + * * Splits this string to a list of lines delimited by any of the following character sequences: CRLF, LF or CR. + */ +public fun String.lines(): List = lineSequence().toList() diff --git a/libraries/stdlib/src/kotlin/text/StringsJVM.kt b/libraries/stdlib/src/kotlin/text/StringsJVM.kt index 04934a0c1cf..7970f5af5f2 100644 --- a/libraries/stdlib/src/kotlin/text/StringsJVM.kt +++ b/libraries/stdlib/src/kotlin/text/StringsJVM.kt @@ -7,35 +7,50 @@ import java.util.regex.MatchResult import java.util.regex.Pattern import java.nio.charset.Charset + /** - * Returns the index within this string of the last occurrence of the specified substring. + * Returns the index within this string of the first occurrence of the specified character, starting from the specified offset. */ -public fun String.lastIndexOf(str: String): Int = (this as java.lang.String).lastIndexOf(str) +private fun String.nativeIndexOf(ch: Char, fromIndex: Int): Int = (this as java.lang.String).indexOf(ch.toInt(), fromIndex) + +/** + * Returns the index within this string of the first occurrence of the specified substring, starting from the specified offset. + */ +private fun String.nativeIndexOf(str: String, fromIndex: Int): Int = (this as java.lang.String).indexOf(str, fromIndex) /** * Returns the index within this string of the last occurrence of the specified character. */ -public fun String.lastIndexOf(ch: Char): Int = (this as java.lang.String).lastIndexOf(ch.toString()) +private fun String.nativeLastIndexOf(ch: Char, fromIndex: Int): Int = (this as java.lang.String).lastIndexOf(ch.toInt(), fromIndex) + +/** + * Returns the index within this string of the last occurrence of the specified character, starting from the specified offset. + */ +private fun String.nativeLastIndexOf(str: String, fromIndex: Int): Int = (this as java.lang.String).lastIndexOf(str, fromIndex) + /** * Compares this string to another string, ignoring case considerations. */ -public fun String.equalsIgnoreCase(anotherString: String): Boolean = (this as java.lang.String).equalsIgnoreCase(anotherString) +deprecated("Use equals(anotherString, ignoreCase = true) instead") +public fun String.equalsIgnoreCase(anotherString: String): Boolean = equals(anotherString, ignoreCase = true) + +/** + * Returns `true` if this string is equal to [anotherString], optionally ignoring character case. + * + * @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) + (this as java.lang.String).equals(anotherString) + else + (this as java.lang.String).equalsIgnoreCase(anotherString) /** * Returns the hash code of this string. */ public fun String.hashCode(): Int = (this as java.lang.String).hashCode() -/** - * Returns the index within this string of the first occurrence of the specified substring. - */ -public fun String.indexOf(str: String): Int = (this as java.lang.String).indexOf(str) - -/** - * Returns the index within this string of the first occurrence of the specified substring, starting from the specified offset. - */ -public fun String.indexOf(str: String, fromIndex: Int): Int = (this as java.lang.String).indexOf(str, fromIndex) /** * Returns a copy of this string with all occurrences of [oldChar] replaced with [newChar]. @@ -78,12 +93,8 @@ public fun String.format(locale: Locale, vararg args : Any?) : String = java.lan /** * Splits this string around matches of the given regular expression. */ -public fun String.split(regex: String): Array = (this as java.lang.String).split(regex) +public fun String.split(regex: Pattern, limit: Int = 0): List = regex.split(this, limit).asList() -/** - * Splits this string around occurrences of the specified character. - */ -public fun String.split(ch: Char): Array = (this as java.lang.String).split(java.util.regex.Pattern.quote(ch.toString())) /** * Returns a substring of this string starting with the specified index. @@ -98,32 +109,32 @@ public fun String.substring(beginIndex: Int, endIndex: Int): String = (this as j /** * Returns `true` if this string starts with the specified prefix. */ -public fun String.startsWith(prefix: String): Boolean = (this as java.lang.String).startsWith(prefix) +public fun String.startsWith(prefix: String, ignoreCase: Boolean = false): Boolean { + if (!ignoreCase) + return (this as java.lang.String).startsWith(prefix) + else + return regionMatches(0, prefix, 0, prefix.length(), ignoreCase) +} /** - * Returns `true` if a subsring of this string starting at the specified offset starts with the specified prefix. + * Returns `true` if a substring of this string starting at the specified offset [thisOffset] starts with the specified prefix. */ -public fun String.startsWith(prefix: String, toffset: Int): Boolean = (this as java.lang.String).startsWith(prefix, toffset) - -/** - * Returns `true` if this string starts with the specified character. - */ -public fun String.startsWith(ch: Char): Boolean = (this as java.lang.String).startsWith(ch.toString()) - -/** - * Returns `true` if this string contains the specified sequence of characters as a substring. - */ -public fun String.contains(seq: CharSequence): Boolean = (this as java.lang.String).contains(seq) +public fun String.startsWith(prefix: String, thisOffset: Int, ignoreCase: Boolean = false): Boolean { + if (!ignoreCase) + return (this as java.lang.String).startsWith(prefix, thisOffset) + else + return regionMatches(thisOffset, prefix, 0, prefix.length(), ignoreCase) +} /** * Returns `true` if this string ends with the specified suffix. */ -public fun String.endsWith(suffix: String): Boolean = (this as java.lang.String).endsWith(suffix) - -/** - * Returns `true` if this string ends with the specified character. - */ -public fun String.endsWith(ch: Char): Boolean = (this as java.lang.String).endsWith(ch.toString()) +public fun String.endsWith(suffix: String, ignoreCase: Boolean = false): Boolean { + if (!ignoreCase) + return (this as java.lang.String).endsWith(suffix) + else + return regionMatches(length() - suffix.length(), suffix, 0, suffix.length(), ignoreCase = true) +} // "constructors" for String @@ -207,11 +218,6 @@ public fun String(stringBuilder: java.lang.StringBuilder): String = java.lang.St */ public fun String.replaceFirst(regex: String, replacement: String): String = (this as java.lang.String).replaceFirst(regex, replacement) -/** - * Splits this string into at most [limit] chunks around matches of the given regular expression. - */ -public fun String.split(regex: String, limit: Int): Array = (this as java.lang.String).split(regex, limit) - /** * Returns the character (Unicode code point) at the specified index. */ @@ -256,15 +262,6 @@ public fun String.contentEquals(sb: StringBuffer): Boolean = (this as java.lang. */ public fun String.getChars(srcBegin: Int, srcEnd: Int, dst: CharArray, dstBegin: Int): Unit = (this as java.lang.String).getChars(srcBegin, srcEnd, dst, dstBegin) -/** - * Returns the index within this string of the first occurrence of the specified character. - */ -public fun String.indexOf(ch: Char): Int = (this as java.lang.String).indexOf(ch.toString()) - -/** - * Returns the index within this string of the first occurrence of the specified character, starting from the specified offset. - */ -public fun String.indexOf(ch: Char, fromIndex: Int): Int = (this as java.lang.String).indexOf(ch.toString(), fromIndex) /** * Returns a canonical representation for this string object. @@ -276,15 +273,6 @@ public fun String.intern(): String = (this as java.lang.String).intern() */ public fun String.isEmpty(): Boolean = (this as java.lang.String).isEmpty() -/** - * Returns the index within this string of the last occurrence of the specified character. - */ -public fun String.lastIndexOf(ch: Char, fromIndex: Int): Int = (this as java.lang.String).lastIndexOf(ch.toString(), fromIndex) - -/** - * Returns the index within this string of the last occurrence of the specified character, starting from the specified offset. - */ -public fun String.lastIndexOf(str: String, fromIndex: Int): Int = (this as java.lang.String).lastIndexOf(str, fromIndex) /** * Returns `true` if this string matches the given regular expression. @@ -304,16 +292,21 @@ public fun String.offsetByCodePoints(index: Int, codePointOffset: Int): Int = (t * @param ooffset the start offset in the other string of the substring to compare. * @param len the length of the substring to compare. */ +deprecated("Use regionMatches overload with ignoreCase as optional last parameter.") public fun String.regionMatches(ignoreCase: Boolean, toffset: Int, other: String, ooffset: Int, len: Int): Boolean = (this as java.lang.String).regionMatches(ignoreCase, toffset, other, ooffset, len) /** * Returns `true` if the specified range in this string is equal to the specified range in another string. - * @param toffset the start offset in this string of the substring to compare. + * @param thisOffset the start offset in this string of the substring to compare. * @param other the string against a substring of which the comparison is performed. - * @param ooffset the start offset in the other string of the substring to compare. - * @param len the length of the substring to compare. + * @param otherOffset the start offset in the other string of the substring to compare. + * @param length the length of the substring to compare. */ -public fun String.regionMatches(toffset: Int, other: String, ooffset: Int, len: Int): Boolean = (this as java.lang.String).regionMatches(toffset, other, ooffset, len) +public fun String.regionMatches(thisOffset: Int, other: String, otherOffset: Int, length: Int, ignoreCase: Boolean = false): Boolean = + if (!ignoreCase) + (this as java.lang.String).regionMatches(thisOffset, other, otherOffset, length) + else + (this as java.lang.String).regionMatches(ignoreCase, thisOffset, other, otherOffset, length) /** * Returns a new string obtained by replacing all occurrences of the [target] substring in this string diff --git a/libraries/stdlib/test/text/StringTest.kt b/libraries/stdlib/test/text/StringTest.kt index 35e7ae787c5..00ebce6d464 100644 --- a/libraries/stdlib/test/text/StringTest.kt +++ b/libraries/stdlib/test/text/StringTest.kt @@ -5,31 +5,40 @@ import org.junit.Test as test class StringTest { - test fun startsWith() { + test fun startsWithString() { assertTrue("abcd".startsWith("ab")) assertTrue("abcd".startsWith("abcd")) assertTrue("abcd".startsWith("a")) assertFalse("abcd".startsWith("abcde")) assertFalse("abcd".startsWith("b")) - assertFalse("".startsWith('a')) + assertFalse("".startsWith("a")) + assertTrue("some".startsWith("")) + assertTrue("".startsWith("")) + + assertTrue("abcd".startsWith("aB", ignoreCase = true)) } - test fun endsWith() { + test fun endsWithString() { assertTrue("abcd".endsWith("d")) assertTrue("abcd".endsWith("abcd")) assertFalse("abcd".endsWith("b")) - assertFalse("".endsWith('a')) + assertTrue("strö".endsWith("RÖ", ignoreCase = true)) + assertFalse("".endsWith("a")) + assertTrue("some".endsWith("")) + assertTrue("".endsWith("")) } - test fun testStartsWithChar() { + test fun startsWithChar() { assertTrue("abcd".startsWith('a')) assertFalse("abcd".startsWith('b')) + assertTrue("abcd".startsWith('A', ignoreCase = true)) assertFalse("".startsWith('a')) } - test fun testEndsWithChar() { + test fun endsWithChar() { assertTrue("abcd".endsWith('d')) assertFalse("abcd".endsWith('b')) + assertTrue("strö".endsWith('Ö', ignoreCase = true)) assertFalse("".endsWith('a')) } @@ -264,4 +273,163 @@ class StringTest { assertEquals("value", "value".removeSurrounding("<", ">")) } + + /* + // unit test commented out until rangesDelimitiedBy would become public + + test fun rangesDelimitedBy() { + assertEquals(listOf(0..2, 4..3, 5..7), "abc--def".rangesDelimitedBy('-').toList()) + assertEquals(listOf(0..2, 5..7, 9..10), "abc--def-xy".rangesDelimitedBy("--", "-").toList()) + assertEquals(listOf(0..2, 7..9, 14..16), "123
456
789".rangesDelimitedBy("
", ignoreCase = true).toList()) + assertEquals(listOf(2..2, 4..6), "a=b=c=d".rangesDelimitedBy("=", startIndex = 2, limit = 2).toList()) + + val s = "sample" + assertEquals(listOf(s.indices), s.rangesDelimitedBy("-").toList()) + assertEquals(listOf(s.indices), s.rangesDelimitedBy("-", startIndex = -1).toList()) + assertTrue(s.rangesDelimitedBy("-", startIndex = s.length()).single().isEmpty()) + } + */ + + + test fun split() { + assertEquals(listOf(""), "".split(";")) + assertEquals(listOf("test"), "test".split(*charArray()), "empty list of delimiters, none matched -> entire string returned") + assertEquals(listOf("test"), "test".split(*array()), "empty list of delimiters, none matched -> entire string returned") + + assertEquals(listOf("abc", "def", "123;456"), "abc;def,123;456".split(';', ',', limit = 3)) + assertEquals(listOf("abc", "def", "123", "456"), "abc
def
123
456".split("
", ignoreCase = true)) + + assertEquals(listOf("abc", "def", "123", "456"), "abc=-def==123=456".split("==", "=-", "=")) + } + + test fun splitToLines() { + val string = "first line\rsecond line\nthird line\r\nlast line" + assertEquals(listOf("first line", "second line", "third line", "last line"), string.lines()) + + + val singleLine = "single line" + assertEquals(listOf(singleLine), singleLine.lines()) + } + + + test fun indexOfAnyChar() { + val string = "abracadabra" + val chars = charArray('d','b') + assertEquals(1 to 'b', string.indexOfAny(chars)) + assertEquals(6 to 'd', string.indexOfAny(chars, startIndex = 2)) + assertEquals(null, string.indexOfAny(chars, startIndex = 9)) + + assertEquals(8 to 'b', string.lastIndexOfAny(chars)) + assertEquals(6 to 'd', string.lastIndexOfAny(chars, startIndex = 7)) + assertEquals(null, string.lastIndexOfAny(chars, startIndex = 0)) + + assertEquals(null, string.indexOfAny(charArray())) + } + + test fun indexOfAnyCharIgnoreCase() { + val string = "abraCadabra" + val chars = charArray('B','c') + assertEquals(1 to 'B', string.indexOfAny(chars, ignoreCase = true)) + assertEquals(4 to 'c', string.indexOfAny(chars, startIndex = 2, ignoreCase = true)) + assertEquals(null, string.indexOfAny(chars, startIndex = 9, ignoreCase = true)) + + assertEquals(8 to 'B', string.lastIndexOfAny(chars, ignoreCase = true)) + assertEquals(4 to 'c', string.lastIndexOfAny(chars, startIndex = 7, ignoreCase = true)) + assertEquals(null, string.lastIndexOfAny(chars, startIndex = 0, ignoreCase = true)) + } + + test fun indexOfAnyString() { + val string = "abracadabra" + val substrings = listOf("rac", "ra") + assertEquals(2 to "rac", string.indexOfAny(substrings)) + assertEquals(9 to "ra", string.indexOfAny(substrings, startIndex = 3)) + assertEquals(2 to "ra", string.indexOfAny(substrings.reverse())) + assertEquals(null, string.indexOfAny(substrings, 10)) + + assertEquals(9 to "ra", string.lastIndexOfAny(substrings)) + assertEquals(2 to "rac", string.lastIndexOfAny(substrings, startIndex = 8)) + assertEquals(2 to "ra", string.lastIndexOfAny(substrings.reverse(), startIndex = 8)) + assertEquals(null, string.lastIndexOfAny(substrings, 1)) + + assertEquals(0 to "", string.indexOfAny(listOf("dab", "")), "empty strings are not ignored") + assertEquals(null, string.indexOfAny(listOf())) + } + + test fun indexOfAnyStringIgnoreCase() { + val string = "aBraCadaBrA" + val substrings = listOf("rAc", "Ra") + + assertEquals(2 to substrings[0], string.indexOfAny(substrings, ignoreCase = true)) + assertEquals(9 to substrings[1], string.indexOfAny(substrings, startIndex = 3, ignoreCase = true)) + assertEquals(null, string.indexOfAny(substrings, startIndex = 10, ignoreCase = true)) + + assertEquals(9 to substrings[1], string.lastIndexOfAny(substrings, ignoreCase = true)) + assertEquals(2 to substrings[0], string.lastIndexOfAny(substrings, startIndex = 8, ignoreCase = true)) + assertEquals(null, string.lastIndexOfAny(substrings, startIndex = 1, ignoreCase = true)) + } + + test fun indexOfChar() { + val string = "bcedef" + assertEquals(-1, string.indexOf('a')) + assertEquals(2, string.indexOf('e')) + assertEquals(2, string.indexOf('e', 2)) + assertEquals(4, string.indexOf('e', 3)) + assertEquals(4, string.lastIndexOf('e')) + assertEquals(2, string.lastIndexOf('e', 3)) + + for (startIndex in -1..string.length()+1) { + assertEquals(string.indexOfAny(charArray('e'), startIndex)?.first ?: -1, string.indexOf('e', startIndex)) + assertEquals(string.lastIndexOfAny(charArray('e'), startIndex)?.first ?: -1, string.lastIndexOf('e', startIndex)) + } + + } + + test fun indexOfCharIgnoreCase() { + val string = "bCEdef" + assertEquals(-1, string.indexOf('a', ignoreCase = true)) + assertEquals(2, string.indexOf('E', ignoreCase = true)) + assertEquals(2, string.indexOf('e', 2, ignoreCase = true)) + assertEquals(4, string.indexOf('E', 3, ignoreCase = true)) + assertEquals(4, string.lastIndexOf('E', ignoreCase = true)) + assertEquals(2, string.lastIndexOf('e', 3, ignoreCase = true)) + + + for (startIndex in -1..string.length()+1){ + assertEquals(string.indexOfAny(charArray('e'), startIndex, ignoreCase = true)?.first ?: -1, string.indexOf('E', startIndex, ignoreCase = true)) + assertEquals(string.lastIndexOfAny(charArray('E'), startIndex, ignoreCase = true)?.first ?: -1, string.lastIndexOf('e', startIndex, ignoreCase = true)) + } + } + + test fun indexOfString() { + val string = "bceded" + for (index in string.indices) + assertEquals(index, string.indexOf("", index)) + assertEquals(1, string.indexOf("ced")) + assertEquals(4, string.indexOf("ed", 3)) + assertEquals(-1, string.indexOf("abcdefgh")) + } + + test fun indexOfStringIgnoreCase() { + val string = "bceded" + for (index in string.indices) + assertEquals(index, string.indexOf("", index, ignoreCase = true)) + assertEquals(1, string.indexOf("cEd", ignoreCase = true)) + assertEquals(4, string.indexOf("Ed", 3, ignoreCase = true)) + assertEquals(-1, string.indexOf("abcdefgh", ignoreCase = true)) + } + + + test fun contains() { + assertTrue("sample".contains("pl")) + assertFalse("sample".contains("PL")) + assertTrue("sömple".contains("Ö", ignoreCase = true)) + + assertTrue("sample".contains("")) + assertTrue("".contains("")) + } + + test fun equalsIgnoreCase() { + assertFalse("sample".equals("Sample", ignoreCase = false)) + assertTrue("sample".equals("Sample", ignoreCase = true)) + } }