From 45bb2fdb8b338ec8ee6df0ac9bf799e2b68ab714 Mon Sep 17 00:00:00 2001 From: Nikolay Igotti Date: Fri, 17 Feb 2017 14:31:28 +0300 Subject: [PATCH] Add some string operations, refactor runtime. (#249) --- backend.native/tests/build.gradle | 7 +- .../tests/runtime/basic/tostring0.kt | 4 +- backend.native/tests/runtime/text/string0.kt | 9 + runtime/src/launcher/cpp/launcher.cpp | 21 +- runtime/src/main/cpp/Memory.cpp | 12 +- runtime/src/main/cpp/Memory.h | 6 +- runtime/src/main/cpp/Runtime.cpp | 58 + runtime/src/main/cpp/Runtime.h | 27 + runtime/src/main/cpp/String.cpp | 233 ++- runtime/src/main/cpp/ToString.cpp | 12 +- runtime/src/main/cpp/Types.cpp | 19 - runtime/src/main/cpp/Types.h | 9 - runtime/src/main/kotlin/konan/Annotations.kt | 5 + .../src/main/kotlin/konan/internal/Char.kt | 31 + .../src/main/kotlin/konan/internal/Strings.kt | 138 ++ runtime/src/main/kotlin/kotlin/Arrays.kt | 771 +++++++++- .../kotlin/kotlin/comparisons/Comparisons.kt | 34 + runtime/src/main/kotlin/kotlin/text/Char.kt | 81 ++ .../src/main/kotlin/kotlin/text/Strings.kt | 1280 +++++++++++++++++ 19 files changed, 2644 insertions(+), 113 deletions(-) create mode 100644 backend.native/tests/runtime/text/string0.kt create mode 100644 runtime/src/main/cpp/Runtime.cpp create mode 100644 runtime/src/main/cpp/Runtime.h create mode 100644 runtime/src/main/kotlin/konan/internal/Char.kt create mode 100644 runtime/src/main/kotlin/konan/internal/Strings.kt create mode 100644 runtime/src/main/kotlin/kotlin/text/Char.kt create mode 100644 runtime/src/main/kotlin/kotlin/text/Strings.kt diff --git a/backend.native/tests/build.gradle b/backend.native/tests/build.gradle index 5d6004195f1..8af80b9b6f8 100644 --- a/backend.native/tests/build.gradle +++ b/backend.native/tests/build.gradle @@ -281,7 +281,7 @@ task hello4(type: RunKonanTest) { } task tostring0(type: RunKonanTest) { - goldValue = "127\n-1\n239\nA\n1122334455\n112233445566778899\n3.14159\n1E+27\n1E-300\ntrue\nfalse\n" + goldValue = "127\n-1\n239\nA\nЁ\nト\n1122334455\n112233445566778899\n3.14159\n1E+27\n1E-300\ntrue\nfalse\n" source = "runtime/basic/tostring0.kt" } @@ -791,6 +791,11 @@ task string_builder0(type: RunKonanTest) { source = "runtime/text/string_builder0.kt" } +task string0(type: RunKonanTest) { + goldValue = "true\ntrue\nПРИВЕТ\nпривет\nПока\n" + source = "runtime/text/string0.kt" +} + task catch1(type: RunKonanTest) { goldValue = "Before\nCaught Throwable\nDone\n" source = "runtime/exceptions/catch1.kt" diff --git a/backend.native/tests/runtime/basic/tostring0.kt b/backend.native/tests/runtime/basic/tostring0.kt index 617ea071945..fb3bbe390af 100644 --- a/backend.native/tests/runtime/basic/tostring0.kt +++ b/backend.native/tests/runtime/basic/tostring0.kt @@ -3,8 +3,8 @@ fun main(args : Array) { println(255.toByte().toString()) println(239.toShort().toString()) println('A'.toString()) - // println('Ё'.toString()) - // println('ト'.toString()) + println('Ё'.toString()) + println('ト'.toString()) println(1122334455.toString()) println(112233445566778899.toString()) // Here we differ from Java, as have no dtoa() yet. diff --git a/backend.native/tests/runtime/text/string0.kt b/backend.native/tests/runtime/text/string0.kt new file mode 100644 index 00000000000..069c3d47f9e --- /dev/null +++ b/backend.native/tests/runtime/text/string0.kt @@ -0,0 +1,9 @@ +fun main(args: Array) { + val str = "hello" + println(str.equals("HElLo", true)) + val strI18n = "Привет" + println(strI18n.equals("прИВет", true)) + println(strI18n.toUpperCase()) + println(strI18n.toLowerCase()) + println("пока".capitalize()) +} \ No newline at end of file diff --git a/runtime/src/launcher/cpp/launcher.cpp b/runtime/src/launcher/cpp/launcher.cpp index ea315ec67d4..5af5fcb6f59 100644 --- a/runtime/src/launcher/cpp/launcher.cpp +++ b/runtime/src/launcher/cpp/launcher.cpp @@ -2,6 +2,7 @@ #include "Memory.h" #include "Natives.h" +#include "Runtime.h" #include "String.h" #include "Types.h" @@ -22,18 +23,16 @@ OBJ_GETTER(setupArgs, int argc, char** argv) { extern "C" void Konan_start(const ObjHeader* ); int main(int argc, char** argv) { + RuntimeState* state = InitRuntime(); - InitMemory(); - InitGlobalVariables(); + if (state != nullptr) { + ObjHolder args; + setupArgs(argc, argv, args.slot()); + Konan_start(args.obj()); + } - { - ObjHolder args; - setupArgs(argc, argv, args.slot()); - Konan_start(args.obj()); - } + DeinitRuntime(state); - DeinitMemory(); - - // Yes, we have to follow Java convention and return zero. - return 0; + // Yes, we have to follow Java convention and return zero. + return 0; } diff --git a/runtime/src/main/cpp/Memory.cpp b/runtime/src/main/cpp/Memory.cpp index 79321fcd6a0..1165202fdbb 100644 --- a/runtime/src/main/cpp/Memory.cpp +++ b/runtime/src/main/cpp/Memory.cpp @@ -31,6 +31,8 @@ constexpr container_size_t kContainerAlignment = 1024; // Single object alignment. constexpr container_size_t kObjectAlignment = 8; +} // namespace + #if USE_GC // Collection threshold default (collect after having so many elements in the // release candidates set). @@ -66,6 +68,9 @@ struct MemoryState { #endif }; +namespace { + +// TODO: remove this global. MemoryState* memoryState = nullptr; // TODO: use those allocators for STL containers as well. @@ -450,7 +455,7 @@ inline void ReleaseRef(const ObjHeader* object) { extern "C" { -void InitMemory() { +MemoryState* InitMemory() { RuntimeAssert(offsetof(ArrayHeader, type_info_) == offsetof(ObjHeader, type_info_), @@ -476,9 +481,10 @@ void InitMemory() { memoryState->gcThreshold = kGcThreshold; memoryState->gcSuspendCount = 0; #endif + return memoryState; } -void DeinitMemory() { +void DeinitMemory(MemoryState* memoryState) { #if TRACE_MEMORY // Free all global objects, to ensure no memory leaks happens. for (auto location: *memoryState->globalObjects) { @@ -505,7 +511,7 @@ void DeinitMemory() { } delete memoryState; - memoryState = nullptr; + ::memoryState = nullptr; } OBJ_GETTER(AllocInstance, const TypeInfo* type_info) { diff --git a/runtime/src/main/cpp/Memory.h b/runtime/src/main/cpp/Memory.h index 76f74fe0b57..0751150d349 100644 --- a/runtime/src/main/cpp/Memory.h +++ b/runtime/src/main/cpp/Memory.h @@ -304,8 +304,10 @@ extern "C" { return result; \ } -void InitMemory(); -void DeinitMemory(); +struct MemoryState; + +MemoryState* InitMemory(); +void DeinitMemory(MemoryState*); // // Object allocation. diff --git a/runtime/src/main/cpp/Runtime.cpp b/runtime/src/main/cpp/Runtime.cpp new file mode 100644 index 00000000000..7d6bb6bffdd --- /dev/null +++ b/runtime/src/main/cpp/Runtime.cpp @@ -0,0 +1,58 @@ +#include + +#include "Runtime.h" + +struct RuntimeState { + MemoryState* memoryState; +}; + +namespace { + +InitNode* initHeadNode = nullptr; +InitNode* initTailNode = nullptr; + +void InitGlobalVariables() { + InitNode *currNode = initHeadNode; + while (currNode != nullptr) { + currNode->init(); + currNode = currNode->next; + } +} + +} // namespace + +#ifdef __cplusplus +extern "C" { +#endif + +void AppendToInitializersTail(struct InitNode *next) { + // TODO: use RuntimeState. + if (initHeadNode == nullptr) { + initHeadNode = next; + } else { + initTailNode->next = next; + } + initTailNode = next; +} + +// TODO: properly use RuntimeState. +RuntimeState* InitRuntime() { + // Set Unicode locale, otherwise towlower() and friends do not work properly. + if (setlocale(LC_CTYPE, "en_US.UTF-8") == nullptr) { + return nullptr; + } + RuntimeState* result = new RuntimeState(); + result->memoryState = InitMemory(); + // Keep global variables in state as well. + InitGlobalVariables(); + return result; +} + +void DeinitRuntime(RuntimeState* state) { + DeinitMemory(state->memoryState); + delete state; +} + +#ifdef __cplusplus +} +#endif diff --git a/runtime/src/main/cpp/Runtime.h b/runtime/src/main/cpp/Runtime.h new file mode 100644 index 00000000000..0c0f0a2df0c --- /dev/null +++ b/runtime/src/main/cpp/Runtime.h @@ -0,0 +1,27 @@ +#ifndef RUNTIME_RUNTIME_H +#define RUNTIME_RUNTIME_H + +#include "Types.h" + +struct RuntimeState; + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*Initializer)(); +struct InitNode { + Initializer init; + struct InitNode* next; +}; + +void AppendToInitializersTail(struct InitNode*); + +RuntimeState* InitRuntime(); +void DeinitRuntime(RuntimeState* state); + +#ifdef __cplusplus +} +#endif + +#endif // RUNTIME_RUNTIME_H diff --git a/runtime/src/main/cpp/String.cpp b/runtime/src/main/cpp/String.cpp index f08bb1ff7fa..01641dda5ab 100644 --- a/runtime/src/main/cpp/String.cpp +++ b/runtime/src/main/cpp/String.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -151,13 +152,236 @@ KBoolean Kotlin_String_equals(KString thiz, KConstRef other) { if (thiz == otherString) return true; return thiz->count_ == otherString->count_ && memcmp(CharArrayAddressOfElementAt(thiz, 0), - CharArrayAddressOfElementAt(otherString, 0), - thiz->count_ * sizeof(KChar)) == 0; + CharArrayAddressOfElementAt(otherString, 0), + thiz->count_ * sizeof(KChar)) == 0; +} + +KBoolean Kotlin_String_equalsIgnoreCase(KString thiz, KConstRef other) { + RuntimeAssert(thiz->type_info() == theStringTypeInfo && + other->type_info() == theStringTypeInfo, "Must be strings"); + // Important, due to literal internalization. + KString otherString = other->array(); + if (thiz == otherString) return true; + if (thiz->count_ != otherString->count_) return false; + auto count = thiz->count_; + const KChar* thizRaw = CharArrayAddressOfElementAt(thiz, 0); + const KChar* otherRaw = CharArrayAddressOfElementAt(otherString, 0); + for (KInt index = 0; index < count; ++index) { + if (towlower(*thizRaw++) != towlower(*otherRaw++)) return false; + } + return true; +} + +OBJ_GETTER(Kotlin_String_replace, KString thiz, KChar oldChar, KChar newChar, + KBoolean ignoreCase) { + auto count = thiz->count_; + ArrayHeader* result = AllocArrayInstance( + theStringTypeInfo, count, OBJ_RESULT)->array(); + const KChar* thizRaw = CharArrayAddressOfElementAt(thiz, 0); + KChar* resultRaw = CharArrayAddressOfElementAt(result, 0); + if (ignoreCase) { + KChar oldCharLower = towlower(oldChar); + for (KInt index = 0; index < count; ++index) { + KChar thizChar = *thizRaw++; + *resultRaw++ = towlower(thizChar) == oldCharLower ? newChar : thizChar; + } + } else { + for (KInt index = 0; index < count; ++index) { + KChar thizChar = *thizRaw++; + *resultRaw++ = thizChar == oldChar ? newChar : thizChar; + } + } + RETURN_OBJ(result->obj()); +} + +OBJ_GETTER(Kotlin_String_toUpperCase, KString thiz) { + auto count = thiz->count_; + ArrayHeader* result = AllocArrayInstance( + theStringTypeInfo, count, OBJ_RESULT)->array(); + const KChar* thizRaw = CharArrayAddressOfElementAt(thiz, 0); + KChar* resultRaw = CharArrayAddressOfElementAt(result, 0); + for (KInt index = 0; index < count; ++index) { + *resultRaw++ = towupper(*thizRaw++); + } + RETURN_OBJ(result->obj()); +} + +OBJ_GETTER(Kotlin_String_toLowerCase, KString thiz) { + auto count = thiz->count_; + ArrayHeader* result = AllocArrayInstance( + theStringTypeInfo, count, OBJ_RESULT)->array(); + const KChar* thizRaw = CharArrayAddressOfElementAt(thiz, 0); + KChar* resultRaw = CharArrayAddressOfElementAt(result, 0); + for (KInt index = 0; index < count; ++index) { + *resultRaw++ = towlower(*thizRaw++); + } + RETURN_OBJ(result->obj()); +} + +KBoolean Kotlin_String_regionMatches(KString thiz, KInt thizOffset, + KString other, KInt otherOffset, + KInt length, KBoolean ignoreCase) { + if (thizOffset < 0 || thizOffset + length > thiz->count_ || + otherOffset < 0 || otherOffset + length > other->count_) { + return false; + } + const KChar* thizRaw = CharArrayAddressOfElementAt(thiz, thizOffset); + const KChar* otherRaw = CharArrayAddressOfElementAt(other, otherOffset); + if (ignoreCase) { + for (KInt index = 0; index < length; ++index) { + if (towlower(*thizRaw++) != towlower(*otherRaw++)) return false; + } + } else { + for (KInt index = 0; index < length; ++index) { + if (*thizRaw++ != *otherRaw++) return false; + } + } + return true; +} + +KBoolean Kotlin_CharSequence_regionMatches(KString thiz, KInt thizOffset, + KString other, KInt otherOffset, + KInt length, KBoolean ignoreCase) { + RuntimeAssert(false, "Kotlin_CharSequence_regionMatches is not implemented"); + return false; +} + +KBoolean Kotlin_Char_isDefined(KChar ch) { + // TODO: fixme! + RuntimeAssert(false, "Kotlin_Char_isDefined() is not implemented"); + return true; +} + +KBoolean Kotlin_Char_isLetter(KChar ch) { + return iswalpha(ch); +} + +KBoolean Kotlin_Char_isLetterOrDigit(KChar ch) { + return iswalnum(ch); +} + +KBoolean Kotlin_Char_isDigit(KChar ch) { + return iswdigit(ch); +} + +KBoolean Kotlin_Char_isIdentifierIgnorable(KChar ch) { + RuntimeAssert(false, "Kotlin_Char_isIdentifierIgnorable() is not implemented"); + return false; +} + +KBoolean Kotlin_Char_isISOControl(KChar ch) { + RuntimeAssert(false, "Kotlin_Char_isISOControl() is not implemented"); + return false; +} + +KBoolean Kotlin_Char_isHighSurrogate(KChar ch) { + return ((ch & 0xfc00) == 0xd800); +} + +KBoolean Kotlin_Char_isLowSurrogate(KChar ch) { + return ((ch & 0xfc00) == 0xdc00); +} + +KBoolean Kotlin_Char_isWhitespace(KChar ch) { + return iswspace(ch); +} + +KBoolean Kotlin_Char_isLowerCase(KChar ch) { + return iswlower(ch); +} + +KBoolean Kotlin_Char_isUpperCase(KChar ch) { + return iswupper(ch); +} + +KChar Kotlin_Char_toLowerCase(KChar ch) { + return towlower(ch); +} + +KChar Kotlin_Char_toUpperCase(KChar ch) { + return towupper(ch); +} + +KInt Kotlin_String_indexOfChar(KString thiz, KChar ch, KInt fromIndex) { + if (fromIndex < 0 || fromIndex > thiz->count_) { + return false; + } + KInt count = thiz->count_; + const KChar* thizRaw = CharArrayAddressOfElementAt(thiz, fromIndex); + while (fromIndex < count) { + if (*thizRaw++ == ch) return fromIndex; + fromIndex++; + } + return -1; +} + +KInt Kotlin_String_lastIndexOfChar(KString thiz, KChar ch, KInt fromIndex) { + if (fromIndex < 0 || fromIndex > thiz->count_ || thiz->count_ == 0) { + return false; + } + KInt count = thiz->count_; + KInt index = count - 1; + const KChar* thizRaw = CharArrayAddressOfElementAt(thiz, index); + while (index >= fromIndex) { + if (*thizRaw-- == ch) return index; + index--; + } + return -1; +} + +// TODO: or code up Knuth-Moris-Pratt. +KInt Kotlin_String_indexOfString(KString thiz, KString other, KInt fromIndex) { + if (fromIndex < 0 || fromIndex > thiz->count_ || + fromIndex + other->count_ > thiz->count_) { + return -1; + } + KInt count = thiz->count_; + const KChar* thizRaw = CharArrayAddressOfElementAt(thiz, fromIndex); + const KChar* otherRaw = CharArrayAddressOfElementAt(thiz, 0); + void* result = memmem(thizRaw, (thiz->count_ - fromIndex) * sizeof(KChar), + otherRaw, other->count_ * sizeof(KChar)); + if (result == nullptr) return -1; + + return (reinterpret_cast(result) - reinterpret_cast( + CharArrayAddressOfElementAt(thiz, 0))) / sizeof(KChar); +} + +KInt Kotlin_String_lastIndexOfString(KString thiz, KString other, KInt fromIndex) { + if (fromIndex < 0 || fromIndex > thiz->count_ || thiz->count_ == 0 || + fromIndex + other->count_ > thiz->count_) { + return false; + } + KInt count = thiz->count_; + KInt otherCount = other->count_; + KInt start = fromIndex; + if (otherCount <= count && start >= 0) { + if (otherCount > 0) { + if (fromIndex > count - otherCount) + start = count - otherCount; + KChar firstChar = *CharArrayAddressOfElementAt(other, 0); + while (true) { + KInt candidate = Kotlin_String_lastIndexOfChar(thiz, firstChar, start); + if (candidate == -1) return -1; + KInt offsetThiz = candidate; + KInt offsetOther = 0; + while (++offsetOther < otherCount && + *CharArrayAddressOfElementAt(thiz, ++offsetThiz) == + *CharArrayAddressOfElementAt(other, offsetOther)) {} + if (offsetOther == otherCount) { + return candidate; + } + start = candidate - 1; + } + } + return start < count ? start : count; + } + return -1; } KInt Kotlin_String_hashCode(KString thiz) { // TODO: consider caching strings hashes. // TODO: maybe use some simpler hashing algorithm? + // Note that we don't use Java's string hash. return CityHash64( CharArrayAddressOfElementAt(thiz, 0), thiz->count_ * sizeof(KChar)); } @@ -174,8 +398,8 @@ OBJ_GETTER(Kotlin_String_subSequence, KString thiz, KInt startIndex, KInt endInd ArrayHeader* result = AllocArrayInstance( theStringTypeInfo, length, OBJ_RESULT)->array(); memcpy(CharArrayAddressOfElementAt(result, 0), - CharArrayAddressOfElementAt(thiz, startIndex), - length * sizeof(KChar)); + CharArrayAddressOfElementAt(thiz, startIndex), + length * sizeof(KChar)); RETURN_OBJ(result->obj()); } @@ -207,5 +431,4 @@ OBJ_GETTER0(Kotlin_io_Console_readLine) { RETURN_RESULT_OF(CreateStringFromCString, data); } - } // extern "C" diff --git a/runtime/src/main/cpp/ToString.cpp b/runtime/src/main/cpp/ToString.cpp index d7aa605b875..10c75020b64 100644 --- a/runtime/src/main/cpp/ToString.cpp +++ b/runtime/src/main/cpp/ToString.cpp @@ -13,8 +13,8 @@ extern "C" { OBJ_GETTER(Kotlin_Any_toString, KConstRef thiz) { char cstring[80]; snprintf(cstring, sizeof(cstring), "%s %p type %p", - IsArray(thiz) ? "array" : "object", - thiz, thiz->type_info_); + IsArray(thiz) ? "array" : "object", + thiz, thiz->type_info_); RETURN_RESULT_OF(CreateStringFromCString, cstring); } @@ -25,10 +25,10 @@ OBJ_GETTER(Kotlin_Byte_toString, KByte value) { } OBJ_GETTER(Kotlin_Char_toString, KChar value) { - char cstring[5]; - // TODO: support UTF-8. - snprintf(cstring, sizeof(cstring), "%c", value); - RETURN_RESULT_OF(CreateStringFromCString, cstring); + ArrayHeader* result = AllocArrayInstance( + theStringTypeInfo, 1, OBJ_RESULT)->array(); + *CharArrayAddressOfElementAt(result, 0) = value; + RETURN_OBJ(result->obj()); } OBJ_GETTER(Kotlin_Short_toString, KShort value) { diff --git a/runtime/src/main/cpp/Types.cpp b/runtime/src/main/cpp/Types.cpp index f5c91305151..740125447ef 100644 --- a/runtime/src/main/cpp/Types.cpp +++ b/runtime/src/main/cpp/Types.cpp @@ -36,25 +36,6 @@ void CheckInstance(const ObjHeader* obj, const TypeInfo* type_info) { ThrowClassCastException(); } -static struct InitNode* initHeadNode = nullptr; -static struct InitNode* initTailNode = nullptr; - -void AppendToInitializersTail(struct InitNode *next) { - if (initHeadNode == nullptr) { - initHeadNode = next; - } else { - initTailNode->next = next; - } - initTailNode = next; -} - -void InitGlobalVariables() { - struct InitNode *currNode = initHeadNode; - while(currNode != nullptr) { - currNode->init(); - currNode = currNode->next; - } -} #ifdef __cplusplus } #endif diff --git a/runtime/src/main/cpp/Types.h b/runtime/src/main/cpp/Types.h index 771655ad3d2..06e5eea7788 100644 --- a/runtime/src/main/cpp/Types.h +++ b/runtime/src/main/cpp/Types.h @@ -41,15 +41,6 @@ KBoolean IsInstance(const ObjHeader* obj, const TypeInfo* type_info) RUNTIME_PUR void CheckCast(const ObjHeader* obj, const TypeInfo* type_info); KBoolean IsArray(KConstRef obj) RUNTIME_PURE; -typedef void (*Initializer)(); -struct InitNode { - Initializer init; - struct InitNode* next; -}; - -void AppendToInitializersTail(struct InitNode*); -void InitGlobalVariables(); - #ifdef __cplusplus } #endif diff --git a/runtime/src/main/kotlin/konan/Annotations.kt b/runtime/src/main/kotlin/konan/Annotations.kt index c62c7c5649a..d38c2888f0d 100644 --- a/runtime/src/main/kotlin/konan/Annotations.kt +++ b/runtime/src/main/kotlin/konan/Annotations.kt @@ -49,6 +49,11 @@ public annotation class FixmeSequences */ public annotation class FixmeVariance +/** + * Need to be fixed because of regular expressions. + */ +public annotation class FixmeRegex + /** * Need to be fixed. */ diff --git a/runtime/src/main/kotlin/konan/internal/Char.kt b/runtime/src/main/kotlin/konan/internal/Char.kt new file mode 100644 index 00000000000..d3efcc388b6 --- /dev/null +++ b/runtime/src/main/kotlin/konan/internal/Char.kt @@ -0,0 +1,31 @@ +package kotlin.text + +/** + * Concatenates this Char and a String. + */ +@kotlin.internal.InlineOnly +public inline operator fun Char.plus(other: String) : String = this.toString() + other + +/** + * 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 +} + +/** + * Returns `true` if this character is a Unicode surrogate code unit. + */ +public fun Char.isSurrogate(): Boolean = this in Char.MIN_SURROGATE..Char.MAX_SURROGATE diff --git a/runtime/src/main/kotlin/konan/internal/Strings.kt b/runtime/src/main/kotlin/konan/internal/Strings.kt new file mode 100644 index 00000000000..240d9cae55e --- /dev/null +++ b/runtime/src/main/kotlin/konan/internal/Strings.kt @@ -0,0 +1,138 @@ +package kotlin.text + +/** + * Returns the index within this string of the first occurrence of the specified character, starting from the specified offset. + */ +@SymbolName("Kotlin_String_indexOfChar") +external internal fun String.nativeIndexOf(ch: Char, fromIndex: Int): Int + +/** + * Returns the index within this string of the first occurrence of the specified substring, starting from the specified offset. + */ +@SymbolName("Kotlin_String_indexOfString") +external internal fun String.nativeIndexOf(str: String, fromIndex: Int): Int + +/** + * Returns the index within this string of the last occurrence of the specified character. + */ +@SymbolName("Kotlin_String_lastIndexOfChar") +external internal fun String.nativeLastIndexOf(ch: Char, fromIndex: Int): Int + +/** + * Returns the index within this string of the last occurrence of the specified character, starting from the specified offset. + */ +@SymbolName("Kotlin_String_lastIndexOfString") +external internal fun String.nativeLastIndexOf(str: String, fromIndex: Int): Int + +/** + * Returns `true` if this string is equal to [other], optionally ignoring character case. + * + * @param ignoreCase `true` to ignore character case when comparing strings. By default `false`. + */ +public fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean { + if (this === null) + return other === null + if (other === null) + return false + return if (!ignoreCase) + this.equals(other) + else + stringEqualsIgnoreCase(this, other) +} + +@SymbolName("Kotlin_String_equalsIgnoreCase") +external internal fun stringEqualsIgnoreCase(thiz: String, other: String): Boolean + +/** + * Returns a new string with all occurrences of [oldChar] replaced with [newChar]. + */ +@SymbolName("Kotlin_String_replace") +external public fun String.replace( + oldChar: Char, newChar: Char, ignoreCase: Boolean = false): String + + +/** + * Returns a new string obtained by replacing all occurrences of the [oldValue] substring in this string + * with the specified [newValue] string. + */ +public fun String.replace(oldValue: String, newValue: String, ignoreCase: Boolean = false): String = + splitToSequence(oldValue, ignoreCase = ignoreCase).joinToString(separator = newValue) + +/** + * Returns a new string with the first occurrence of [oldChar] replaced with [newChar]. + */ +public fun String.replaceFirst(oldChar: Char, newChar: Char, ignoreCase: Boolean = false): String { + val index = indexOf(oldChar, ignoreCase = ignoreCase) + return if (index < 0) this else this.replaceRange(index, index + 1, newChar.toString()) +} + +/** + * Returns a new string obtained by replacing the first occurrence of the [oldValue] substring in this string + * with the specified [newValue] string. + */ +public fun String.replaceFirst(oldValue: String, newValue: String, ignoreCase: Boolean = false): String { + val index = indexOf(oldValue, ignoreCase = ignoreCase) + return if (index < 0) this else this.replaceRange(index, index + oldValue.length, newValue) +} + +/** + * Returns a copy of this string converted to upper case using the rules of the default locale. + */ +@SymbolName("Kotlin_String_toUpperCase") +external public fun String.toUpperCase(): String + +/** + * Returns a copy of this string converted to lower case using the rules of the default locale. + */ +@SymbolName("Kotlin_String_toLowerCase") +external public inline fun String.toLowerCase(): String + +/** + * Returns a copy of this string having its first letter uppercased, or the original string, + * if it's empty or already starts with an upper case letter. + * + * @sample samples.text.Strings.captialize + */ +public fun String.capitalize(): String { + return if (isNotEmpty() && this[0].isLowerCase()) substring(0, 1).toUpperCase() + substring(1) else this +} + +/** + * Returns a copy of this string having its first letter lowercased, or the original string, + * if it's empty or already starts with a lower case letter. + * + * @sample samples.text.Strings.decaptialize + */ +public fun String.decapitalize(): String { + return if (isNotEmpty() && this[0].isUpperCase()) substring(0, 1).toLowerCase() + substring(1) else this +} + +/** + * Returns `true` if this string is empty or consists solely of whitespace characters. + */ +public fun CharSequence.isBlank(): Boolean = length == 0 || indices.all { this[it].isWhitespace() } + +/** + * Returns `true` if the specified range in this char sequence is equal to the specified range in another char sequence. + * @param thisOffset the start offset in this char sequence of the substring to compare. + * @param other the string against a substring of which the comparison is performed. + * @param otherOffset the start offset in the other char sequence of the substring to compare. + * @param length the length of the substring to compare. + */ +@SymbolName("Kotlin_CharSequence_regionMatches") +external public fun CharSequence.regionMatches( + thisOffset: Int, other: CharSequence, otherOffset: Int, length: Int, + ignoreCase: Boolean = false): Boolean + + +/** + * Returns `true` if the specified range in this string is equal to the specified range in another string. + * @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 otherOffset the start offset in the other string of the substring to compare. + * @param length the length of the substring to compare. + */ +@SymbolName("Kotlin_String_regionMatches") +external public fun String.regionMatches( + thisOffset: Int, other: String, otherOffset: Int, length: Int, + ignoreCase: Boolean = false): Boolean diff --git a/runtime/src/main/kotlin/kotlin/Arrays.kt b/runtime/src/main/kotlin/kotlin/Arrays.kt index 946d73128d7..9ebbe87c68e 100644 --- a/runtime/src/main/kotlin/kotlin/Arrays.kt +++ b/runtime/src/main/kotlin/kotlin/Arrays.kt @@ -371,6 +371,134 @@ public operator fun <@kotlin.internal.OnlyInputTypes T> Array.contains(el return indexOf(element) >= 0 } +/** + * Returns `true` if [element] is found in the array. + */ +public operator fun ByteArray.contains(element: Byte): Boolean { + return indexOf(element) >= 0 +} + +/** + * Returns `true` if [element] is found in the array. + */ +public operator fun ShortArray.contains(element: Short): Boolean { + return indexOf(element) >= 0 +} + +/** + * Returns `true` if [element] is found in the array. + */ +public operator fun IntArray.contains(element: Int): Boolean { + return indexOf(element) >= 0 +} + +/** + * Returns `true` if [element] is found in the array. + */ +public operator fun LongArray.contains(element: Long): Boolean { + return indexOf(element) >= 0 +} + +/** + * Returns `true` if [element] is found in the array. + */ +public operator fun FloatArray.contains(element: Float): Boolean { + return indexOf(element) >= 0 +} + +/** + * Returns `true` if [element] is found in the array. + */ +public operator fun DoubleArray.contains(element: Double): Boolean { + return indexOf(element) >= 0 +} + +/** + * Returns `true` if [element] is found in the array. + */ +public operator fun BooleanArray.contains(element: Boolean): Boolean { + return indexOf(element) >= 0 +} + +/** + * Returns `true` if [element] is found in the array. + */ +public operator fun CharArray.contains(element: Char): Boolean { + return indexOf(element) >= 0 +} + +/** + * Returns an element at the given [index] or throws an [IndexOutOfBoundsException] if the [index] is out of bounds of this array. + */ +@kotlin.internal.InlineOnly +public inline fun Array.elementAt(index: Int): T { + return get(index) +} + +/** + * Returns an element at the given [index] or throws an [IndexOutOfBoundsException] if the [index] is out of bounds of this array. + */ +@kotlin.internal.InlineOnly +public inline fun ByteArray.elementAt(index: Int): Byte { + return get(index) +} + +/** + * Returns an element at the given [index] or throws an [IndexOutOfBoundsException] if the [index] is out of bounds of this array. + */ +@kotlin.internal.InlineOnly +public inline fun ShortArray.elementAt(index: Int): Short { + return get(index) +} + +/** + * Returns an element at the given [index] or throws an [IndexOutOfBoundsException] if the [index] is out of bounds of this array. + */ +@kotlin.internal.InlineOnly +public inline fun IntArray.elementAt(index: Int): Int { + return get(index) +} + +/** + * Returns an element at the given [index] or throws an [IndexOutOfBoundsException] if the [index] is out of bounds of this array. + */ +@kotlin.internal.InlineOnly +public inline fun LongArray.elementAt(index: Int): Long { + return get(index) +} + +/** + * Returns an element at the given [index] or throws an [IndexOutOfBoundsException] if the [index] is out of bounds of this array. + */ +@kotlin.internal.InlineOnly +public inline fun FloatArray.elementAt(index: Int): Float { + return get(index) +} + +/** + * Returns an element at the given [index] or throws an [IndexOutOfBoundsException] if the [index] is out of bounds of this array. + */ +@kotlin.internal.InlineOnly +public inline fun DoubleArray.elementAt(index: Int): Double { + return get(index) +} + +/** + * Returns an element at the given [index] or throws an [IndexOutOfBoundsException] if the [index] is out of bounds of this array. + */ +@kotlin.internal.InlineOnly +public inline fun BooleanArray.elementAt(index: Int): Boolean { + return get(index) +} + +/** + * Returns an element at the given [index] or throws an [IndexOutOfBoundsException] if the [index] is out of bounds of this array. + */ +@kotlin.internal.InlineOnly +public inline fun CharArray.elementAt(index: Int): Char { + return get(index) +} + /** * Returns first index of [element], or -1 if the array does not contain element. */ @@ -391,6 +519,102 @@ public fun <@kotlin.internal.OnlyInputTypes T> Array.indexOf(element: T): return -1 } +/** + * Returns first index of [element], or -1 if the array does not contain element. + */ +public fun ByteArray.indexOf(element: Byte): Int { + for (index in indices) { + if (element == this[index]) { + return index + } + } + return -1 +} + +/** + * Returns first index of [element], or -1 if the array does not contain element. + */ +public fun ShortArray.indexOf(element: Short): Int { + for (index in indices) { + if (element == this[index]) { + return index + } + } + return -1 +} + +/** + * Returns first index of [element], or -1 if the array does not contain element. + */ +public fun IntArray.indexOf(element: Int): Int { + for (index in indices) { + if (element == this[index]) { + return index + } + } + return -1 +} + +/** + * Returns first index of [element], or -1 if the array does not contain element. + */ +public fun LongArray.indexOf(element: Long): Int { + for (index in indices) { + if (element == this[index]) { + return index + } + } + return -1 +} + +/** + * Returns first index of [element], or -1 if the array does not contain element. + */ +public fun FloatArray.indexOf(element: Float): Int { + for (index in indices) { + if (element == this[index]) { + return index + } + } + return -1 +} + +/** + * Returns first index of [element], or -1 if the array does not contain element. + */ +public fun DoubleArray.indexOf(element: Double): Int { + for (index in indices) { + if (element == this[index]) { + return index + } + } + return -1 +} + +/** + * Returns first index of [element], or -1 if the array does not contain element. + */ +public fun BooleanArray.indexOf(element: Boolean): Int { + for (index in indices) { + if (element == this[index]) { + return index + } + } + return -1 +} + +/** + * Returns first index of [element], or -1 if the array does not contain element. + */ +public fun CharArray.indexOf(element: Char): Int { + for (index in indices) { + if (element == this[index]) { + return index + } + } + return -1 +} + /** * Returns index of the first element matching the given [predicate], or -1 if the array does not contain such element. */ @@ -403,6 +627,102 @@ public inline fun Array.indexOfFirst(predicate: (T) -> Boolean): Int return -1 } +/** + * Returns index of the first element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun ByteArray.indexOfFirst(predicate: (Byte) -> Boolean): Int { + for (index in indices) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the first element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun ShortArray.indexOfFirst(predicate: (Short) -> Boolean): Int { + for (index in indices) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the first element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun IntArray.indexOfFirst(predicate: (Int) -> Boolean): Int { + for (index in indices) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the first element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun LongArray.indexOfFirst(predicate: (Long) -> Boolean): Int { + for (index in indices) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the first element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun FloatArray.indexOfFirst(predicate: (Float) -> Boolean): Int { + for (index in indices) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the first element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun DoubleArray.indexOfFirst(predicate: (Double) -> Boolean): Int { + for (index in indices) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the first element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun BooleanArray.indexOfFirst(predicate: (Boolean) -> Boolean): Int { + for (index in indices) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the first element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun CharArray.indexOfFirst(predicate: (Char) -> Boolean): Int { + for (index in indices) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + /** * Returns index of the last element matching the given [predicate], or -1 if the array does not contain such element. */ @@ -415,6 +735,102 @@ public inline fun Array.indexOfLast(predicate: (T) -> Boolean): Int { return -1 } +/** + * Returns index of the last element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun ByteArray.indexOfLast(predicate: (Byte) -> Boolean): Int { + for (index in indices.reversed()) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the last element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun ShortArray.indexOfLast(predicate: (Short) -> Boolean): Int { + for (index in indices.reversed()) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the last element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun IntArray.indexOfLast(predicate: (Int) -> Boolean): Int { + for (index in indices.reversed()) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the last element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun LongArray.indexOfLast(predicate: (Long) -> Boolean): Int { + for (index in indices.reversed()) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the last element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun FloatArray.indexOfLast(predicate: (Float) -> Boolean): Int { + for (index in indices.reversed()) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the last element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun DoubleArray.indexOfLast(predicate: (Double) -> Boolean): Int { + for (index in indices.reversed()) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the last element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun BooleanArray.indexOfLast(predicate: (Boolean) -> Boolean): Int { + for (index in indices.reversed()) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the last element matching the given [predicate], or -1 if the array does not contain such element. + */ +public inline fun CharArray.indexOfLast(predicate: (Char) -> Boolean): Int { + for (index in indices.reversed()) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + /** * Returns a list containing all elements not matching the given [predicate]. */ @@ -492,6 +908,253 @@ public inline fun Array.lastOrNull(predicate: (T) -> Boolean): T? { return null } +/** + * Returns the single element, or throws an exception if the array is empty or has more than one element. + */ +public fun Array.single(): T { + return when (size) { + 0 -> throw NoSuchElementException("Array is empty.") + 1 -> this[0] + else -> throw IllegalArgumentException("Array has more than one element.") + } +} + +/** + * Returns the single element, or throws an exception if the array is empty or has more than one element. + */ +public fun ByteArray.single(): Byte { + return when (size) { + 0 -> throw NoSuchElementException("Array is empty.") + 1 -> this[0] + else -> throw IllegalArgumentException("Array has more than one element.") + } +} + +/** + * Returns the single element, or throws an exception if the array is empty or has more than one element. + */ +public fun ShortArray.single(): Short { + return when (size) { + 0 -> throw NoSuchElementException("Array is empty.") + 1 -> this[0] + else -> throw IllegalArgumentException("Array has more than one element.") + } +} + +/** + * Returns the single element, or throws an exception if the array is empty or has more than one element. + */ +public fun IntArray.single(): Int { + return when (size) { + 0 -> throw NoSuchElementException("Array is empty.") + 1 -> this[0] + else -> throw IllegalArgumentException("Array has more than one element.") + } +} + +/** + * Returns the single element, or throws an exception if the array is empty or has more than one element. + */ +public fun LongArray.single(): Long { + return when (size) { + 0 -> throw NoSuchElementException("Array is empty.") + 1 -> this[0] + else -> throw IllegalArgumentException("Array has more than one element.") + } +} + +/** + * Returns the single element, or throws an exception if the array is empty or has more than one element. + */ +public fun DoubleArray.single(): Double { + return when (size) { + 0 -> throw NoSuchElementException("Array is empty.") + 1 -> this[0] + else -> throw IllegalArgumentException("Array has more than one element.") + } +} + +/** + * Returns the single element, or throws an exception if the array is empty or has more than one element. + */ +public fun BooleanArray.single(): Boolean { + return when (size) { + 0 -> throw NoSuchElementException("Array is empty.") + 1 -> this[0] + else -> throw IllegalArgumentException("Array has more than one element.") + } +} + +/** + * Returns the single element, or throws an exception if the array is empty or has more than one element. + */ +public fun CharArray.single(): Char { + return when (size) { + 0 -> throw NoSuchElementException("Array is empty.") + 1 -> this[0] + else -> throw IllegalArgumentException("Array has more than one element.") + } +} + +/** + * Returns the single element matching the given [predicate], or throws exception if there is no or more than one matching element. + */ +public inline fun Array.single(predicate: (T) -> Boolean): T { + var single: T? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Array contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Array contains no element matching the predicate.") + return single as T +} + +/** + * Returns the single element matching the given [predicate], or throws exception if there is no or more than one matching element. + */ +public inline fun ByteArray.single(predicate: (Byte) -> Boolean): Byte { + var single: Byte? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Array contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Array contains no element matching the predicate.") + return single as Byte +} + +/** + * Returns the single element matching the given [predicate], or throws exception if there is no or more than one matching element. + */ +public inline fun ShortArray.single(predicate: (Short) -> Boolean): Short { + var single: Short? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Array contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Array contains no element matching the predicate.") + return single as Short +} + +/** + * Returns the single element matching the given [predicate], or throws exception if there is no or more than one matching element. + */ +public inline fun IntArray.single(predicate: (Int) -> Boolean): Int { + var single: Int? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Array contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Array contains no element matching the predicate.") + return single as Int +} + +/** + * Returns the single element matching the given [predicate], or throws exception if there is no or more than one matching element. + */ +public inline fun LongArray.single(predicate: (Long) -> Boolean): Long { + var single: Long? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Array contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Array contains no element matching the predicate.") + return single as Long +} + +/** + * Returns the single element matching the given [predicate], or throws exception if there is no or more than one matching element. + */ +public inline fun FloatArray.single(predicate: (Float) -> Boolean): Float { + var single: Float? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Array contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Array contains no element matching the predicate.") + return single as Float +} + +/** + * Returns the single element matching the given [predicate], or throws exception if there is no or more than one matching element. + */ +public inline fun DoubleArray.single(predicate: (Double) -> Boolean): Double { + var single: Double? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Array contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Array contains no element matching the predicate.") + return single as Double +} + +/** + * Returns the single element matching the given [predicate], or throws exception if there is no or more than one matching element. + */ +public inline fun BooleanArray.single(predicate: (Boolean) -> Boolean): Boolean { + var single: Boolean? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Array contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Array contains no element matching the predicate.") + return single as Boolean +} + +/** + * Returns the single element matching the given [predicate], or throws exception if there is no or more than one matching element. + */ +public inline fun CharArray.single(predicate: (Char) -> Boolean): Char { + var single: Char? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Array contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Array contains no element matching the predicate.") + return single as Char +} + +/** + * Returns single element, or `null` if the array is empty or has more than one element. + */ +public fun Array.singleOrNull(): T? { + return if (size == 1) this[0] else null +} /** * Applies the given [transform] function to each element of the original array @@ -578,7 +1241,6 @@ public fun Array.sum(): Double { } // From _Arrays.kt. - /** * Returns the range of valid indices for the array. */ @@ -633,6 +1295,59 @@ public val BooleanArray.indices: IntRange public val CharArray.indices: IntRange get() = IntRange(0, lastIndex) +/** + * Returns the last valid index for the array. + */ +public val Array.lastIndex: Int + get() = size - 1 + +/** + * Returns the last valid index for the array. + */ +public val ByteArray.lastIndex: Int + get() = size - 1 + +/** + * Returns the last valid index for the array. + */ +public val ShortArray.lastIndex: Int + get() = size - 1 + +/** + * Returns the last valid index for the array. + */ +public val IntArray.lastIndex: Int + get() = size - 1 + +/** + * Returns the last valid index for the array. + */ +public val LongArray.lastIndex: Int + get() = size - 1 + +/** + * Returns the last valid index for the array. + */ +public val FloatArray.lastIndex: Int + get() = size - 1 + +/** + * Returns the last valid index for the array. + */ +public val DoubleArray.lastIndex: Int + get() = size - 1 + +/** + * Returns the last valid index for the array. + */ +public val BooleanArray.lastIndex: Int + get() = size - 1 + +/** + * Returns the last valid index for the array. + */ +public val CharArray.lastIndex: Int + get() = size - 1 /** * Returns `true` if the array is empty. @@ -777,12 +1492,6 @@ public inline fun CharArray.isNotEmpty(): Boolean { return !isEmpty() } -/** - * Returns the last valid index for the array. - */ -public val Array.lastIndex: Int - get() = size - 1 - /** * Returns last index of [element], or -1 if the array does not contain element. */ @@ -803,54 +1512,6 @@ public fun <@kotlin.internal.OnlyInputTypes T> Array.lastIndexOf(element: return -1 } -/** - * Returns the last valid index for the array. - */ -public val ByteArray.lastIndex: Int - get() = size - 1 - -/** - * Returns the last valid index for the array. - */ -public val ShortArray.lastIndex: Int - get() = size - 1 - -/** - * Returns the last valid index for the array. - */ -public val IntArray.lastIndex: Int - get() = size - 1 - -/** - * Returns the last valid index for the array. - */ -public val LongArray.lastIndex: Int - get() = size - 1 - -/** - * Returns the last valid index for the array. - */ -public val FloatArray.lastIndex: Int - get() = size - 1 - -/** - * Returns the last valid index for the array. - */ -public val DoubleArray.lastIndex: Int - get() = size - 1 - -/** - * Returns the last valid index for the array. - */ -public val BooleanArray.lastIndex: Int - get() = size - 1 - -/** - * Returns the last valid index for the array. - */ -public val CharArray.lastIndex: Int - get() = size - 1 - /** * Appends all elements to the given [destination] collection. */ diff --git a/runtime/src/main/kotlin/kotlin/comparisons/Comparisons.kt b/runtime/src/main/kotlin/kotlin/comparisons/Comparisons.kt index 7611c0b2448..d0144d9e2ec 100644 --- a/runtime/src/main/kotlin/kotlin/comparisons/Comparisons.kt +++ b/runtime/src/main/kotlin/kotlin/comparisons/Comparisons.kt @@ -298,3 +298,37 @@ private object ReverseOrderComparator: Comparator> { @Suppress("VIRTUAL_MEMBER_HIDDEN") fun reversed(): Comparator> = NaturalOrderComparator } + +// From _Comparisions.kt. +/** + * Returns the greater of two values. + * If values are equal, returns the first one. + */ +public fun > maxOf(a: T, b: T): T { + return if (a >= b) a else b +} + +/** + * Returns the greater of two values. + */ +@kotlin.internal.InlineOnly +public inline fun maxOf(a: Int, b: Int): Int { + return if (a >= b) a else b +} + +/** + * Returns the smaller of two values. + * If values are equal, returns the first one. + */ +@SinceKotlin("1.1") +public fun > minOf(a: T, b: T): T { + return if (a <= b) a else b +} + +/** + * Returns the smaller of two values. + */ +@kotlin.internal.InlineOnly +public inline fun minOf(a: Int, b: Int): Int { + return if (a <= b) a else b +} diff --git a/runtime/src/main/kotlin/kotlin/text/Char.kt b/runtime/src/main/kotlin/kotlin/text/Char.kt new file mode 100644 index 00000000000..d37894a153a --- /dev/null +++ b/runtime/src/main/kotlin/kotlin/text/Char.kt @@ -0,0 +1,81 @@ +package kotlin.text + +/** + * Returns `true` if this character (Unicode code point) is defined in Unicode. + */ +@SymbolName("Kotlin_Char_isDefined") +external public fun Char.isDefined(): Boolean + +/** + * Returns `true` if this character is a letter. + */ +@SymbolName("Kotlin_Char_isLetter") +external public fun Char.isLetter(): Boolean + +/** + * Returns `true` if this character is a letter or digit. + */ +@SymbolName("Kotlin_Char_isLetterOrDigit") +external public fun Char.isLetterOrDigit(): Boolean + +/** + * Returns `true` if this character (Unicode code point) is a digit. + */ +@SymbolName("Kotlin_Char_isDigit") +external public fun Char.isDigit(): Boolean + +/** + * Returns `true` if this character (Unicode code point) should be regarded as an ignorable + * character in a Java identifier or a Unicode identifier. + */ +@SymbolName("Kotlin_Char_isIdentifierIgnorable") +external public fun Char.isIdentifierIgnorable(): Boolean + +/** + * Returns `true` if this character is an ISO control character. + */ +@SymbolName("Kotlin_Char_isISOControl") +external public inline fun Char.isISOControl(): Boolean + +/** + * Determines whether a character is whitespace according to the Unicode standard. + * Returns `true` if the character is whitespace. + */ +@SymbolName("Kotlin_Char_isWhitespace") +external public fun Char.isWhitespace(): Boolean + +/** + * Returns `true` if this character is upper case. + */ +@SymbolName("Kotlin_Char_isUpperCase") +external public fun Char.isUpperCase(): Boolean + +/** + * Returns `true` if this character is lower case. + */ +@SymbolName("Kotlin_Char_isLowerCase") +external public fun Char.isLowerCase(): Boolean + +/** + * Converts this character to uppercase. + */ +@SymbolName("Kotlin_Char_toUpperCase") +external public fun Char.toUpperCase(): Char + +/** + * Converts this character to lowercase. + */ +@SymbolName("Kotlin_Char_toLowerCase") +external public fun Char.toLowerCase(): Char + +/** + * Returns `true` if this character is a Unicode high-surrogate code unit (also known as leading-surrogate code unit). + */ +@SymbolName("Kotlin_Char_isHighSurrogate") +external public fun Char.isHighSurrogate(): Boolean + +/** + * Returns `true` if this character is a Unicode low-surrogate code unit (also known as trailing-surrogate code unit). + */ +@SymbolName("Kotlin_Char_isLowSurrogate") +external public fun Char.isLowSurrogate(): Boolean \ No newline at end of file diff --git a/runtime/src/main/kotlin/kotlin/text/Strings.kt b/runtime/src/main/kotlin/kotlin/text/Strings.kt new file mode 100644 index 00000000000..0d1e5db58f9 --- /dev/null +++ b/runtime/src/main/kotlin/kotlin/text/Strings.kt @@ -0,0 +1,1280 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kotlin.text + +import kotlin.comparisons.* + +/** + * Returns a sub sequence of this char sequence having leading and trailing characters matching the [predicate] trimmed. + */ +public inline fun CharSequence.trim(predicate: (Char) -> Boolean): CharSequence { + var startIndex = 0 + var endIndex = length - 1 + var startFound = false + + while (startIndex <= endIndex) { + val index = if (!startFound) startIndex else endIndex + val match = predicate(this[index]) + + if (!startFound) { + if (!match) + startFound = true + else + startIndex += 1 + } + else { + if (!match) + break + else + endIndex -= 1 + } + } + + return subSequence(startIndex, endIndex + 1) +} + +/** + * Returns a string with leading and trailing characters matching the [predicate] trimmed. + */ +public inline fun String.trim(predicate: (Char) -> Boolean): String + = (this as CharSequence).trim(predicate).toString() + +/** + * Returns a sub sequence of this char sequence having leading characters matching the [predicate] trimmed. + */ +public inline fun CharSequence.trimStart(predicate: (Char) -> Boolean): CharSequence { + for (index in this.indices) + if (!predicate(this[index])) + return subSequence(index, length) + + return "" +} + +/** + * Returns a string with leading characters matching the [predicate] trimmed. + */ +public inline fun String.trimStart(predicate: (Char) -> Boolean): String + = (this as CharSequence).trimStart(predicate).toString() + +/** + * Returns a sub sequence of this char sequence having trailing characters matching the [predicate] trimmed. + */ +public inline fun CharSequence.trimEnd(predicate: (Char) -> Boolean): CharSequence { + for (index in this.indices.reversed()) + if (!predicate(this[index])) + return substring(0, index + 1) + + return "" +} + +/** + * Returns a string with trailing characters matching the [predicate] trimmed. + */ +public inline fun String.trimEnd(predicate: (Char) -> Boolean): String + = (this as CharSequence).trimEnd(predicate).toString() + +/** + * Returns a sub sequence of this char sequence having leading and trailing characters from the [chars] array trimmed. + */ +public fun CharSequence.trim(vararg chars: Char): CharSequence = trim { it in chars } + +/** + * Returns a string with leading and trailing characters from the [chars] array trimmed. + */ +public fun String.trim(vararg chars: Char): String = trim { it in chars } + +/** + * Returns a sub sequence of this char sequence having leading and trailing characters from the [chars] array trimmed. + */ +public fun CharSequence.trimStart(vararg chars: Char): CharSequence = trimStart { it in chars } + +/** + * Returns a string with leading and trailing characters from the [chars] array trimmed. + */ +public fun String.trimStart(vararg chars: Char): String = trimStart { it in chars } + +/** + * Returns a sub sequence of this char sequence having trailing characters from the [chars] array trimmed. + */ +public fun CharSequence.trimEnd(vararg chars: Char): CharSequence = trimEnd { it in chars } + +/** + * Returns a string with trailing characters from the [chars] array trimmed. + */ +public fun String.trimEnd(vararg chars: Char): String = trimEnd { it in chars } + +/** + * Returns a sub sequence of this char sequence having leading and trailing whitespace trimmed. + */ +public fun CharSequence.trim(): CharSequence = trim { it.isWhitespace() } + +/** + * Returns a string with leading and trailing whitespace trimmed. + */ +@kotlin.internal.InlineOnly +public inline fun String.trim(): String = (this as CharSequence).trim().toString() + +/** + * Returns a sub sequence of this char sequence having leading whitespace removed. + */ +public fun CharSequence.trimStart(): CharSequence = trimStart { it.isWhitespace() } + +/** + * Returns a string with leading whitespace removed. + */ +@kotlin.internal.InlineOnly +public inline fun String.trimStart(): String = (this as CharSequence).trimStart().toString() + +/** + * Returns a sub sequence of this char sequence having trailing whitespace removed. + */ +public fun CharSequence.trimEnd(): CharSequence = trimEnd { it.isWhitespace() } + +/** + * Returns a string with trailing whitespace removed. + */ +@kotlin.internal.InlineOnly +public inline fun String.trimEnd(): String = (this as CharSequence).trimEnd().toString() + +/** + * Returns a char sequence with content of this char sequence padded at the beginning + * to the specified [length] with the specified character or space. + * + * @param length the desired string length. + * @param padChar the character to pad string with, if it has length less than the [length] specified. Space is used by default. + * @returns Returns a string, of length at least [length], consisting of string prepended with [padChar] as many times. + * as are necessary to reach that length. + */ +public fun CharSequence.padStart(length: Int, padChar: Char = ' '): CharSequence { + if (length < 0) + throw IllegalArgumentException("Desired length $length is less than zero.") + if (length <= this.length) + return this.subSequence(0, this.length) + + val sb = StringBuilder(length) + for (i in 1..(length - this.length)) + sb.append(padChar) + sb.append(this) + return sb +} + +/** + * Pads the string to the specified [length] at the beginning with the specified character or space. + * + * @param length the desired string length. + * @param padChar the character to pad string with, if it has length less than the [length] specified. Space is used by default. + * @returns Returns a string, of length at least [length], consisting of string prepended with [padChar] as many times. + * as are necessary to reach that length. + */ +public fun String.padStart(length: Int, padChar: Char = ' '): String + = (this as CharSequence).padStart(length, padChar).toString() + +/** + * Returns a char sequence with content of this char sequence padded at the end + * to the specified [length] with the specified character or space. + * + * @param length the desired string length. + * @param padChar the character to pad string with, if it has length less than the [length] specified. Space is used by default. + * @returns Returns a string, of length at least [length], consisting of string prepended with [padChar] as many times. + * as are necessary to reach that length. + */ +public fun CharSequence.padEnd(length: Int, padChar: Char = ' '): CharSequence { + if (length < 0) + throw IllegalArgumentException("Desired length $length is less than zero.") + if (length <= this.length) + return this.subSequence(0, this.length) + + val sb = StringBuilder(length) + sb.append(this) + for (i in 1..(length - this.length)) + sb.append(padChar) + return sb +} + +/** + * Pads the string to the specified [length] at the end with the specified character or space. + * + * @param length the desired string length. + * @param padChar the character to pad string with, if it has length less than the [length] specified. Space is used by default. + * @returns Returns a string, of length at least [length], consisting of string prepended with [padChar] as many times. + * as are necessary to reach that length. + */ +public fun String.padEnd(length: Int, padChar: Char = ' '): String + = (this as CharSequence).padEnd(length, padChar).toString() + +/** + * Returns `true` if this nullable char sequence is either `null` or empty. + */ +@kotlin.internal.InlineOnly +public inline fun CharSequence?.isNullOrEmpty(): Boolean = this == null || this.length == 0 + +/** + * Returns `true` if this char sequence is empty (contains no characters). + */ +@kotlin.internal.InlineOnly +public inline fun CharSequence.isEmpty(): Boolean = length == 0 + +/** + * Returns `true` if this char sequence is not empty. + */ +@kotlin.internal.InlineOnly +public inline fun CharSequence.isNotEmpty(): Boolean = length > 0 + +// implemented differently in JVM and JS +//public fun String.isBlank(): Boolean = length() == 0 || all { it.isWhitespace() } + + +/** + * Returns `true` if this char sequence is not empty and contains some characters except of whitespace characters. + */ +@kotlin.internal.InlineOnly +public inline fun CharSequence.isNotBlank(): Boolean = !isBlank() + +/** + * Returns `true` if this nullable char sequence is either `null` or empty or consists solely of whitespace characters. + */ +@kotlin.internal.InlineOnly +public inline fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank() + +/** + * Iterator for characters of the given char sequence. + */ +public operator fun CharSequence.iterator(): CharIterator = object : CharIterator() { + private var index = 0 + + public override fun nextChar(): Char = get(index++) + + public override fun hasNext(): Boolean = index < length +} + +/** Returns the string if it is not `null`, or the empty string otherwise. */ +@kotlin.internal.InlineOnly +public inline fun String?.orEmpty(): String = this ?: "" + +/** + * Returns the range of valid character indices for this char sequence. + */ +public val CharSequence.indices: IntRange + get() = 0..length - 1 + +/** + * Returns the index of the last character in the char sequence or -1 if it is empty. + */ +public val CharSequence.lastIndex: Int + get() = this.length - 1 + +/** + * Returns `true` if this CharSequence has Unicode surrogate pair at the specified [index]. + */ +public fun CharSequence.hasSurrogatePairAt(index: Int): Boolean { + return index in 0..length - 2 + && this[index].isHighSurrogate() + && this[index + 1].isLowSurrogate() +} + +/** + * Returns a substring specified by the given [range] of indices. + */ +public fun String.substring(range: IntRange): String = substring(range.start, range.endInclusive + 1) + +/** + * Returns a subsequence of this char sequence specified by the given [range] of indices. + */ +public fun CharSequence.subSequence(range: IntRange): CharSequence = subSequence(range.start, range.endInclusive + 1) + +/** + * Returns a subsequence of this char sequence. + * + * This extension is chosen only for invocation with old-named parameters. + * Replace parameter names with the same as those of [CharSequence.subSequence]. + */ +@kotlin.internal.InlineOnly +@Deprecated("Use parameters named startIndex and endIndex.", ReplaceWith("subSequence(startIndex = start, endIndex = end)")) +public inline fun String.subSequence(start: Int, end: Int): CharSequence = subSequence(start, end) + +/** + * Returns a substring of chars from a range of this char sequence starting at the [startIndex] and ending right before the [endIndex]. + * + * @param startIndex the start index (inclusive). + * @param endIndex the end index (exclusive). If not specified, the length of the char sequence is used. + */ +@kotlin.internal.InlineOnly +public inline fun CharSequence.substring(startIndex: Int, endIndex: Int = length): String = subSequence(startIndex, endIndex).toString() + +/** + * Returns a substring of chars at indices from the specified [range] of this char sequence. + */ +public fun CharSequence.substring(range: IntRange): String = subSequence(range.start, range.endInclusive + 1).toString() + +/** + * Returns a substring before the first occurrence of [delimiter]. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.substringBefore(delimiter: Char, missingDelimiterValue: String = this): String { + val index = indexOf(delimiter) + return if (index == -1) missingDelimiterValue else substring(0, index) +} + +/** + * Returns a substring before the first occurrence of [delimiter]. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.substringBefore(delimiter: String, missingDelimiterValue: String = this): String { + val index = indexOf(delimiter) + return if (index == -1) missingDelimiterValue else substring(0, index) +} + +/** + * Returns a substring after the first occurrence of [delimiter]. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.substringAfter(delimiter: Char, missingDelimiterValue: String = this): String { + val index = indexOf(delimiter) + return if (index == -1) missingDelimiterValue else substring(index + 1, length) +} + +/** + * Returns a substring after the first occurrence of [delimiter]. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.substringAfter(delimiter: String, missingDelimiterValue: String = this): String { + val index = indexOf(delimiter) + return if (index == -1) missingDelimiterValue else substring(index + delimiter.length, length) +} + +/** + * Returns a substring before the last occurrence of [delimiter]. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.substringBeforeLast(delimiter: Char, missingDelimiterValue: String = this): String { + val index = lastIndexOf(delimiter) + return if (index == -1) missingDelimiterValue else substring(0, index) +} + +/** + * Returns a substring before the last occurrence of [delimiter]. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.substringBeforeLast(delimiter: String, missingDelimiterValue: String = this): String { + val index = lastIndexOf(delimiter) + return if (index == -1) missingDelimiterValue else substring(0, index) +} + +/** + * Returns a substring after the last occurrence of [delimiter]. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.substringAfterLast(delimiter: Char, missingDelimiterValue: String = this): String { + val index = lastIndexOf(delimiter) + return if (index == -1) missingDelimiterValue else substring(index + 1, length) +} + +/** + * Returns a substring after the last occurrence of [delimiter]. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.substringAfterLast(delimiter: String, missingDelimiterValue: String = this): String { + val index = lastIndexOf(delimiter) + return if (index == -1) missingDelimiterValue else substring(index + delimiter.length, length) +} + +/** + * Returns a char sequence with content of this char sequence where its part at the given range + * is replaced with the [replacement] char sequence. + * @param startIndex the index of the first character to be replaced. + * @param endIndex the index of the first character after the replacement to keep in the string. + */ +public fun CharSequence.replaceRange(startIndex: Int, endIndex: Int, replacement: CharSequence): CharSequence { + if (endIndex < startIndex) + throw IndexOutOfBoundsException("End index ($endIndex) is less than start index ($startIndex).") + val sb = StringBuilder() + sb.append(this, 0, startIndex) + sb.append(replacement) + sb.append(this, endIndex, length) + return sb +} + +/** + * Replaces the part of the string at the given range with the [replacement] char sequence. + * @param startIndex the index of the first character to be replaced. + * @param endIndex the index of the first character after the replacement to keep in the string. + */ +@kotlin.internal.InlineOnly +public inline fun String.replaceRange(startIndex: Int, endIndex: Int, replacement: CharSequence): String + = (this as CharSequence).replaceRange(startIndex, endIndex, replacement).toString() + +/** + * Returns a char sequence with content of this char sequence where its part at the given [range] + * is replaced with the [replacement] char sequence. + * + * The end index of the [range] is included in the part to be replaced. + */ +public fun CharSequence.replaceRange(range: IntRange, replacement: CharSequence): CharSequence + = replaceRange(range.start, range.endInclusive + 1, replacement) + +/** + * Replace the part of string at the given [range] with the [replacement] string. + * + * The end index of the [range] is included in the part to be replaced. + */ +@kotlin.internal.InlineOnly +public inline fun String.replaceRange(range: IntRange, replacement: CharSequence): String + = (this as CharSequence).replaceRange(range, replacement).toString() + +/** + * Returns a char sequence with content of this char sequence where its part at the given range is removed. + * + * @param startIndex the index of the first character to be removed. + * @param endIndex the index of the first character after the removed part to keep in the string. + * + * [endIndex] is not included in the removed part. + */ +public fun CharSequence.removeRange(startIndex: Int, endIndex: Int): CharSequence { + if (endIndex < startIndex) + throw IndexOutOfBoundsException("End index ($endIndex) is less than start index ($startIndex).") + + if (endIndex == startIndex) + return this.subSequence(0, length) + + val sb = StringBuilder(length - (endIndex - startIndex)) + sb.append(this, 0, startIndex) + sb.append(this, endIndex, length) + return sb +} + +/** + * Removes the part of a string at a given range. + * @param startIndex the index of the first character to be removed. + * @param endIndex the index of the first character after the removed part to keep in the string. + * + * [endIndex] is not included in the removed part. + */ +@kotlin.internal.InlineOnly +public inline fun String.removeRange(startIndex: Int, endIndex: Int): String + = (this as CharSequence).removeRange(startIndex, endIndex).toString() + +/** + * Returns a char sequence with content of this char sequence where its part at the given [range] is removed. + * + * The end index of the [range] is included in the removed part. + */ +public fun CharSequence.removeRange(range: IntRange): CharSequence = removeRange(range.start, range.endInclusive + 1) + +/** + * Removes the part of a string at the given [range]. + * + * The end index of the [range] is included in the removed part. + */ +@kotlin.internal.InlineOnly +public inline fun String.removeRange(range: IntRange): String + = (this as CharSequence).removeRange(range).toString() + +/** + * If this char sequence starts with the given [prefix], returns a new char sequence + * with the prefix removed. Otherwise, returns a new char sequence with the same characters. + */ +public fun CharSequence.removePrefix(prefix: CharSequence): CharSequence { + if (startsWith(prefix)) { + return subSequence(prefix.length, length) + } + return subSequence(0, length) +} + +/** + * If this string starts with the given [prefix], returns a copy of this string + * with the prefix removed. Otherwise, returns this string. + */ +public fun String.removePrefix(prefix: CharSequence): String { + if (startsWith(prefix)) { + return substring(prefix.length) + } + return this +} + +/** + * If this char sequence ends with the given [suffix], returns a new char sequence + * with the suffix removed. Otherwise, returns a new char sequence with the same characters. + */ +public fun CharSequence.removeSuffix(suffix: CharSequence): CharSequence { + if (endsWith(suffix)) { + return subSequence(0, length - suffix.length) + } + return subSequence(0, length) +} + +/** + * If this string ends with the given [suffix], returns a copy of this string + * with the suffix removed. Otherwise, returns this string. + */ +public fun String.removeSuffix(suffix: CharSequence): String { + if (endsWith(suffix)) { + return substring(0, length - suffix.length) + } + return this +} + +/** + * When this char sequence starts with the given [prefix] and ends with the given [suffix], + * returns a new char sequence having both the given [prefix] and [suffix] removed. + * Otherwise returns a new char sequence with the same characters. + */ +public fun CharSequence.removeSurrounding(prefix: CharSequence, suffix: CharSequence): CharSequence { + if ((length >= prefix.length + suffix.length) && startsWith(prefix) && endsWith(suffix)) { + return subSequence(prefix.length, length - suffix.length) + } + return subSequence(0, length) +} + +/** + * Removes from a string both the given [prefix] and [suffix] if and only if + * it starts with the [prefix] and ends with the [suffix]. + * Otherwise returns this string unchanged. + */ +public fun String.removeSurrounding(prefix: CharSequence, suffix: CharSequence): String { + if ((length >= prefix.length + suffix.length) && startsWith(prefix) && endsWith(suffix)) { + return substring(prefix.length, length - suffix.length) + } + return this +} + +/** + * When this char sequence starts with and ends with the given [delimiter], + * returns a new char sequence having this [delimiter] removed both from the start and end. + * Otherwise returns a new char sequence with the same characters. + */ +public fun CharSequence.removeSurrounding(delimiter: CharSequence): CharSequence = removeSurrounding(delimiter, delimiter) + +/** + * Removes the given [delimiter] string from both the start and the end of this string + * if and only if it starts with and ends with the [delimiter]. + * Otherwise returns this string unchanged. + */ +public fun String.removeSurrounding(delimiter: CharSequence): String = removeSurrounding(delimiter, delimiter) + +/** + * Replace part of string before the first occurrence of given delimiter with the [replacement] string. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.replaceBefore(delimiter: Char, replacement: String, missingDelimiterValue: String = this): String { + val index = indexOf(delimiter) + return if (index == -1) missingDelimiterValue else replaceRange(0, index, replacement) +} + +/** + * Replace part of string before the first occurrence of given delimiter with the [replacement] string. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.replaceBefore(delimiter: String, replacement: String, missingDelimiterValue: String = this): String { + val index = indexOf(delimiter) + return if (index == -1) missingDelimiterValue else replaceRange(0, index, replacement) +} + +/** + * Replace part of string after the first occurrence of given delimiter with the [replacement] string. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.replaceAfter(delimiter: Char, replacement: String, missingDelimiterValue: String = this): String { + val index = indexOf(delimiter) + return if (index == -1) missingDelimiterValue else replaceRange(index + 1, length, replacement) +} + +/** + * Replace part of string after the first occurrence of given delimiter with the [replacement] string. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.replaceAfter(delimiter: String, replacement: String, missingDelimiterValue: String = this): String { + val index = indexOf(delimiter) + return if (index == -1) missingDelimiterValue else replaceRange(index + delimiter.length, length, replacement) +} + +/** + * Replace part of string after the last occurrence of given delimiter with the [replacement] string. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.replaceAfterLast(delimiter: String, replacement: String, missingDelimiterValue: String = this): String { + val index = lastIndexOf(delimiter) + return if (index == -1) missingDelimiterValue else replaceRange(index + delimiter.length, length, replacement) +} + +/** + * Replace part of string after the last occurrence of given delimiter with the [replacement] string. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.replaceAfterLast(delimiter: Char, replacement: String, missingDelimiterValue: String = this): String { + val index = lastIndexOf(delimiter) + return if (index == -1) missingDelimiterValue else replaceRange(index + 1, length, replacement) +} + +/** + * Replace part of string before the last occurrence of given delimiter with the [replacement] string. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.replaceBeforeLast(delimiter: Char, replacement: String, missingDelimiterValue: String = this): String { + val index = lastIndexOf(delimiter) + return if (index == -1) missingDelimiterValue else replaceRange(0, index, replacement) +} + +/** + * Replace part of string before the last occurrence of given delimiter with the [replacement] string. + * If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string. + */ +public fun String.replaceBeforeLast(delimiter: String, replacement: String, missingDelimiterValue: String = this): String { + val index = lastIndexOf(delimiter) + return if (index == -1) missingDelimiterValue else replaceRange(0, index, replacement) +} + + +// public fun String.replace(oldChar: Char, newChar: Char, ignoreCase: Boolean): String // JVM- and JS-specific +// public fun String.replace(oldValue: String, newValue: String, ignoreCase: Boolean): String // JVM- and JS-specific + +/** + * Returns a new string obtained by replacing each substring of this char sequence that matches the given regular expression + * with the given [replacement]. + * + * The [replacement] can consist of any combination of literal text and $-substitutions. To treat the replacement string + * literally escape it with the [kotlin.text.Regex.Companion.escapeReplacement] method. + */ +//@FixmeRegex +//@kotlin.internal.InlineOnly +//public inline fun CharSequence.replace(regex: Regex, replacement: String): String = regex.replace(this, replacement) + +/** + * Returns a new string obtained by replacing each substring of this char sequence that matches the given regular expression + * with the result of the given function [transform] that takes [MatchResult] and returns a string to be used as a + * replacement for that match. + */ +//@FixmeRegex +//@kotlin.internal.InlineOnly +//public inline fun CharSequence.replace(regex: Regex, noinline transform: (MatchResult) -> CharSequence): String = regex.replace(this, transform) + +/** + * Replaces the first occurrence of the given regular expression [regex] in this char sequence with specified [replacement] expression. + * + * @param replacement A replacement expression that can include substitutions. See [Regex.replaceFirst] for details. + */ +//@FixmeRegex +//@kotlin.internal.InlineOnly +//public inline fun CharSequence.replaceFirst(regex: Regex, replacement: String): String = regex.replaceFirst(this, replacement) + + +/** + * Returns `true` if this char sequence matches the given regular expression. + */ +//@FixmeRegex +//@kotlin.internal.InlineOnly +//public inline fun CharSequence.matches(regex: Regex): Boolean = regex.matches(this) + +/** + * Implementation of [regionMatches] for CharSequences. + * Invoked when it's already known that arguments are not Strings, so that no additional type checks are performed. + */ +internal fun CharSequence.regionMatchesImpl(thisOffset: Int, other: CharSequence, otherOffset: Int, length: Int, ignoreCase: Boolean): Boolean { + if ((otherOffset < 0) || (thisOffset < 0) || (thisOffset > this.length - length) + || (otherOffset > other.length - length)) { + return false + } + + for (index in 0..length-1) { + if (!this[thisOffset + index].equals(other[otherOffset + index], ignoreCase)) + return false + } + return true +} + +/** + * Returns `true` if this char sequence starts with the specified character. + */ +public fun CharSequence.startsWith(char: Char, ignoreCase: Boolean = false): Boolean = + this.length > 0 && this[0].equals(char, ignoreCase) + +/** + * Returns `true` if this char sequence ends with the specified character. + */ +public fun CharSequence.endsWith(char: Char, ignoreCase: Boolean = false): Boolean = + this.length > 0 && this[lastIndex].equals(char, ignoreCase) + +/** + * Returns `true` if this char sequence starts with the specified prefix. + */ +public fun CharSequence.startsWith(prefix: CharSequence, ignoreCase: Boolean = false): Boolean { + if (!ignoreCase && this is String && prefix is String) + return this.startsWith(prefix) + else + return regionMatchesImpl(0, prefix, 0, prefix.length, ignoreCase) +} + +/** + * Returns `true` if a substring of this char sequence starting at the specified offset [startIndex] starts with the specified prefix. + */ +public fun CharSequence.startsWith(prefix: CharSequence, startIndex: Int, ignoreCase: Boolean = false): Boolean { + if (!ignoreCase && this is String && prefix is String) + return this.startsWith(prefix, startIndex) + else + return regionMatchesImpl(startIndex, prefix, 0, prefix.length, ignoreCase) +} + +/** + * Returns `true` if this char sequence ends with the specified suffix. + */ +public fun CharSequence.endsWith(suffix: CharSequence, ignoreCase: Boolean = false): Boolean { + if (!ignoreCase && this is String && suffix is String) + return this.endsWith(suffix) + else + return regionMatchesImpl(length - suffix.length, suffix, 0, suffix.length, ignoreCase) +} + + +// common prefix and suffix + +/** + * Returns the longest string `prefix` such that this char sequence and [other] char sequence 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 = minOf(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 char sequence and [other] char sequence 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 = minOf(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 CharSequence.findAnyOf(chars: CharArray, startIndex: Int, ignoreCase: Boolean, last: Boolean): Pair? { + if (!ignoreCase && chars.size == 1 && this is String) { + 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) startIndex.coerceAtLeast(0)..lastIndex else startIndex.coerceAtMost(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 index of the first occurrence of any of the specified [chars] in this char sequence, + * 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 An index of the first occurrence of matched character from [chars] or -1 if none of [chars] are found. + * + */ +public fun CharSequence.indexOfAny(chars: CharArray, startIndex: Int = 0, ignoreCase: Boolean = false): Int = + findAnyOf(chars, startIndex, ignoreCase, last = false)?.first ?: -1 + +/** + * Finds the index of the last occurrence of any of the specified [chars] in this char sequence, + * 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 An index of the last occurrence of matched character from [chars] or -1 if none of [chars] are found. + * + */ +public fun CharSequence.lastIndexOfAny(chars: CharArray, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Int = + findAnyOf(chars, startIndex, ignoreCase, last = true)?.first ?: -1 + + +private fun CharSequence.indexOf(other: CharSequence, startIndex: Int, endIndex: Int, ignoreCase: Boolean, last: Boolean = false): Int { + val indices = if (!last) + startIndex.coerceAtLeast(0)..endIndex.coerceAtMost(length) + else + startIndex.coerceAtMost(lastIndex) downTo endIndex.coerceAtLeast(0) + + if (this is String && other is String) { // smart cast + for (index in indices) { + if (other.regionMatches(0, this, index, other.length, ignoreCase)) + return index + } + } else { + for (index in indices) { + if (other.regionMatchesImpl(0, this, index, other.length, ignoreCase)) + return index + } + } + return -1 +} + +private fun CharSequence.findAnyOf(strings: Collection, startIndex: Int, ignoreCase: Boolean, last: Boolean): Pair? { + if (!ignoreCase && strings.size == 1) { + val string = strings.single() + val index = if (!last) indexOf(string, startIndex) else lastIndexOf(string, startIndex) + return if (index < 0) null else index to string + } + + val indices = if (!last) startIndex.coerceAtLeast(0)..length else startIndex.coerceAtMost(lastIndex) downTo 0 + + if (this is String) { + for (index in indices) { + val matchingString = strings.firstOrNull { it.regionMatches(0, this, index, it.length, ignoreCase) } + if (matchingString != null) + return index to matchingString + } + } else { + for (index in indices) { + val matchingString = strings.firstOrNull { it.regionMatchesImpl(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 char sequence, + * 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 [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 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 CharSequence.findAnyOf(strings: Collection, startIndex: Int = 0, ignoreCase: Boolean = false): Pair? = + findAnyOf(strings, startIndex, ignoreCase, last = false) + +/** + * Finds the last occurrence of any of the specified [strings] in this char sequence, + * 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 CharSequence.findLastAnyOf(strings: Collection, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Pair? = + findAnyOf(strings, startIndex, ignoreCase, last = true) + +/** + * Finds the index of the first occurrence of any of the specified [strings] in this char sequence, + * 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 An index of the first occurrence of matched string from [strings] or -1 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 CharSequence.indexOfAny(strings: Collection, startIndex: Int = 0, ignoreCase: Boolean = false): Int = + findAnyOf(strings, startIndex, ignoreCase, last = false)?.first ?: -1 + +/** + * Finds the index of the last occurrence of any of the specified [strings] in this char sequence, + * 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 An index of the last occurrence of matched string from [strings] or -1 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 CharSequence.lastIndexOfAny(strings: Collection, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Int = + findAnyOf(strings, startIndex, ignoreCase, last = true)?.first ?: -1 + + +// 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 CharSequence.indexOf(char: Char, startIndex: Int = 0, ignoreCase: Boolean = false): Int { + return if (ignoreCase || this !is String) + indexOfAny(charArrayOf(char), startIndex, ignoreCase) + else + nativeIndexOf(char, startIndex) +} + +/** + * Returns the index within this char sequence 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 CharSequence.indexOf(string: String, startIndex: Int = 0, ignoreCase: Boolean = false): Int { + return if (ignoreCase || this !is String) + indexOf(string, startIndex, length, ignoreCase) + else + nativeIndexOf(string, startIndex) +} + +/** + * Returns the index within this char sequence 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 CharSequence.lastIndexOf(char: Char, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Int { + return if (ignoreCase || this !is String) + lastIndexOfAny(charArrayOf(char), startIndex, ignoreCase) + else + nativeLastIndexOf(char, startIndex) +} + +/** + * Returns the index within this char sequence 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 CharSequence.lastIndexOf(string: String, startIndex: Int = lastIndex, ignoreCase: Boolean = false): Int { + return if (ignoreCase || this !is String) + indexOf(string, startIndex, 0, ignoreCase, last = true) + else + nativeLastIndexOf(string, startIndex) +} + +/** + * Returns `true` if this char sequence contains the specified [other] sequence of characters as a substring. + * + * @param ignoreCase `true` to ignore character case when comparing strings. By default `false`. + */ +@Suppress("INAPPLICABLE_OPERATOR_MODIFIER") +public operator fun CharSequence.contains(other: CharSequence, ignoreCase: Boolean = false): Boolean = + if (other is String) + indexOf(other, ignoreCase = ignoreCase) >= 0 + else + indexOf(other, 0, length, ignoreCase) >= 0 + + + +/** + * Returns `true` if this char sequence contains the specified character [char]. + * + * @param ignoreCase `true` to ignore character case when comparing characters. By default `false`. + */ +@Suppress("INAPPLICABLE_OPERATOR_MODIFIER") +public operator fun CharSequence.contains(char: Char, ignoreCase: Boolean = false): Boolean = + indexOf(char, ignoreCase = ignoreCase) >= 0 + +/** + * Returns `true` if this char sequence contains at least one match of the specified regular expression [regex]. + */ +//@FixmeRegex +//@kotlin.internal.InlineOnly +//public inline operator fun CharSequence.contains(regex: Regex): Boolean = regex.containsMatchIn(this) + + +// rangesDelimitedBy + + +private class DelimitedRangesSequence(private val input: CharSequence, private val startIndex: Int, private val limit: Int, private val getNextMatch: CharSequence.(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 = startIndex.coerceIn(0, input.length) + var nextSearchIndex: Int = currentStartIndex + var nextItem: IntRange? = null + var counter: Int = 0 + + private fun calcNext() { + if (nextSearchIndex < 0) { + nextState = 0 + nextItem = null + } + else { + if (limit > 0 && ++counter >= limit || nextSearchIndex > input.length) { + nextItem = currentStartIndex..input.lastIndex + nextSearchIndex = -1 + } + else { + val match = input.getNextMatch(nextSearchIndex) + if (match == null) { + nextItem = currentStartIndex..input.lastIndex + nextSearchIndex = -1 + } + else { + val (index,length) = match + nextItem = currentStartIndex..index-1 + currentStartIndex = index + length + nextSearchIndex = currentStartIndex + if (length == 0) 1 else 0 + } + } + 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 char sequence 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 CharSequence.rangesDelimitedBy(delimiters: CharArray, 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 -> findAnyOf(delimiters, startIndex, ignoreCase = ignoreCase, last = false)?.let { it.first to 1 } }) +} + + +/** + * Returns a sequence of index ranges of substrings in this char sequence 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 CharSequence.rangesDelimitedBy(delimiters: Array, 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 -> findAnyOf(delimitersList, startIndex, ignoreCase = ignoreCase, last = false)?.let { it.first to it.second.length } }) + +} + + +// split + +/** + * Splits this char sequence 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 CharSequence.splitToSequence(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): Sequence = + rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).map { substring(it) } + +/** + * Splits this char sequence 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 CharSequence.split(vararg delimiters: String, ignoreCase: Boolean = false, limit: Int = 0): List = + rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) } + +/** + * Splits this char sequence 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 CharSequence.splitToSequence(vararg delimiters: Char, ignoreCase: Boolean = false, limit: Int = 0): Sequence = + rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).map { substring(it) } + +/** + * Splits this char sequence 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 CharSequence.split(vararg delimiters: Char, ignoreCase: Boolean = false, limit: Int = 0): List = + rangesDelimitedBy(delimiters, ignoreCase = ignoreCase, limit = limit).asIterable().map { substring(it) } + +/** + * Splits this char sequence around matches of the given regular expression. + * + * @param limit Non-negative value specifying the maximum number of substrings to return. + * Zero by default means no limit is set. + */ +//@FixmeRegex +//@kotlin.internal.InlineOnly +//public inline fun CharSequence.split(regex: Regex, limit: Int = 0): List = regex.split(this, limit) + +/** + * Splits this char sequence to a sequence of lines delimited by any of the following character sequences: CRLF, LF or CR. + */ +public fun CharSequence.lineSequence(): Sequence = splitToSequence("\r\n", "\n", "\r") + +/** + * * Splits this char sequence to a list of lines delimited by any of the following character sequences: CRLF, LF or CR. + */ +public fun CharSequence.lines(): List = lineSequence().toList() + +// From _Strings.kt. +/** + * Returns index of the first character matching the given [predicate], or -1 if the char sequence does not contain such character. + */ +public inline fun CharSequence.indexOfFirst(predicate: (Char) -> Boolean): Int { + for (index in indices) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns index of the last character matching the given [predicate], or -1 if the char sequence does not contain such character. + */ +public inline fun CharSequence.indexOfLast(predicate: (Char) -> Boolean): Int { + for (index in indices.reversed()) { + if (predicate(this[index])) { + return index + } + } + return -1 +} + +/** + * Returns the last character. + * @throws [NoSuchElementException] if the char sequence is empty. + */ +public fun CharSequence.last(): Char { + if (isEmpty()) + throw NoSuchElementException("Char sequence is empty.") + return this[lastIndex] +} + +/** + * Returns the single character, or throws an exception if the char sequence is empty or has more than one character. + */ +public fun CharSequence.single(): Char { + return when (length) { + 0 -> throw NoSuchElementException("Char sequence is empty.") + 1 -> this[0] + else -> throw IllegalArgumentException("Char sequence has more than one element") + } +} + +/** + * Returns the single character matching the given [predicate], or throws exception if there is no or more than one matching character. + */ +public inline fun CharSequence.single(predicate: (Char) -> Boolean): Char { + var single: Char? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) throw IllegalArgumentException("Char sequence contains more than one matching element.") + single = element + found = true + } + } + if (!found) throw NoSuchElementException("Char sequence contains no character matching the predicate.") + return single as Char +} + +/** + * Returns single character, or `null` if the char sequence is empty or has more than one character. + */ +public fun CharSequence.singleOrNull(): Char? { + return if (length == 1) this[0] else null +} + +/** + * Returns the single character matching the given [predicate], or `null` if character was not found or more than one character was found. + */ +public inline fun CharSequence.singleOrNull(predicate: (Char) -> Boolean): Char? { + var single: Char? = null + var found = false + for (element in this) { + if (predicate(element)) { + if (found) return null + single = element + found = true + } + } + if (!found) return null + return single +} \ No newline at end of file