[WASM] Regex std implementation
This commit is contained in:
committed by
TeamCityServer
parent
4f9b54da26
commit
d55e16a030
+3
-3
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package kotlin.text
|
||||
@@ -8,7 +8,7 @@ package kotlin.text
|
||||
/**
|
||||
* An object to which char sequences and values can be appended.
|
||||
*/
|
||||
actual interface Appendable {
|
||||
public actual interface Appendable {
|
||||
/**
|
||||
* Appends the specified character [value] to this Appendable and returns this instance.
|
||||
*
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.text
|
||||
|
||||
import kotlin.IllegalArgumentException
|
||||
|
||||
/**
|
||||
* Returns `true` if this character (Unicode code point) is defined in Unicode.
|
||||
*
|
||||
* A character is considered to be defined in Unicode if its [category] is not [CharCategory.UNASSIGNED].
|
||||
*/
|
||||
public actual fun Char.isDefined(): Boolean {
|
||||
if (this < '\u0080') {
|
||||
return true
|
||||
}
|
||||
return getCategoryValue() != CharCategory.UNASSIGNED.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a letter.
|
||||
*
|
||||
* A character is considered to be a letter if its [category] is [CharCategory.UPPERCASE_LETTER],
|
||||
* [CharCategory.LOWERCASE_LETTER], [CharCategory.TITLECASE_LETTER], [CharCategory.MODIFIER_LETTER], or [CharCategory.OTHER_LETTER].
|
||||
*
|
||||
* @sample samples.text.Chars.isLetter
|
||||
*/
|
||||
public actual fun Char.isLetter(): Boolean {
|
||||
if (this in 'a'..'z' || this in 'A'..'Z') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return isLetterImpl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a letter or digit.
|
||||
*
|
||||
* @see isLetter
|
||||
* @see isDigit
|
||||
*
|
||||
* @sample samples.text.Chars.isLetterOrDigit
|
||||
*/
|
||||
public actual fun Char.isLetterOrDigit(): Boolean {
|
||||
if (this in 'a'..'z' || this in 'A'..'Z' || this in '0'..'9') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
|
||||
return isDigit() || isLetter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a digit.
|
||||
*
|
||||
* A character is considered to be a digit if its [category] is [CharCategory.DECIMAL_DIGIT_NUMBER].
|
||||
*
|
||||
* @sample samples.text.Chars.isDigit
|
||||
*/
|
||||
public actual fun Char.isDigit(): Boolean {
|
||||
if (this in '0'..'9') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return isDigitImpl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a character is whitespace according to the Unicode standard.
|
||||
* Returns `true` if the character is whitespace.
|
||||
*
|
||||
* @sample samples.text.Chars.isWhitespace
|
||||
*/
|
||||
public actual fun Char.isWhitespace(): Boolean = isWhitespaceImpl()
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is upper case.
|
||||
*
|
||||
* A character is considered to be an upper case character if its [category] is [CharCategory.UPPERCASE_LETTER],
|
||||
* or it has contributory property `Other_Uppercase` as defined by the Unicode Standard.
|
||||
*
|
||||
* @sample samples.text.Chars.isUpperCase
|
||||
*/
|
||||
public actual fun Char.isUpperCase(): Boolean {
|
||||
if (this in 'A'..'Z') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return isUpperCaseImpl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is lower case.
|
||||
*
|
||||
* A character is considered to be a lower case character if its [category] is [CharCategory.LOWERCASE_LETTER],
|
||||
* or it has contributory property `Other_Lowercase` as defined by the Unicode Standard.
|
||||
*
|
||||
* @sample samples.text.Chars.isLowerCase
|
||||
*/
|
||||
public actual fun Char.isLowerCase(): Boolean {
|
||||
if (this in 'a'..'z') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return isLowerCaseImpl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a title case letter.
|
||||
*
|
||||
* A character is considered to be a title case letter if its [category] is [CharCategory.TITLECASE_LETTER].
|
||||
*
|
||||
* @sample samples.text.Chars.isTitleCase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isTitleCase(): Boolean {
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return getCategoryValue() == CharCategory.TITLECASE_LETTER.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this character to upper case using Unicode mapping rules of the invariant locale.
|
||||
*/
|
||||
@Deprecated("Use uppercaseChar() instead.", ReplaceWith("uppercaseChar()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun Char.toUpperCase(): Char = uppercaseCharImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to upper case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function performs one-to-one character mapping.
|
||||
* To support one-to-many character mapping use the [uppercase] function.
|
||||
* If this character has no mapping equivalent, the character itself is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.uppercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun Char.uppercaseChar(): Char = uppercaseCharImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to upper case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function supports one-to-many character mapping, thus the length of the returned string can be greater than one.
|
||||
* For example, `'\uFB00'.uppercase()` returns `"\u0046\u0046"`,
|
||||
* where `'\uFB00'` is the LATIN SMALL LIGATURE FF character (`ff`).
|
||||
* If this character has no upper case mapping, the result of `toString()` of this char is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.uppercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun Char.uppercase(): String = uppercaseImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to lower case using Unicode mapping rules of the invariant locale.
|
||||
*/
|
||||
@Deprecated("Use lowercaseChar() instead.", ReplaceWith("lowercaseChar()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun Char.toLowerCase(): Char = lowercaseCharImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to lower case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function performs one-to-one character mapping.
|
||||
* To support one-to-many character mapping use the [lowercase] function.
|
||||
* If this character has no mapping equivalent, the character itself is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.lowercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun Char.lowercaseChar(): Char = lowercaseCharImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to lower case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function supports one-to-many character mapping, thus the length of the returned string can be greater than one.
|
||||
* For example, `'\u0130'.lowercase()` returns `"\u0069\u0307"`,
|
||||
* where `'\u0130'` is the LATIN CAPITAL LETTER I WITH DOT ABOVE character (`İ`).
|
||||
* If this character has no lower case mapping, the result of `toString()` of this char is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.lowercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun Char.lowercase(): String = lowercaseImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to title case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function performs one-to-one character mapping.
|
||||
* To support one-to-many character mapping use the [titlecase] function.
|
||||
* If this character has no mapping equivalent, the result of calling [uppercaseChar] is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.titlecase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.titlecaseChar(): Char = titlecaseCharImpl()
|
||||
|
||||
/**
|
||||
* Returns the Unicode general category of this character.
|
||||
*/
|
||||
public actual val Char.category: CharCategory
|
||||
get() = CharCategory.valueOf(getCategoryValue())
|
||||
|
||||
/**
|
||||
* Checks whether the given [radix] is valid radix for string to number and number to string conversion.
|
||||
*/
|
||||
@PublishedApi
|
||||
internal actual fun checkRadix(radix: Int): Int {
|
||||
if(radix !in Char.MIN_RADIX..Char.MAX_RADIX) {
|
||||
throw IllegalArgumentException("radix $radix was not in valid range ${Char.MIN_RADIX..Char.MAX_RADIX}")
|
||||
}
|
||||
return radix
|
||||
}
|
||||
|
||||
// Char.Compaion methods. Konan specific.
|
||||
|
||||
// TODO: Make public when supplementary codepoints are supported.
|
||||
/** Converts a unicode code point to lower case. */
|
||||
internal fun Char.Companion.toLowerCase(codePoint: Int): Int =
|
||||
if (codePoint < MIN_SUPPLEMENTARY_CODE_POINT) {
|
||||
@Suppress("DEPRECATION")
|
||||
codePoint.toChar().lowercaseChar().toInt()
|
||||
} else {
|
||||
codePoint // TODO: Implement this transformation for supplementary codepoints.
|
||||
}
|
||||
|
||||
/** Converts a unicode code point to upper case. */
|
||||
internal fun Char.Companion.toUpperCase(codePoint: Int): Int =
|
||||
if (codePoint < MIN_SUPPLEMENTARY_CODE_POINT) {
|
||||
@Suppress("DEPRECATION")
|
||||
codePoint.toChar().uppercaseChar().toInt()
|
||||
} else {
|
||||
codePoint // TODO: Implement this transformation for supplementary codepoints.
|
||||
}
|
||||
|
||||
/** Converts a surrogate pair to a unicode code point. Doesn't validate that the characters are a valid surrogate pair. */
|
||||
// TODO: Consider removing from public API
|
||||
public fun Char.Companion.toCodePoint(high: Char, low: Char): Int =
|
||||
(((high - MIN_HIGH_SURROGATE) shl 10) or (low - MIN_LOW_SURROGATE)) + 0x10000
|
||||
|
||||
/** Checks if the codepoint specified is a supplementary codepoint or not. */
|
||||
// TODO: Consider removing from public API
|
||||
public fun Char.Companion.isSupplementaryCodePoint(codepoint: Int): Boolean =
|
||||
codepoint in MIN_SUPPLEMENTARY_CODE_POINT..MAX_CODE_POINT
|
||||
|
||||
// TODO: Consider removing from public API
|
||||
public fun Char.Companion.isSurrogatePair(high: Char, low: Char): Boolean = high.isHighSurrogate() && low.isLowSurrogate()
|
||||
|
||||
/**
|
||||
* Converts the codepoint specified to a char array. If the codepoint is not supplementary, the method will
|
||||
* return an array with one element otherwise it will return an array A with a high surrogate in A[0] and
|
||||
* a low surrogate in A[1].
|
||||
*/
|
||||
// TODO: Consider removing from public API
|
||||
@Suppress("DEPRECATION")
|
||||
public fun Char.Companion.toChars(codePoint: Int): CharArray =
|
||||
when {
|
||||
codePoint in 0 until MIN_SUPPLEMENTARY_CODE_POINT -> charArrayOf(codePoint.toChar())
|
||||
codePoint in MIN_SUPPLEMENTARY_CODE_POINT..MAX_CODE_POINT -> {
|
||||
val low = ((codePoint - 0x10000) and 0x3FF) + MIN_LOW_SURROGATE.toInt()
|
||||
val high = (((codePoint - 0x10000) ushr 10) and 0x3FF) + MIN_HIGH_SURROGATE.toInt()
|
||||
charArrayOf(high.toChar(), low.toChar())
|
||||
}
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
+7
-9
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.text
|
||||
|
||||
/**
|
||||
@@ -166,11 +165,10 @@ public actual enum class CharCategory(public val value: Int, public actual val c
|
||||
|
||||
public companion object {
|
||||
public fun valueOf(category: Int): CharCategory =
|
||||
when (category) {
|
||||
in 0..16 -> values()[category]
|
||||
in 18..30 -> values()[category - 1]
|
||||
else -> throw IllegalArgumentException("Category #$category is not defined.")
|
||||
}
|
||||
|
||||
when (category) {
|
||||
in 0..16 -> values()[category]
|
||||
in 18..30 -> values()[category - 1]
|
||||
else -> throw IllegalArgumentException("Category #$category is not defined.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package kotlin.text
|
||||
|
||||
/**
|
||||
* Encapsulates a syntax error that occurred during the compilation of a
|
||||
* [Pattern]. Might include a detailed description, the original regular
|
||||
* expression, and the index at which the error occurred.
|
||||
*/
|
||||
internal class PatternSyntaxException(
|
||||
val description: String = "",
|
||||
val pattern: String = "",
|
||||
val index: Int = -1
|
||||
) : IllegalArgumentException(formatMessage(description, pattern, index)) {
|
||||
companion object {
|
||||
fun formatMessage(description: String, pattern: String, index: Int): String {
|
||||
if (index < 0 || pattern == "") {
|
||||
return description
|
||||
}
|
||||
|
||||
val filler = if (index >= 1) " ".repeat(index) else ""
|
||||
return """
|
||||
$description near index: $index
|
||||
$pattern
|
||||
$filler^
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.text
|
||||
|
||||
import kotlin.text.regex.*
|
||||
|
||||
@PublishedApi
|
||||
internal interface FlagEnum {
|
||||
val value: Int
|
||||
val mask: Int
|
||||
}
|
||||
|
||||
private fun Iterable<FlagEnum>.toInt(): Int = this.fold(0, { value, option -> value or option.value })
|
||||
|
||||
private fun fromInt(value: Int): Set<RegexOption> =
|
||||
RegexOption.values().filterTo(mutableSetOf<RegexOption>()) { value and it.mask == it.value }
|
||||
|
||||
/**
|
||||
* Provides enumeration values to use to set regular expression options.
|
||||
*/
|
||||
public actual enum class RegexOption(override val value: Int, override val mask: Int = value) : FlagEnum {
|
||||
// common
|
||||
|
||||
/** Enables case-insensitive matching. Case comparison is Unicode-aware. */
|
||||
IGNORE_CASE(Pattern.CASE_INSENSITIVE),
|
||||
|
||||
/**
|
||||
* Enables multiline mode.
|
||||
*
|
||||
* In multiline mode the expressions `^` and `$` match just after or just before,
|
||||
* respectively, a line terminator or the end of the input sequence.
|
||||
*/
|
||||
MULTILINE(Pattern.MULTILINE),
|
||||
|
||||
/**
|
||||
* Enables literal parsing of the pattern.
|
||||
*
|
||||
* Metacharacters or escape sequences in the input sequence will be given no special meaning.
|
||||
*/
|
||||
LITERAL(Pattern.LITERAL),
|
||||
|
||||
/** Enables Unix lines mode. In this mode, only the `'\n'` is recognized as a line terminator. */
|
||||
UNIX_LINES(Pattern.UNIX_LINES),
|
||||
|
||||
/** Permits whitespace and comments in pattern. */
|
||||
COMMENTS(Pattern.COMMENTS),
|
||||
|
||||
/** Enables the mode, when the expression `.` matches any character, including a line terminator. */
|
||||
DOT_MATCHES_ALL(Pattern.DOTALL),
|
||||
|
||||
/** Enables equivalence by canonical decomposition. */
|
||||
CANON_EQ(Pattern.CANON_EQ)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents the results from a single capturing group within a [MatchResult] of [Regex].
|
||||
*
|
||||
* @param value The value of captured group.
|
||||
* @param range The range of indices in the input string where group was captured.
|
||||
*
|
||||
* The [range] property is available on JVM only.
|
||||
*/
|
||||
public actual data class MatchGroup(actual val value: String, val range: IntRange)
|
||||
|
||||
/**
|
||||
* Represents a compiled regular expression.
|
||||
* Provides functions to match strings in text with a pattern, replace the found occurrences and split text around matches.
|
||||
*/
|
||||
public actual class Regex internal constructor(internal val nativePattern: Pattern) {
|
||||
|
||||
internal enum class Mode {
|
||||
FIND, MATCH
|
||||
}
|
||||
|
||||
/** Creates a regular expression from the specified [pattern] string and the default options. */
|
||||
actual constructor(pattern: String): this(Pattern(pattern))
|
||||
|
||||
/** Creates a regular expression from the specified [pattern] string and the specified single [option]. */
|
||||
actual constructor(pattern: String, option: RegexOption): this(Pattern(pattern, ensureUnicodeCase(option.value)))
|
||||
|
||||
/** Creates a regular expression from the specified [pattern] string and the specified set of [options]. */
|
||||
actual constructor(pattern: String, options: Set<RegexOption>): this(Pattern(pattern, ensureUnicodeCase(options.toInt())))
|
||||
|
||||
|
||||
/** The pattern string of this regular expression. */
|
||||
actual val pattern: String
|
||||
get() = nativePattern.pattern
|
||||
|
||||
private val startNode = nativePattern.startNode
|
||||
|
||||
/** The set of options that were used to create this regular expression. */
|
||||
actual val options: Set<RegexOption> = fromInt(nativePattern.flags)
|
||||
|
||||
actual companion object {
|
||||
/**
|
||||
* Returns a regular expression that matches the specified [literal] string literally.
|
||||
* No characters of that string will have special meaning when searching for an occurrence of the regular expression.
|
||||
*/
|
||||
actual fun fromLiteral(literal: String): Regex = Regex(literal, RegexOption.LITERAL)
|
||||
|
||||
/**
|
||||
* Returns a regular expression pattern string that matches the specified [literal] string literally.
|
||||
* No characters of that string will have special meaning when searching for an occurrence of the regular expression.
|
||||
*/
|
||||
actual fun escape(literal: String): String = Pattern.quote(literal)
|
||||
|
||||
/**
|
||||
* Returns a literal replacement expression for the specified [literal] string.
|
||||
* No characters of that string will have special meaning when it is used as a replacement string in [Regex.replace] function.
|
||||
*/
|
||||
actual fun escapeReplacement(literal: String): String {
|
||||
if (!literal.contains('\\') && !literal.contains('$'))
|
||||
return literal
|
||||
|
||||
val result = StringBuilder(literal.length * 2)
|
||||
literal.forEach {
|
||||
if (it == '\\' || it == '$') {
|
||||
result.append('\\')
|
||||
}
|
||||
result.append(it)
|
||||
}
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
private fun ensureUnicodeCase(flags: Int) = flags
|
||||
}
|
||||
|
||||
private fun doMatch(input: CharSequence, mode: Mode): MatchResult? {
|
||||
// TODO: Harmony has a default constructor for MatchResult. Do we need it?
|
||||
// TODO: Reuse the matchResult.
|
||||
val matchResult = MatchResultImpl(input, this)
|
||||
matchResult.mode = mode
|
||||
val matches = startNode.matches(0, input, matchResult) >= 0
|
||||
if (!matches) {
|
||||
return null
|
||||
}
|
||||
matchResult.finalizeMatch()
|
||||
return matchResult
|
||||
}
|
||||
|
||||
/** Indicates whether the regular expression matches the entire [input]. */
|
||||
actual infix fun matches(input: CharSequence): Boolean = doMatch(input, Mode.MATCH) != null
|
||||
|
||||
/** Indicates whether the regular expression can find at least one match in the specified [input]. */
|
||||
actual fun containsMatchIn(input: CharSequence): Boolean = find(input) != null
|
||||
|
||||
@SinceKotlin("1.5")
|
||||
@ExperimentalStdlibApi
|
||||
public actual fun matchesAt(input: CharSequence, index: Int): Boolean =
|
||||
// TODO: expand and simplify
|
||||
matchAt(input, index) != null
|
||||
|
||||
/**
|
||||
* Returns the first match of a regular expression in the [input], beginning at the specified [startIndex].
|
||||
*
|
||||
* @param startIndex An index to start search with, by default 0. Must be not less than zero and not greater than `input.length()`
|
||||
* @return An instance of [MatchResult] if match was found or `null` otherwise.
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or greater than the length of the [input] char sequence.
|
||||
* @sample samples.text.Regexps.find
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
actual fun find(input: CharSequence, startIndex: Int = 0): MatchResult? {
|
||||
if (startIndex < 0 || startIndex > input.length) {
|
||||
throw IndexOutOfBoundsException("Start index is out of bounds: $startIndex, input length: ${input.length}")
|
||||
}
|
||||
val matchResult = MatchResultImpl(input, this)
|
||||
matchResult.mode = Mode.FIND
|
||||
matchResult.startIndex = startIndex
|
||||
val foundIndex = startNode.find(startIndex, input, matchResult)
|
||||
if (foundIndex >= 0) {
|
||||
matchResult.finalizeMatch()
|
||||
return matchResult
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sequence of all occurrences of a regular expression within the [input] string, beginning at the specified [startIndex].
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or greater than the length of the [input] char sequence.
|
||||
*
|
||||
* @sample samples.text.Regexps.findAll
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
actual fun findAll(input: CharSequence, startIndex: Int = 0): Sequence<MatchResult> {
|
||||
if (startIndex < 0 || startIndex > input.length) {
|
||||
throw IndexOutOfBoundsException("Start index is out of bounds: $startIndex, input length: ${input.length}")
|
||||
}
|
||||
return generateSequence({ find(input, startIndex) }, MatchResult::next)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to match the entire [input] CharSequence against the pattern.
|
||||
*
|
||||
* @return An instance of [MatchResult] if the entire input matches or `null` otherwise.
|
||||
*/
|
||||
actual fun matchEntire(input: CharSequence): MatchResult?= doMatch(input, Mode.MATCH)
|
||||
|
||||
@SinceKotlin("1.5")
|
||||
@ExperimentalStdlibApi
|
||||
public actual fun matchAt(input: CharSequence, index: Int): MatchResult? {
|
||||
if (index < 0 || index > input.length) {
|
||||
throw IndexOutOfBoundsException("index is out of bounds: $index, input length: ${input.length}")
|
||||
}
|
||||
val matchResult = MatchResultImpl(input, this)
|
||||
matchResult.mode = Mode.FIND
|
||||
matchResult.startIndex = index
|
||||
val matches = startNode.matches(index, input, matchResult) >= 0
|
||||
if (!matches) {
|
||||
return null
|
||||
}
|
||||
matchResult.finalizeMatch()
|
||||
return matchResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all occurrences of this regular expression in the specified [input] string with specified [replacement] expression.
|
||||
*
|
||||
* The replacement string may contain references to the captured groups during a match. Occurrences of `$index`
|
||||
* in the replacement string will be substituted with the subsequences corresponding to the captured groups with the specified index.
|
||||
* The first digit after '$' is always treated as part of group reference. Subsequent digits are incorporated
|
||||
* into `index` only if they would form a valid group reference. Only the digits '0'..'9' are considered as potential components
|
||||
* of the group reference. Note that indexes of captured groups start from 1, and the group with index 0 is the whole match.
|
||||
*
|
||||
* Backslash character '\' can be used to include the succeeding character as a literal in the replacement string, e.g, `\$` or `\\`.
|
||||
* [Regex.escapeReplacement] can be used if [replacement] have to be treated as a literal string.
|
||||
*
|
||||
* Note that named capturing groups are not supported in Kotlin/Native.
|
||||
*
|
||||
* @param input the char sequence to find matches of this regular expression in
|
||||
* @param replacement the expression to replace found matches with
|
||||
* @return the result of replacing each occurrence of this regular expression in [input] with the result of evaluating the [replacement] expression
|
||||
* @throws RuntimeException if [replacement] expression is malformed, or capturing group with specified `name` or `index` does not exist
|
||||
*/
|
||||
actual fun replace(input: CharSequence, replacement: String): String
|
||||
= replace(input) { match -> substituteGroupRefs(match, replacement) }
|
||||
|
||||
/**
|
||||
* Replaces all occurrences of this regular expression in the specified [input] string with the result of
|
||||
* the given function [transform] that takes [MatchResult] and returns a string to be used as a
|
||||
* replacement for that match.
|
||||
*/
|
||||
actual fun replace(input: CharSequence, transform: (MatchResult) -> CharSequence): String {
|
||||
var match: MatchResult? = find(input) ?: return input.toString()
|
||||
|
||||
var lastStart = 0
|
||||
val length = input.length
|
||||
val sb = StringBuilder(length)
|
||||
do {
|
||||
val foundMatch = match!!
|
||||
sb.append(input, lastStart, foundMatch.range.start)
|
||||
sb.append(transform(foundMatch))
|
||||
lastStart = foundMatch.range.endInclusive + 1
|
||||
match = foundMatch.next()
|
||||
} while (lastStart < length && match != null)
|
||||
|
||||
if (lastStart < length) {
|
||||
sb.append(input, lastStart, length)
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the first occurrence of this regular expression in the specified [input] string with specified [replacement] expression.
|
||||
*
|
||||
* The replacement string may contain references to the captured groups during a match. Occurrences of `$index`
|
||||
* in the replacement string will be substituted with the subsequences corresponding to the captured groups with the specified index.
|
||||
* The first digit after '$' is always treated as part of group reference. Subsequent digits are incorporated
|
||||
* into `index` only if they would form a valid group reference. Only the digits '0'..'9' are considered as potential components
|
||||
* of the group reference. Note that indexes of captured groups start from 1, and the group with index 0 is the whole match.
|
||||
*
|
||||
* Backslash character '\' can be used to include the succeeding character as a literal in the replacement string, e.g, `\$` or `\\`.
|
||||
* [Regex.escapeReplacement] can be used if [replacement] have to be treated as a literal string.
|
||||
*
|
||||
* Note that named capturing groups are not supported in Kotlin/Native.
|
||||
*
|
||||
* @param input the char sequence to find a match of this regular expression in
|
||||
* @param replacement the expression to replace the found match with
|
||||
* @return the result of replacing the first occurrence of this regular expression in [input] with the result of evaluating the [replacement] expression
|
||||
* @throws RuntimeException if [replacement] expression is malformed, or capturing group with specified `name` or `index` does not exist
|
||||
*/
|
||||
actual fun replaceFirst(input: CharSequence, replacement: String): String {
|
||||
val match = find(input) ?: return input.toString()
|
||||
val length = input.length
|
||||
val result = StringBuilder(length)
|
||||
result.append(input, 0, match.range.start)
|
||||
result.append(substituteGroupRefs(match, replacement))
|
||||
if (match.range.endInclusive + 1 < length) {
|
||||
result.append(input, match.range.endInclusive + 1, length)
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the [input] CharSequence to a list of strings around matches of this regular expression.
|
||||
*
|
||||
* @param limit Non-negative value specifying the maximum number of substrings the string can be split to.
|
||||
* Zero by default means no limit is set.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
actual fun split(input: CharSequence, limit: Int = 0): List<String> {
|
||||
requireNonNegativeLimit(limit)
|
||||
|
||||
var match: MatchResult? = find(input)
|
||||
|
||||
if (match == null || limit == 1) return listOf(input.toString())
|
||||
|
||||
val result = ArrayList<String>(if (limit > 0) limit.coerceAtMost(10) else 10)
|
||||
var lastStart = 0
|
||||
val lastSplit = limit - 1 // negative if there's no limit
|
||||
|
||||
do {
|
||||
result.add(input.substring(lastStart, match!!.range.start))
|
||||
lastStart = match.range.endInclusive + 1
|
||||
if (lastSplit >= 0 && result.size == lastSplit) break
|
||||
match = match.next()
|
||||
} while (match != null)
|
||||
|
||||
result.add(input.substring(lastStart, input.length))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the [input] CharSequence to a sequence of strings around matches of this regular expression.
|
||||
*
|
||||
* @param limit Non-negative value specifying the maximum number of substrings the string can be split to.
|
||||
* Zero by default means no limit is set.
|
||||
* @sample samples.text.Regexps.splitToSequence
|
||||
*/
|
||||
@SinceKotlin("1.6")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun splitToSequence(input: CharSequence, limit: Int = 0): Sequence<String> {
|
||||
requireNonNegativeLimit(limit)
|
||||
|
||||
return sequence {
|
||||
var match = find(input)
|
||||
if (match == null || limit == 1) {
|
||||
yield(input.toString())
|
||||
return@sequence
|
||||
}
|
||||
|
||||
var nextStart = 0
|
||||
var splitCount = 0
|
||||
|
||||
do {
|
||||
val foundMatch = match!!
|
||||
yield(input.substring(nextStart, foundMatch.range.first))
|
||||
nextStart = foundMatch.range.endInclusive + 1
|
||||
match = foundMatch.next()
|
||||
} while (++splitCount != limit - 1 && match != null)
|
||||
|
||||
yield(input.substring(nextStart, input.length))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string representation of this regular expression, namely the [pattern] of this regular expression.
|
||||
*
|
||||
* Note that another regular expression constructed from the same pattern string may have different [options]
|
||||
* and may match strings differently.
|
||||
*/
|
||||
override fun toString(): String = nativePattern.toString()
|
||||
}
|
||||
|
||||
// The same code from K/JS regex.kt
|
||||
private fun substituteGroupRefs(match: MatchResult, replacement: String): String {
|
||||
var index = 0
|
||||
val result = StringBuilder(replacement.length)
|
||||
|
||||
while (index < replacement.length) {
|
||||
val char = replacement[index++]
|
||||
if (char == '\\') {
|
||||
if (index == replacement.length)
|
||||
throw IllegalArgumentException("The Char to be escaped is missing")
|
||||
|
||||
result.append(replacement[index++])
|
||||
} else if (char == '$') {
|
||||
if (index == replacement.length)
|
||||
throw IllegalArgumentException("Capturing group index is missing")
|
||||
|
||||
if (replacement[index] == '{')
|
||||
throw IllegalArgumentException("Named capturing group reference currently is not supported")
|
||||
|
||||
if (replacement[index] !in '0'..'9')
|
||||
throw IllegalArgumentException("Invalid capturing group reference")
|
||||
|
||||
val endIndex = replacement.readGroupIndex(index, match.groupValues.size)
|
||||
val groupIndex = replacement.substring(index, endIndex).toInt()
|
||||
|
||||
if (groupIndex >= match.groupValues.size)
|
||||
throw IndexOutOfBoundsException("Group with index $groupIndex does not exist")
|
||||
|
||||
result.append(match.groupValues[groupIndex])
|
||||
index = endIndex
|
||||
} else {
|
||||
result.append(char)
|
||||
}
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
private fun String.readGroupIndex(startIndex: Int, groupCount: Int): Int {
|
||||
// at least one digit after '$' is always captured
|
||||
var index = startIndex + 1
|
||||
var groupIndex = this[startIndex] - '0'
|
||||
|
||||
// capture the largest valid group index
|
||||
while (index < length && this[index] in '0'..'9') {
|
||||
val newGroupIndex = (groupIndex * 10) + (this[index] - '0')
|
||||
if (newGroupIndex in 0 until groupCount) {
|
||||
groupIndex = newGroupIndex
|
||||
index++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION") // Char.toInt()
|
||||
package kotlin.text.regex
|
||||
|
||||
import kotlin.collections.associate
|
||||
import kotlin.native.concurrent.AtomicReference
|
||||
import kotlin.native.concurrent.freeze
|
||||
import kotlin.native.BitSet
|
||||
|
||||
/**
|
||||
* Unicode category (i.e. Ll, Lu).
|
||||
*/
|
||||
internal open class UnicodeCategory(protected val category: Int) : AbstractCharClass() {
|
||||
override fun contains(ch: Int): Boolean = alt xor (ch.toChar().category.value == category)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unicode category scope (i.e IsL, IsM, ...)
|
||||
*/
|
||||
internal class UnicodeCategoryScope(category: Int) : UnicodeCategory(category) {
|
||||
override fun contains(ch: Int): Boolean {
|
||||
return alt xor (((category shr ch.toChar().category.value) and 1) != 0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents character classes, i.e. sets of character either predefined or user defined.
|
||||
* Note: this class represent a token, not node, so being constructed by lexer.
|
||||
*/
|
||||
internal abstract class AbstractCharClass : SpecialToken() {
|
||||
/**
|
||||
* Show if the class has alternative meaning:
|
||||
* if the class contains character 'a' and alt == true then the class will contains all characters except 'a'.
|
||||
*/
|
||||
internal var alt: Boolean = false
|
||||
internal var altSurrogates: Boolean = false
|
||||
|
||||
internal val lowHighSurrogates = BitSet(SURROGATE_CARDINALITY) // Bit set for surrogates?
|
||||
|
||||
/*
|
||||
* Indicates if this class may contain supplementary Unicode codepoints.
|
||||
* If this flag is specified it doesn't mean that this class contains supplementary characters but may contain.
|
||||
*/
|
||||
var mayContainSupplCodepoints = false
|
||||
protected set
|
||||
|
||||
/** Returns true if this char class contains character specified. */
|
||||
abstract operator fun contains(ch: Int): Boolean
|
||||
open fun contains(ch: Char): Boolean = contains(ch.toInt())
|
||||
|
||||
/**
|
||||
* Returns BitSet representing this character class or `null`
|
||||
* if this character class does not have character representation;
|
||||
*/
|
||||
open internal val bits: BitSet?
|
||||
get() = null
|
||||
|
||||
fun hasLowHighSurrogates(): Boolean {
|
||||
return if (altSurrogates)
|
||||
lowHighSurrogates.nextClearBit(0) != -1
|
||||
else
|
||||
lowHighSurrogates.nextSetBit(0) != -1
|
||||
}
|
||||
|
||||
override val type: Type = Type.CHARCLASS
|
||||
|
||||
open val instance: AbstractCharClass
|
||||
get() = this
|
||||
|
||||
|
||||
private val surrogates_ = AtomicReference<AbstractCharClass?>(null)
|
||||
fun classWithSurrogates(): AbstractCharClass {
|
||||
surrogates_.value?.let {
|
||||
return it
|
||||
}
|
||||
val surrogates = lowHighSurrogates
|
||||
val result = object : AbstractCharClass() {
|
||||
override fun contains(ch: Int): Boolean {
|
||||
val index = ch - Char.MIN_SURROGATE.toInt()
|
||||
|
||||
return if (index >= 0 && index < AbstractCharClass.SURROGATE_CARDINALITY) {
|
||||
this.altSurrogates xor surrogates[index]
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
result.setNegative(this.altSurrogates)
|
||||
surrogates_.compareAndSet(null, result.freeze())
|
||||
return surrogates_.value!!
|
||||
}
|
||||
|
||||
|
||||
// We cannot cache this class as we've done with surrogates above because
|
||||
// here is a circular reference between it and AbstractCharClass.
|
||||
fun classWithoutSurrogates(): AbstractCharClass {
|
||||
val result = object : AbstractCharClass() {
|
||||
override fun contains(ch: Int): Boolean {
|
||||
val index = ch - Char.MIN_SURROGATE.toInt()
|
||||
|
||||
val containslHS = if (index >= 0 && index < AbstractCharClass.SURROGATE_CARDINALITY)
|
||||
this.altSurrogates xor this@AbstractCharClass.lowHighSurrogates.get(index)
|
||||
else
|
||||
false
|
||||
|
||||
return this@AbstractCharClass.contains(ch) && !containslHS
|
||||
}
|
||||
}
|
||||
result.setNegative(isNegative())
|
||||
result.mayContainSupplCodepoints = mayContainSupplCodepoints
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets this CharClass to negative form, i.e. if they will add some characters and after that set this
|
||||
* class to negative it will accept all the characters except previously set ones.
|
||||
*
|
||||
* Although this method will not alternate all the already set characters,
|
||||
* just overall meaning of the class.
|
||||
*/
|
||||
fun setNegative(value: Boolean): AbstractCharClass {
|
||||
if (alt xor value) {
|
||||
alt = !alt
|
||||
altSurrogates = !altSurrogates
|
||||
}
|
||||
if (!mayContainSupplCodepoints) {
|
||||
mayContainSupplCodepoints = true
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun isNegative(): Boolean {
|
||||
return alt
|
||||
}
|
||||
|
||||
internal abstract class CachedCharClass {
|
||||
lateinit private var posValue: AbstractCharClass
|
||||
|
||||
lateinit private var negValue: AbstractCharClass
|
||||
|
||||
// Somewhat ugly init sequence, as computeValue() may depend on fields, initialized in subclass ctor.
|
||||
protected fun initValues() {
|
||||
posValue = computeValue()
|
||||
negValue = computeValue().setNegative(true)
|
||||
}
|
||||
|
||||
fun getValue(negative: Boolean): AbstractCharClass = if (!negative) posValue else negValue
|
||||
protected abstract fun computeValue(): AbstractCharClass
|
||||
}
|
||||
|
||||
internal class CachedDigit : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add('0', '9')
|
||||
}
|
||||
|
||||
internal class CachedNonDigit : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass =
|
||||
CharClass().add('0', '9').setNegative(true).apply { mayContainSupplCodepoints = true }
|
||||
}
|
||||
|
||||
internal class CachedSpace : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
/* 9-13 - \t\n\x0B\f\r; 32 - ' ' */
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add(9, 13).add(32)
|
||||
}
|
||||
|
||||
internal class CachedNonSpace : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass =
|
||||
CachedSpace().getValue(negative = true).apply { mayContainSupplCodepoints = true }
|
||||
}
|
||||
|
||||
internal class CachedWord : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add('a', 'z').add('A', 'Z').add('0', '9').add('_')
|
||||
}
|
||||
|
||||
internal class CachedNonWord : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass =
|
||||
CachedWord().getValue(negative = true).apply { mayContainSupplCodepoints = true }
|
||||
}
|
||||
|
||||
internal class CachedLower : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add('a', 'z')
|
||||
}
|
||||
|
||||
internal class CachedUpper : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add('A', 'Z')
|
||||
}
|
||||
|
||||
internal class CachedASCII : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add(0x00, 0x7F)
|
||||
}
|
||||
|
||||
internal class CachedAlpha : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add('a', 'z').add('A', 'Z')
|
||||
}
|
||||
|
||||
internal class CachedAlnum : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass =
|
||||
(CachedAlpha().getValue(negative = false) as CharClass).add('0', '9')
|
||||
}
|
||||
|
||||
internal class CachedPunct : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
/* Punctuation !"#$%&'()*+,-./:;<=>?@ [\]^_` {|}~ */
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add(0x21, 0x40).add(0x5B, 0x60).add(0x7B, 0x7E)
|
||||
}
|
||||
|
||||
internal class CachedGraph : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
/* plus punctuation */
|
||||
override fun computeValue(): AbstractCharClass =
|
||||
(CachedAlnum().getValue(negative = false) as CharClass)
|
||||
.add(0x21, 0x40)
|
||||
.add(0x5B, 0x60)
|
||||
.add(0x7B, 0x7E)
|
||||
}
|
||||
|
||||
internal class CachedPrint : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass =
|
||||
(CachedGraph().getValue(negative = true) as CharClass).add(0x20)
|
||||
}
|
||||
|
||||
internal class CachedBlank : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add(' ').add('\t')
|
||||
}
|
||||
|
||||
internal class CachedCntrl : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add(0x00, 0x1F).add(0x7F)
|
||||
}
|
||||
|
||||
internal class CachedXDigit : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass = CharClass().add('0', '9').add('a', 'f').add('A', 'F')
|
||||
}
|
||||
|
||||
internal class CachedRange(var start: Int, var end: Int) : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass =
|
||||
object: AbstractCharClass() {
|
||||
override fun contains(ch: Int): Boolean = alt xor (ch in start..end)
|
||||
}.apply {
|
||||
if (end >= Char.MIN_SUPPLEMENTARY_CODE_POINT) {
|
||||
mayContainSupplCodepoints = true
|
||||
}
|
||||
val minSurrogate = Char.MIN_SURROGATE.toInt()
|
||||
val maxSurrogate = Char.MAX_SURROGATE.toInt()
|
||||
// There is an intersection with surrogate characters.
|
||||
if (end >= minSurrogate && start <= maxSurrogate && start <= end) {
|
||||
val surrogatesStart = maxOf(start, minSurrogate) - minSurrogate
|
||||
val surrogatesEnd = minOf(end, maxSurrogate) - minSurrogate
|
||||
lowHighSurrogates.set(surrogatesStart..surrogatesEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class CachedSpecialsBlock : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
public override fun computeValue(): AbstractCharClass = CharClass().add(0xFEFF, 0xFEFF).add(0xFFF0, 0xFFFD)
|
||||
}
|
||||
|
||||
internal class CachedCategoryScope(
|
||||
val category: Int,
|
||||
val mayContainSupplCodepoints: Boolean,
|
||||
val containsAllSurrogates: Boolean = false) : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass {
|
||||
val result = UnicodeCategoryScope(category)
|
||||
if (containsAllSurrogates) {
|
||||
result.lowHighSurrogates.set(0, SURROGATE_CARDINALITY)
|
||||
}
|
||||
|
||||
result.mayContainSupplCodepoints = mayContainSupplCodepoints
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
internal class CachedCategory(
|
||||
val category: Int,
|
||||
val mayContainSupplCodepoints: Boolean,
|
||||
val containsAllSurrogates: Boolean = false) : CachedCharClass() {
|
||||
init {
|
||||
initValues()
|
||||
}
|
||||
override fun computeValue(): AbstractCharClass {
|
||||
val result = UnicodeCategory(category)
|
||||
if (containsAllSurrogates) {
|
||||
result.lowHighSurrogates.set(0, SURROGATE_CARDINALITY)
|
||||
}
|
||||
result.mayContainSupplCodepoints = mayContainSupplCodepoints
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
//Char.MAX_SURROGATE - Char.MIN_SURROGATE + 1
|
||||
const val SURROGATE_CARDINALITY = 2048
|
||||
|
||||
/**
|
||||
* Character classes.
|
||||
* See http://www.unicode.org/reports/tr18/, http://www.unicode.org/Public/4.1.0/ucd/Blocks.txt
|
||||
*/
|
||||
enum class CharClasses(val regexName : String, val factory: () -> CachedCharClass) {
|
||||
LOWER("Lower", ::CachedLower),
|
||||
UPPER("Upper", ::CachedUpper),
|
||||
ASCII("ASCII", ::CachedASCII),
|
||||
ALPHA("Alpha", ::CachedAlpha),
|
||||
DIGIT("Digit", ::CachedDigit),
|
||||
ALNUM("Alnum", :: CachedAlnum),
|
||||
PUNCT("Punct", ::CachedPunct),
|
||||
GRAPH("Graph", ::CachedGraph),
|
||||
PRINT("Print", ::CachedPrint),
|
||||
BLANK("Blank", ::CachedBlank),
|
||||
CNTRL("Cntrl", ::CachedCntrl),
|
||||
XDIGIT("XDigit", ::CachedXDigit),
|
||||
SPACE("Space", ::CachedSpace),
|
||||
WORD("w", ::CachedWord),
|
||||
NON_WORD("W", ::CachedNonWord),
|
||||
SPACE_SHORT("s", ::CachedSpace),
|
||||
NON_SPACE("S", ::CachedNonSpace),
|
||||
DIGIT_SHORT("d", ::CachedDigit),
|
||||
NON_DIGIT("D", ::CachedNonDigit),
|
||||
BASIC_LATIN("BasicLatin", { CachedRange(0x0000, 0x007F) }),
|
||||
LATIN1_SUPPLEMENT("Latin-1Supplement", { CachedRange(0x0080, 0x00FF) }),
|
||||
LATIN_EXTENDED_A("LatinExtended-A", { CachedRange(0x0100, 0x017F) }),
|
||||
LATIN_EXTENDED_B("LatinExtended-B", { CachedRange(0x0180, 0x024F) }),
|
||||
IPA_EXTENSIONS("IPAExtensions", { CachedRange(0x0250, 0x02AF) }),
|
||||
SPACING_MODIFIER_LETTERS("SpacingModifierLetters", { CachedRange(0x02B0, 0x02FF) }),
|
||||
COMBINING_DIACRITICAL_MARKS("CombiningDiacriticalMarks", { CachedRange(0x0300, 0x036F) }),
|
||||
GREEK("Greek", { CachedRange(0x0370, 0x03FF) }),
|
||||
CYRILLIC("Cyrillic", { CachedRange(0x0400, 0x04FF) }),
|
||||
CYRILLIC_SUPPLEMENT("CyrillicSupplement", { CachedRange(0x0500, 0x052F) }),
|
||||
ARMENIAN("Armenian", { CachedRange(0x0530, 0x058F) }),
|
||||
HEBREW("Hebrew", { CachedRange(0x0590, 0x05FF) }),
|
||||
ARABIC("Arabic", { CachedRange(0x0600, 0x06FF) }),
|
||||
SYRIAC("Syriac", { CachedRange(0x0700, 0x074F) }),
|
||||
ARABICSUPPLEMENT("ArabicSupplement", { CachedRange(0x0750, 0x077F) }),
|
||||
THAANA("Thaana", { CachedRange(0x0780, 0x07BF) }),
|
||||
DEVANAGARI("Devanagari", { CachedRange(0x0900, 0x097F) }),
|
||||
BENGALI("Bengali", { CachedRange(0x0980, 0x09FF) }),
|
||||
GURMUKHI("Gurmukhi", { CachedRange(0x0A00, 0x0A7F) }),
|
||||
GUJARATI("Gujarati", { CachedRange(0x0A80, 0x0AFF) }),
|
||||
ORIYA("Oriya", { CachedRange(0x0B00, 0x0B7F) }),
|
||||
TAMIL("Tamil", { CachedRange(0x0B80, 0x0BFF) }),
|
||||
TELUGU("Telugu", { CachedRange(0x0C00, 0x0C7F) }),
|
||||
KANNADA("Kannada", { CachedRange(0x0C80, 0x0CFF) }),
|
||||
MALAYALAM("Malayalam", { CachedRange(0x0D00, 0x0D7F) }),
|
||||
SINHALA("Sinhala", { CachedRange(0x0D80, 0x0DFF) }),
|
||||
THAI("Thai", { CachedRange(0x0E00, 0x0E7F) }),
|
||||
LAO("Lao", { CachedRange(0x0E80, 0x0EFF) }),
|
||||
TIBETAN("Tibetan", { CachedRange(0x0F00, 0x0FFF) }),
|
||||
MYANMAR("Myanmar", { CachedRange(0x1000, 0x109F) }),
|
||||
GEORGIAN("Georgian", { CachedRange(0x10A0, 0x10FF) }),
|
||||
HANGULJAMO("HangulJamo", { CachedRange(0x1100, 0x11FF) }),
|
||||
ETHIOPIC("Ethiopic", { CachedRange(0x1200, 0x137F) }),
|
||||
ETHIOPICSUPPLEMENT("EthiopicSupplement", { CachedRange(0x1380, 0x139F) }),
|
||||
CHEROKEE("Cherokee", { CachedRange(0x13A0, 0x13FF) }),
|
||||
UNIFIEDCANADIANABORIGINALSYLLABICS("UnifiedCanadianAboriginalSyllabics", { CachedRange(0x1400, 0x167F) }),
|
||||
OGHAM("Ogham", { CachedRange(0x1680, 0x169F) }),
|
||||
RUNIC("Runic", { CachedRange(0x16A0, 0x16FF) }),
|
||||
TAGALOG("Tagalog", { CachedRange(0x1700, 0x171F) }),
|
||||
HANUNOO("Hanunoo", { CachedRange(0x1720, 0x173F) }),
|
||||
BUHID("Buhid", { CachedRange(0x1740, 0x175F) }),
|
||||
TAGBANWA("Tagbanwa", { CachedRange(0x1760, 0x177F) }),
|
||||
KHMER("Khmer", { CachedRange(0x1780, 0x17FF) }),
|
||||
MONGOLIAN("Mongolian", { CachedRange(0x1800, 0x18AF) }),
|
||||
LIMBU("Limbu", { CachedRange(0x1900, 0x194F) }),
|
||||
TAILE("TaiLe", { CachedRange(0x1950, 0x197F) }),
|
||||
NEWTAILUE("NewTaiLue", { CachedRange(0x1980, 0x19DF) }),
|
||||
KHMERSYMBOLS("KhmerSymbols", { CachedRange(0x19E0, 0x19FF) }),
|
||||
BUGINESE("Buginese", { CachedRange(0x1A00, 0x1A1F) }),
|
||||
PHONETICEXTENSIONS("PhoneticExtensions", { CachedRange(0x1D00, 0x1D7F) }),
|
||||
PHONETICEXTENSIONSSUPPLEMENT("PhoneticExtensionsSupplement", { CachedRange(0x1D80, 0x1DBF) }),
|
||||
COMBININGDIACRITICALMARKSSUPPLEMENT("CombiningDiacriticalMarksSupplement", { CachedRange(0x1DC0, 0x1DFF) }),
|
||||
LATINEXTENDEDADDITIONAL("LatinExtendedAdditional", { CachedRange(0x1E00, 0x1EFF) }),
|
||||
GREEKEXTENDED("GreekExtended", { CachedRange(0x1F00, 0x1FFF) }),
|
||||
GENERALPUNCTUATION("GeneralPunctuation", { CachedRange(0x2000, 0x206F) }),
|
||||
SUPERSCRIPTSANDSUBSCRIPTS("SuperscriptsandSubscripts", { CachedRange(0x2070, 0x209F) }),
|
||||
CURRENCYSYMBOLS("CurrencySymbols", { CachedRange(0x20A0, 0x20CF) }),
|
||||
COMBININGMARKSFORSYMBOLS("CombiningMarksforSymbols", { CachedRange(0x20D0, 0x20FF) }),
|
||||
LETTERLIKESYMBOLS("LetterlikeSymbols", { CachedRange(0x2100, 0x214F) }),
|
||||
NUMBERFORMS("NumberForms", { CachedRange(0x2150, 0x218F) }),
|
||||
ARROWS("Arrows", { CachedRange(0x2190, 0x21FF) }),
|
||||
MATHEMATICALOPERATORS("MathematicalOperators", { CachedRange(0x2200, 0x22FF) }),
|
||||
MISCELLANEOUSTECHNICAL("MiscellaneousTechnical", { CachedRange(0x2300, 0x23FF) }),
|
||||
CONTROLPICTURES("ControlPictures", { CachedRange(0x2400, 0x243F) }),
|
||||
OPTICALCHARACTERRECOGNITION("OpticalCharacterRecognition", { CachedRange(0x2440, 0x245F) }),
|
||||
ENCLOSEDALPHANUMERICS("EnclosedAlphanumerics", { CachedRange(0x2460, 0x24FF) }),
|
||||
BOXDRAWING("BoxDrawing", { CachedRange(0x2500, 0x257F) }),
|
||||
BLOCKELEMENTS("BlockElements", { CachedRange(0x2580, 0x259F) }),
|
||||
GEOMETRICSHAPES("GeometricShapes", { CachedRange(0x25A0, 0x25FF) }),
|
||||
MISCELLANEOUSSYMBOLS("MiscellaneousSymbols", { CachedRange(0x2600, 0x26FF) }),
|
||||
DINGBATS("Dingbats", { CachedRange(0x2700, 0x27BF) }),
|
||||
MISCELLANEOUSMATHEMATICALSYMBOLS_A("MiscellaneousMathematicalSymbols-A", { CachedRange(0x27C0, 0x27EF) }),
|
||||
SUPPLEMENTALARROWS_A("SupplementalArrows-A", { CachedRange(0x27F0, 0x27FF) }),
|
||||
BRAILLEPATTERNS("BraillePatterns", { CachedRange(0x2800, 0x28FF) }),
|
||||
SUPPLEMENTALARROWS_B("SupplementalArrows-B", { CachedRange(0x2900, 0x297F) }),
|
||||
MISCELLANEOUSMATHEMATICALSYMBOLS_B("MiscellaneousMathematicalSymbols-B", { CachedRange(0x2980, 0x29FF) }),
|
||||
SUPPLEMENTALMATHEMATICALOPERATORS("SupplementalMathematicalOperators", { CachedRange(0x2A00, 0x2AFF) }),
|
||||
MISCELLANEOUSSYMBOLSANDARROWS("MiscellaneousSymbolsandArrows", { CachedRange(0x2B00, 0x2BFF) }),
|
||||
GLAGOLITIC("Glagolitic", { CachedRange(0x2C00, 0x2C5F) }),
|
||||
COPTIC("Coptic", { CachedRange(0x2C80, 0x2CFF) }),
|
||||
GEORGIANSUPPLEMENT("GeorgianSupplement", { CachedRange(0x2D00, 0x2D2F) }),
|
||||
TIFINAGH("Tifinagh", { CachedRange(0x2D30, 0x2D7F) }),
|
||||
ETHIOPICEXTENDED("EthiopicExtended", { CachedRange(0x2D80, 0x2DDF) }),
|
||||
SUPPLEMENTALPUNCTUATION("SupplementalPunctuation", { CachedRange(0x2E00, 0x2E7F) }),
|
||||
CJKRADICALSSUPPLEMENT("CJKRadicalsSupplement", { CachedRange(0x2E80, 0x2EFF) }),
|
||||
KANGXIRADICALS("KangxiRadicals", { CachedRange(0x2F00, 0x2FDF) }),
|
||||
IDEOGRAPHICDESCRIPTIONCHARACTERS("IdeographicDescriptionCharacters", { CachedRange(0x2FF0, 0x2FFF) }),
|
||||
CJKSYMBOLSANDPUNCTUATION("CJKSymbolsandPunctuation", { CachedRange(0x3000, 0x303F) }),
|
||||
HIRAGANA("Hiragana", { CachedRange(0x3040, 0x309F) }),
|
||||
KATAKANA("Katakana", { CachedRange(0x30A0, 0x30FF) }),
|
||||
BOPOMOFO("Bopomofo", { CachedRange(0x3100, 0x312F) }),
|
||||
HANGULCOMPATIBILITYJAMO("HangulCompatibilityJamo", { CachedRange(0x3130, 0x318F) }),
|
||||
KANBUN("Kanbun", { CachedRange(0x3190, 0x319F) }),
|
||||
BOPOMOFOEXTENDED("BopomofoExtended", { CachedRange(0x31A0, 0x31BF) }),
|
||||
CJKSTROKES("CJKStrokes", { CachedRange(0x31C0, 0x31EF) }),
|
||||
KATAKANAPHONETICEXTENSIONS("KatakanaPhoneticExtensions", { CachedRange(0x31F0, 0x31FF) }),
|
||||
ENCLOSEDCJKLETTERSANDMONTHS("EnclosedCJKLettersandMonths", { CachedRange(0x3200, 0x32FF) }),
|
||||
CJKCOMPATIBILITY("CJKCompatibility", { CachedRange(0x3300, 0x33FF) }),
|
||||
CJKUNIFIEDIDEOGRAPHSEXTENSIONA("CJKUnifiedIdeographsExtensionA", { CachedRange(0x3400, 0x4DB5) }),
|
||||
YIJINGHEXAGRAMSYMBOLS("YijingHexagramSymbols", { CachedRange(0x4DC0, 0x4DFF) }),
|
||||
CJKUNIFIEDIDEOGRAPHS("CJKUnifiedIdeographs", { CachedRange(0x4E00, 0x9FFF) }),
|
||||
YISYLLABLES("YiSyllables", { CachedRange(0xA000, 0xA48F) }),
|
||||
YIRADICALS("YiRadicals", { CachedRange(0xA490, 0xA4CF) }),
|
||||
MODIFIERTONELETTERS("ModifierToneLetters", { CachedRange(0xA700, 0xA71F) }),
|
||||
SYLOTINAGRI("SylotiNagri", { CachedRange(0xA800, 0xA82F) }),
|
||||
HANGULSYLLABLES("HangulSyllables", { CachedRange(0xAC00, 0xD7A3) }),
|
||||
HIGHSURROGATES("HighSurrogates", { CachedRange(0xD800, 0xDB7F) }),
|
||||
HIGHPRIVATEUSESURROGATES("HighPrivateUseSurrogates", { CachedRange(0xDB80, 0xDBFF) }),
|
||||
LOWSURROGATES("LowSurrogates", { CachedRange(0xDC00, 0xDFFF) }),
|
||||
PRIVATEUSEAREA("PrivateUseArea", { CachedRange(0xE000, 0xF8FF) }),
|
||||
CJKCOMPATIBILITYIDEOGRAPHS("CJKCompatibilityIdeographs", { CachedRange(0xF900, 0xFAFF) }),
|
||||
ALPHABETICPRESENTATIONFORMS("AlphabeticPresentationForms", { CachedRange(0xFB00, 0xFB4F) }),
|
||||
ARABICPRESENTATIONFORMS_A("ArabicPresentationForms-A", { CachedRange(0xFB50, 0xFDFF) }),
|
||||
VARIATIONSELECTORS("VariationSelectors", { CachedRange(0xFE00, 0xFE0F) }),
|
||||
VERTICALFORMS("VerticalForms", { CachedRange(0xFE10, 0xFE1F) }),
|
||||
COMBININGHALFMARKS("CombiningHalfMarks", { CachedRange(0xFE20, 0xFE2F) }),
|
||||
CJKCOMPATIBILITYFORMS("CJKCompatibilityForms", { CachedRange(0xFE30, 0xFE4F) }),
|
||||
SMALLFORMVARIANTS("SmallFormVariants", { CachedRange(0xFE50, 0xFE6F) }),
|
||||
ARABICPRESENTATIONFORMS_B("ArabicPresentationForms-B", { CachedRange(0xFE70, 0xFEFF) }),
|
||||
HALFWIDTHANDFULLWIDTHFORMS("HalfwidthandFullwidthForms", { CachedRange(0xFF00, 0xFFEF) }),
|
||||
ALL("all", { CachedRange(0x00, 0x10FFFF) }),
|
||||
SPECIALS("Specials", ::CachedSpecialsBlock),
|
||||
CN("Cn", { CachedCategory(CharCategory.UNASSIGNED.value, true) }),
|
||||
ISL("IsL", { CachedCategoryScope(0x3E, true) }),
|
||||
LU("Lu", { CachedCategory(CharCategory.UPPERCASE_LETTER.value, true) }),
|
||||
LL("Ll", { CachedCategory(CharCategory.LOWERCASE_LETTER.value, true) }),
|
||||
LT("Lt", { CachedCategory(CharCategory.TITLECASE_LETTER.value, false) }),
|
||||
LM("Lm", { CachedCategory(CharCategory.MODIFIER_LETTER.value, false) }),
|
||||
LO("Lo", { CachedCategory(CharCategory.OTHER_LETTER.value, true) }),
|
||||
ISM("IsM", { CachedCategoryScope(0x1C0, true) }),
|
||||
MN("Mn", { CachedCategory(CharCategory.NON_SPACING_MARK.value, true) }),
|
||||
ME("Me", { CachedCategory(CharCategory.ENCLOSING_MARK.value, false) }),
|
||||
MC("Mc", { CachedCategory(CharCategory.COMBINING_SPACING_MARK.value, true) }),
|
||||
N("N", { CachedCategoryScope(0xE00, true) }),
|
||||
ND("Nd", { CachedCategory(CharCategory.DECIMAL_DIGIT_NUMBER.value, true) }),
|
||||
NL("Nl", { CachedCategory(CharCategory.LETTER_NUMBER.value, true) }),
|
||||
NO("No", { CachedCategory(CharCategory.OTHER_NUMBER.value, true) }),
|
||||
ISZ("IsZ", { CachedCategoryScope(0x7000, false) }),
|
||||
ZS("Zs", { CachedCategory(CharCategory.SPACE_SEPARATOR.value, false) }),
|
||||
ZL("Zl", { CachedCategory(CharCategory.LINE_SEPARATOR.value, false) }),
|
||||
ZP("Zp", { CachedCategory(CharCategory.PARAGRAPH_SEPARATOR.value, false) }),
|
||||
ISC("IsC", { CachedCategoryScope(0xF0000, true, true) }),
|
||||
CC("Cc", { CachedCategory(CharCategory.CONTROL.value, false) }),
|
||||
CF("Cf", { CachedCategory(CharCategory.FORMAT.value, true) }),
|
||||
CO("Co", { CachedCategory(CharCategory.PRIVATE_USE.value, true) }),
|
||||
CS("Cs", { CachedCategory(CharCategory.SURROGATE.value, false, true) }),
|
||||
ISP("IsP", { CachedCategoryScope((1 shl CharCategory.DASH_PUNCTUATION.value)
|
||||
or (1 shl CharCategory.START_PUNCTUATION.value)
|
||||
or (1 shl CharCategory.END_PUNCTUATION.value)
|
||||
or (1 shl CharCategory.CONNECTOR_PUNCTUATION.value)
|
||||
or (1 shl CharCategory.OTHER_PUNCTUATION.value)
|
||||
or (1 shl CharCategory.INITIAL_QUOTE_PUNCTUATION.value)
|
||||
or (1 shl CharCategory.FINAL_QUOTE_PUNCTUATION.value), true) }),
|
||||
PD("Pd", { CachedCategory(CharCategory.DASH_PUNCTUATION.value, false) }),
|
||||
PS("Ps", { CachedCategory(CharCategory.START_PUNCTUATION.value, false) }),
|
||||
PE("Pe", { CachedCategory(CharCategory.END_PUNCTUATION.value, false) }),
|
||||
PC("Pc", { CachedCategory(CharCategory.CONNECTOR_PUNCTUATION.value, false) }),
|
||||
PO("Po", { CachedCategory(CharCategory.OTHER_PUNCTUATION.value, true) }),
|
||||
ISS("IsS", { CachedCategoryScope(0x7E000000, true) }),
|
||||
SM("Sm", { CachedCategory(CharCategory.MATH_SYMBOL.value, true) }),
|
||||
SC("Sc", { CachedCategory(CharCategory.CURRENCY_SYMBOL.value, false) }),
|
||||
SK("Sk", { CachedCategory(CharCategory.MODIFIER_SYMBOL.value, false) }),
|
||||
SO("So", { CachedCategory(CharCategory.OTHER_SYMBOL.value, true) }),
|
||||
PI("Pi", { CachedCategory(CharCategory.INITIAL_QUOTE_PUNCTUATION.value, false) }),
|
||||
PF("Pf", { CachedCategory(CharCategory.FINAL_QUOTE_PUNCTUATION.value, false) })
|
||||
}
|
||||
|
||||
private val classCache = Array<AtomicReference<CachedCharClass?>>(CharClasses.values().size, {
|
||||
AtomicReference<CachedCharClass?>(null)
|
||||
})
|
||||
private val classCacheMap = CharClasses.values().associate { it -> it.regexName to it }
|
||||
|
||||
fun intersects(ch1: Int, ch2: Int): Boolean = ch1 == ch2
|
||||
fun intersects(cc: AbstractCharClass, ch: Int): Boolean = cc.contains(ch)
|
||||
|
||||
fun intersects(cc1: AbstractCharClass, cc2: AbstractCharClass): Boolean {
|
||||
if (cc1.bits == null || cc2.bits == null) {
|
||||
return true
|
||||
}
|
||||
return cc1.bits!!.intersects(cc2.bits!!)
|
||||
}
|
||||
|
||||
fun getPredefinedClass(name: String, negative: Boolean): AbstractCharClass {
|
||||
val charClass = classCacheMap[name] ?: throw PatternSyntaxException("No such character class")
|
||||
val cachedClass = classCache[charClass.ordinal].value ?: run {
|
||||
classCache[charClass.ordinal].compareAndSwap(null, charClass.factory().freeze())
|
||||
classCache[charClass.ordinal].value!!
|
||||
}
|
||||
return cachedClass.getValue(negative)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION") // Char.toInt()
|
||||
package kotlin.text.regex
|
||||
|
||||
private object unixLT : AbstractLineTerminator() {
|
||||
override fun isLineTerminator(codepoint: Int): Boolean = (codepoint == '\n'.toInt())
|
||||
override fun isLineTerminatorPair(char1: Char, char2: Char): Boolean = false
|
||||
override fun isAfterLineTerminator(previous: Char, checked: Char): Boolean = (previous == '\n')
|
||||
}
|
||||
|
||||
private object unicodeLT : AbstractLineTerminator() {
|
||||
override fun isLineTerminatorPair(char1: Char, char2: Char): Boolean {
|
||||
return char1 == '\r' && char2 == '\n'
|
||||
}
|
||||
|
||||
override fun isLineTerminator(codepoint: Int): Boolean {
|
||||
return codepoint == '\n'.toInt()
|
||||
|| codepoint == '\r'.toInt()
|
||||
|| codepoint == '\u0085'.toInt()
|
||||
|| codepoint or 1 == '\u2029'.toInt()
|
||||
}
|
||||
|
||||
override fun isAfterLineTerminator(previous: Char, checked: Char): Boolean {
|
||||
return previous == '\n' || previous == '\u0085' || previous.toInt() or 1 == '\u2029'.toInt()
|
||||
|| previous == '\r' && checked != '\n'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Line terminator factory
|
||||
*/
|
||||
internal abstract class AbstractLineTerminator {
|
||||
|
||||
/** Checks if the single character is a line terminator or not. */
|
||||
open fun isLineTerminator(char: Char): Boolean = isLineTerminator(char.toInt())
|
||||
|
||||
/** Checks if the codepoint is a line terminator or not */
|
||||
abstract fun isLineTerminator(codepoint: Int): Boolean
|
||||
|
||||
/** Checks if the pair of symbols is a line terminator (e.g. for \r\n case) */
|
||||
abstract fun isLineTerminatorPair(char1: Char, char2: Char): Boolean
|
||||
|
||||
/** Checks if a [checked] character is after a line terminator using the [previous] character.*/
|
||||
abstract fun isAfterLineTerminator(previous: Char, checked: Char): Boolean
|
||||
|
||||
companion object {
|
||||
fun getInstance(flag: Int): AbstractLineTerminator {
|
||||
if (flag and Pattern.UNIX_LINES != 0) {
|
||||
return unixLT
|
||||
} else {
|
||||
return unicodeLT
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
@file:Suppress("DEPRECATION") // Char.toInt()
|
||||
package kotlin.text.regex
|
||||
|
||||
import kotlin.native.BitSet
|
||||
|
||||
/**
|
||||
* User defined character classes (e.g. [abef]).
|
||||
*/
|
||||
// TODO: replace the implementation with one using BitSet for first 256 symbols and a hash table / tree for the rest of UTF.
|
||||
internal class CharClass(val ignoreCase: Boolean = false, negative: Boolean = false) : AbstractCharClass() {
|
||||
|
||||
var invertedSurrogates = false
|
||||
|
||||
/**
|
||||
* Shows if the alt flags was inverted during the range construction process.
|
||||
* E.g. consider the following range: [\D3]. Here we firstly add the \D char class (which has the alt flag set) into a
|
||||
* resulting char set. After that the resulting char also has the alt flag set. But then we need to add the '3' character
|
||||
* into this class with positive sense. So we set the inverted flag to show that the range is not negative ([^..])
|
||||
* by itself but was inverted during some transformations.
|
||||
*/
|
||||
var inverted = false
|
||||
|
||||
var hideBits = false
|
||||
|
||||
internal var bits_ = BitSet()
|
||||
override val bits: BitSet?
|
||||
get() {
|
||||
if (hideBits)
|
||||
return null
|
||||
return bits_
|
||||
}
|
||||
|
||||
var nonBitSet: AbstractCharClass? = null
|
||||
|
||||
private val Int.asciiSupplement: Int
|
||||
get() = when {
|
||||
this in 'a'.toInt()..'z'.toInt() -> this - 32
|
||||
this in 'A'.toInt()..'Z'.toInt() -> this + 32
|
||||
else -> this
|
||||
}
|
||||
private val Int.isSurrogate: Boolean
|
||||
get() = this in Char.MIN_SURROGATE.toInt()..Char.MAX_SURROGATE.toInt()
|
||||
|
||||
init {
|
||||
setNegative(negative)
|
||||
}
|
||||
|
||||
/*
|
||||
* We can use this method safely even if nonBitSet != null
|
||||
* due to specific of range constructions in regular expressions.
|
||||
*/
|
||||
fun add(ch: Int): CharClass {
|
||||
var character = ch
|
||||
if (ignoreCase) {
|
||||
if (character.toChar() in 'a'..'z' || character.toChar() in 'A'..'Z') {
|
||||
bits_.set(character.asciiSupplement, !inverted)
|
||||
} else if (character > 128) {
|
||||
character = Char.toLowerCase(Char.toUpperCase(character))
|
||||
}
|
||||
}
|
||||
if (character.toChar().isSurrogate()) {
|
||||
lowHighSurrogates.set(character - Char.MIN_SURROGATE.toInt(), !invertedSurrogates)
|
||||
}
|
||||
bits_.set(character, !inverted)
|
||||
if (!mayContainSupplCodepoints && Char.isSupplementaryCodePoint(ch)) {
|
||||
mayContainSupplCodepoints = true
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun add(ch: Char): CharClass = add(ch.toInt())
|
||||
|
||||
/*
|
||||
* The difference between add(AbstractCharClass) and union(AbstractCharClass)
|
||||
* is that add() is used for constructions like "[^abc\\d]"
|
||||
* (this pattern doesn't match "1")
|
||||
* while union is used for constructions like "[^abc[\\d]]"
|
||||
* (this pattern matches "1").
|
||||
*/
|
||||
fun add(another: AbstractCharClass): CharClass {
|
||||
|
||||
if (!mayContainSupplCodepoints && another.mayContainSupplCodepoints) {
|
||||
mayContainSupplCodepoints = true
|
||||
}
|
||||
|
||||
// Process surrogates.
|
||||
if (!invertedSurrogates) {
|
||||
|
||||
//A | !B = ! ((A ^ B) & B)
|
||||
if (another.altSurrogates) {
|
||||
lowHighSurrogates.xor(another.lowHighSurrogates)
|
||||
lowHighSurrogates.and(another.lowHighSurrogates)
|
||||
altSurrogates = !altSurrogates
|
||||
invertedSurrogates = true
|
||||
|
||||
//A | B
|
||||
} else {
|
||||
lowHighSurrogates.or(another.lowHighSurrogates)
|
||||
}
|
||||
} else {
|
||||
|
||||
//!A | !B = !(A & B)
|
||||
if (another.altSurrogates) {
|
||||
lowHighSurrogates.and(another.lowHighSurrogates)
|
||||
|
||||
//!A | B = !(A & !B)
|
||||
} else {
|
||||
lowHighSurrogates.andNot(another.lowHighSurrogates)
|
||||
}
|
||||
}
|
||||
|
||||
val anotherBits = another.bits
|
||||
if (!hideBits && anotherBits != null) {
|
||||
if (!inverted) {
|
||||
|
||||
//A | !B = ! ((A ^ B) & B)
|
||||
if (another.isNegative()) {
|
||||
bits_.xor(anotherBits)
|
||||
bits_.and(anotherBits)
|
||||
alt = !alt
|
||||
inverted = true
|
||||
|
||||
//A | B
|
||||
} else {
|
||||
bits_.or(anotherBits)
|
||||
}
|
||||
} else {
|
||||
|
||||
//!A | !B = !(A & B)
|
||||
if (another.isNegative()) {
|
||||
bits_.and(anotherBits)
|
||||
|
||||
//!A | B = !(A & !B)
|
||||
} else {
|
||||
bits_.andNot(anotherBits)
|
||||
}
|
||||
}
|
||||
// Some of charclasses hides its bits
|
||||
} else {
|
||||
val curAlt = alt
|
||||
|
||||
if (nonBitSet == null) {
|
||||
|
||||
if (curAlt && !inverted && bits_.isEmpty) {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return another.contains(ch)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
/*
|
||||
* We keep the value of alt unchanged for
|
||||
* constructions like [^[abc]fgb] by using
|
||||
* the formula a ^ b == !a ^ !b.
|
||||
*/
|
||||
if (curAlt) {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return !(curAlt xor bits_.get(ch) || curAlt xor inverted xor another.contains(ch))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return curAlt xor bits_.get(ch) || curAlt xor inverted xor another.contains(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideBits = true
|
||||
} else {
|
||||
val nb = nonBitSet
|
||||
|
||||
if (curAlt) {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return !(curAlt xor (nb!!.contains(ch) || another.contains(ch)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return curAlt xor (nb!!.contains(ch) || another.contains(ch))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun add(start: Int, end: Int): CharClass {
|
||||
if (start > end)
|
||||
throw IllegalArgumentException("Incorrect range of symbols (start > end)")
|
||||
val minSurrogate = Char.MIN_SURROGATE.toInt()
|
||||
val maxSurrogate = Char.MAX_SURROGATE.toInt()
|
||||
if (ignoreCase) {
|
||||
// TODO: Make a faster implementation.
|
||||
for (i in start..end) {
|
||||
add(i)
|
||||
}
|
||||
} else {
|
||||
// No intersection with surrogate characters.
|
||||
if (end < minSurrogate || start > maxSurrogate) {
|
||||
bits_.set(start, end + 1, !inverted)
|
||||
} else {
|
||||
val surrogatesStart = maxOf(start, minSurrogate)
|
||||
val surrogatesEnd = minOf(end, maxSurrogate)
|
||||
bits_.set(start, end + 1, !inverted)
|
||||
lowHighSurrogates.set(surrogatesStart - minSurrogate,
|
||||
surrogatesEnd - minSurrogate + 1,
|
||||
!invertedSurrogates)
|
||||
if (!mayContainSupplCodepoints && end >= Char.MIN_SUPPLEMENTARY_CODE_POINT) {
|
||||
mayContainSupplCodepoints = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun add(start: Char, end: Char): CharClass = add(start.toInt(), end.toInt())
|
||||
|
||||
// OR operation
|
||||
fun union(another: AbstractCharClass) {
|
||||
if (!mayContainSupplCodepoints && another.mayContainSupplCodepoints) {
|
||||
mayContainSupplCodepoints = true
|
||||
}
|
||||
|
||||
|
||||
if (altSurrogates xor another.altSurrogates) {
|
||||
|
||||
//!A | B = !(A & !B)
|
||||
if (altSurrogates) {
|
||||
lowHighSurrogates.andNot(another.lowHighSurrogates)
|
||||
|
||||
//A | !B = !((A ^ B) & B)
|
||||
} else {
|
||||
lowHighSurrogates.xor(another.lowHighSurrogates)
|
||||
lowHighSurrogates.and(another.lowHighSurrogates)
|
||||
altSurrogates = true
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
//!A | !B = !(A & B)
|
||||
if (altSurrogates) {
|
||||
lowHighSurrogates.and(another.lowHighSurrogates)
|
||||
|
||||
//A | B
|
||||
} else {
|
||||
lowHighSurrogates.or(another.lowHighSurrogates)
|
||||
}
|
||||
}
|
||||
|
||||
val anotherBits = another.bits
|
||||
if (!hideBits && anotherBits != null) {
|
||||
if (alt xor another.isNegative()) {
|
||||
|
||||
//!A | B = !(A & !B)
|
||||
if (alt) {
|
||||
bits_.andNot(anotherBits)
|
||||
|
||||
//A | !B = !((A ^ B) & B)
|
||||
} else {
|
||||
bits_.xor(anotherBits)
|
||||
bits_.and(anotherBits)
|
||||
alt = true
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
//!A | !B = !(A & B)
|
||||
if (alt) {
|
||||
bits_.and(anotherBits)
|
||||
|
||||
//A | B
|
||||
} else {
|
||||
bits_.or(anotherBits)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val curAlt = alt
|
||||
|
||||
if (nonBitSet == null) {
|
||||
|
||||
if (!inverted && bits_.isEmpty) {
|
||||
if (curAlt) {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return !another.contains(ch)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return another.contains(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
if (curAlt) {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return !(another.contains(ch) || curAlt xor bits_.get(ch))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return another.contains(ch) || curAlt xor bits_.get(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hideBits = true
|
||||
} else {
|
||||
val nb = nonBitSet
|
||||
|
||||
if (curAlt) {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return !(curAlt xor nb!!.contains(ch) || another.contains(ch))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return curAlt xor nb!!.contains(ch) || another.contains(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AND operation
|
||||
fun intersection(another: AbstractCharClass) {
|
||||
if (!mayContainSupplCodepoints && another.mayContainSupplCodepoints) {
|
||||
mayContainSupplCodepoints = true
|
||||
}
|
||||
|
||||
if (altSurrogates xor another.altSurrogates) {
|
||||
|
||||
//!A & B = ((A ^ B) & B)
|
||||
if (altSurrogates) {
|
||||
lowHighSurrogates.xor(another.lowHighSurrogates)
|
||||
lowHighSurrogates.and(another.lowHighSurrogates)
|
||||
altSurrogates = false
|
||||
|
||||
//A & !B
|
||||
} else {
|
||||
lowHighSurrogates.andNot(another.lowHighSurrogates)
|
||||
}
|
||||
} else {
|
||||
|
||||
//!A & !B = !(A | B)
|
||||
if (altSurrogates) {
|
||||
lowHighSurrogates.or(another.lowHighSurrogates)
|
||||
|
||||
//A & B
|
||||
} else {
|
||||
lowHighSurrogates.and(another.lowHighSurrogates)
|
||||
}
|
||||
}
|
||||
|
||||
val anotherBits = another.bits
|
||||
if (!hideBits && anotherBits != null) {
|
||||
|
||||
if (alt xor another.isNegative()) {
|
||||
|
||||
//!A & B = ((A ^ B) & B)
|
||||
if (alt) {
|
||||
bits_.xor(anotherBits)
|
||||
bits_.and(anotherBits)
|
||||
alt = false
|
||||
|
||||
//A & !B
|
||||
} else {
|
||||
bits_.andNot(anotherBits)
|
||||
}
|
||||
} else {
|
||||
|
||||
//!A & !B = !(A | B)
|
||||
if (alt) {
|
||||
bits_.or(anotherBits)
|
||||
|
||||
//A & B
|
||||
} else {
|
||||
bits_.and(anotherBits)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val curAlt = alt
|
||||
|
||||
if (nonBitSet == null) {
|
||||
|
||||
if (!inverted && bits_.isEmpty) {
|
||||
if (curAlt) {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return !another.contains(ch)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return another.contains(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
if (curAlt) {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return !(another.contains(ch) && curAlt xor bits_.get(ch))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return another.contains(ch) && curAlt xor bits_.get(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hideBits = true
|
||||
} else {
|
||||
val nb = nonBitSet
|
||||
|
||||
if (curAlt) {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return !(curAlt xor nb!!.contains(ch) && another.contains(ch))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nonBitSet = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return curAlt xor nb!!.contains(ch) && another.contains(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if character class contains symbol specified,
|
||||
* `false` otherwise. Note: #setNegative() method changes the
|
||||
* meaning of contains method;
|
||||
|
||||
* @param ch
|
||||
* *
|
||||
* @return `true` if character class contains symbol specified;
|
||||
* */
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
if (nonBitSet == null) {
|
||||
return alt xor bits_.get(ch)
|
||||
} else {
|
||||
return alt xor nonBitSet!!.contains(ch)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
override val instance: AbstractCharClass
|
||||
get() {
|
||||
|
||||
if (nonBitSet == null) {
|
||||
val bs = bits
|
||||
|
||||
val res = object : AbstractCharClass() {
|
||||
override operator fun contains(ch: Int): Boolean {
|
||||
return this.alt xor bs!!.get(ch)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val temp = StringBuilder()
|
||||
var i = bs!!.nextSetBit(0)
|
||||
while (i >= 0) {
|
||||
temp.append(Char.toChars(i))
|
||||
temp.append('|')
|
||||
i = bs.nextSetBit(i + 1)
|
||||
}
|
||||
|
||||
if (temp.length > 0)
|
||||
temp.deleteAt(temp.length - 1)
|
||||
|
||||
return temp.toString()
|
||||
}
|
||||
|
||||
}
|
||||
return res.setNegative(isNegative())
|
||||
} else {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
//for debugging purposes only
|
||||
override fun toString(): String {
|
||||
val temp = StringBuilder()
|
||||
var i = bits_.nextSetBit(0)
|
||||
while (i >= 0) {
|
||||
temp.append(Char.toChars(i))
|
||||
temp.append('|')
|
||||
i = bits_.nextSetBit(i + 1)
|
||||
}
|
||||
|
||||
if (temp.length > 0)
|
||||
temp.deleteAt(temp.length - 1)
|
||||
|
||||
return temp.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,922 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
// TODO: Licenses.
|
||||
|
||||
@file:Suppress("DEPRECATION") // Char.toInt()
|
||||
package kotlin.text.regex
|
||||
|
||||
/**
|
||||
* This is base class for special tokens like character classes and quantifiers.
|
||||
*/
|
||||
internal abstract class SpecialToken {
|
||||
|
||||
/**
|
||||
* Returns the type of the token, may return following values:
|
||||
* TOK_CHARCLASS - token representing character class;
|
||||
* TOK_QUANTIFIER - token representing quantifier;
|
||||
*/
|
||||
abstract val type: Type
|
||||
|
||||
enum class Type {
|
||||
CHARCLASS,
|
||||
QUANTIFIER
|
||||
}
|
||||
}
|
||||
|
||||
internal class Lexer(val patternString: String, flags: Int) {
|
||||
|
||||
// The property is set in the init block after some transformations over the pattern string.
|
||||
private val pattern: CharArray
|
||||
|
||||
var flags = flags
|
||||
private set
|
||||
|
||||
// Modes ===========================================================================================================
|
||||
enum class Mode {
|
||||
PATTERN,
|
||||
RANGE,
|
||||
ESCAPE
|
||||
}
|
||||
|
||||
/**
|
||||
* Mode: whether the lexer processes character range ([Mode.RANGE]), escaped sequence ([Mode.RANGE])
|
||||
* or any other part of a regex ([PATTERN]).
|
||||
*/
|
||||
var mode = Mode.PATTERN
|
||||
private set
|
||||
|
||||
/** When in [Mode.ESCAPE] mode, this field will save the previous one */
|
||||
private var savedMode = Mode.PATTERN
|
||||
|
||||
fun setModeWithReread(value: Mode) {
|
||||
if(value == Mode.PATTERN || value == Mode.RANGE) {
|
||||
mode = value
|
||||
}
|
||||
if (mode == Mode.PATTERN) {
|
||||
reread()
|
||||
}
|
||||
}
|
||||
|
||||
// Tokens ==========================================================================================================
|
||||
internal var lookBack: Int = 0 // Previous char read.
|
||||
private set
|
||||
internal var currentChar: Int = 0 // Current character read. Returns 0 if there is no more characters.
|
||||
private set
|
||||
internal var lookAhead: Int = 0 // Next character.
|
||||
private set
|
||||
|
||||
internal var curSpecialToken: SpecialToken? = null // Current special token (e.g. quantifier)
|
||||
private set
|
||||
internal var lookAheadSpecialToken: SpecialToken? = null // Next special token
|
||||
private set
|
||||
|
||||
// Indices in the pattern.
|
||||
var index = 0 // Current char being processed index.
|
||||
private set
|
||||
var prevNonWhitespaceIndex = 0 // Previous non-whitespace character index.
|
||||
private set
|
||||
var curTokenIndex = 0 // Current token start index.
|
||||
private set
|
||||
var lookAheadTokenIndex = 0 // Next token index.
|
||||
private set
|
||||
|
||||
init {
|
||||
var processedPattern = patternString
|
||||
if (flags and Pattern.LITERAL > 0) {
|
||||
processedPattern = Pattern.quote(patternString)
|
||||
} else if (flags and Pattern.CANON_EQ > 0) {
|
||||
processedPattern = Lexer.normalize(patternString)
|
||||
}
|
||||
|
||||
this.pattern = processedPattern.toCharArray().copyOf(processedPattern.length + 2)
|
||||
this.pattern[this.pattern.size - 1] = 0.toChar()
|
||||
this.pattern[this.pattern.size - 2] = 0.toChar()
|
||||
|
||||
// Read first two tokens.
|
||||
movePointer()
|
||||
movePointer()
|
||||
}
|
||||
|
||||
// Character checks ================================================================================================
|
||||
/** Returns true, if current token is special, i.e. quantifier, or other compound token. */
|
||||
val isSpecial: Boolean get() = curSpecialToken != null
|
||||
val isQuantifier: Boolean get() = isSpecial && curSpecialToken!!.type == SpecialToken.Type.QUANTIFIER
|
||||
val isNextSpecial: Boolean get() = lookAheadSpecialToken != null
|
||||
|
||||
private fun Int.isSurrogatePair() : Boolean {
|
||||
val high = (this ushr 16).toChar()
|
||||
val low = this.toChar()
|
||||
return high.isHighSurrogate() && low.isLowSurrogate()
|
||||
}
|
||||
|
||||
private fun Char.isLineSeparator(): Boolean =
|
||||
this == '\n' || this == '\r' || this == '\u0085' || this.toInt() or 1 == '\u2029'.toInt()
|
||||
|
||||
/** Checks if there are any characters in the pattern. */
|
||||
fun isEmpty(): Boolean =
|
||||
currentChar == 0 && lookAhead == 0 && index >= pattern.size && !isSpecial
|
||||
|
||||
/** Return true if the current character is letter, false otherwise .*/
|
||||
fun isLetter(): Boolean =
|
||||
!isEmpty() && !isSpecial && isLetter(currentChar)
|
||||
|
||||
/** Check if the current char is high/low surrogate. */
|
||||
fun isHighSurrogate(): Boolean = currentChar in 0xDBFF..0xD800
|
||||
fun isLowSurrogate(): Boolean = currentChar in 0xDFFF..0xDC00
|
||||
fun isSurrogate(): Boolean = isHighSurrogate() || isLowSurrogate()
|
||||
|
||||
/**
|
||||
* Restores flags for Lexer
|
||||
* @param flags
|
||||
*/
|
||||
fun restoreFlags(flags: Int) {
|
||||
this.flags = flags
|
||||
lookAhead = currentChar
|
||||
lookAheadSpecialToken = curSpecialToken
|
||||
|
||||
// curTokenIndex is an index of closing bracket ')'
|
||||
index = curTokenIndex + 1
|
||||
lookAheadTokenIndex = curTokenIndex
|
||||
movePointer()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return patternString
|
||||
}
|
||||
|
||||
// Processing index moving =========================================================================================
|
||||
/** Returns current character and moves string index to the next one. */
|
||||
operator fun next(): Int {
|
||||
movePointer()
|
||||
return lookBack
|
||||
}
|
||||
|
||||
/** Returns current special token and moves string index to the next one */
|
||||
fun nextSpecial(): SpecialToken? {
|
||||
val res = curSpecialToken
|
||||
movePointer()
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Reread current character. May be required if a previous token changes mode
|
||||
* to one with different character interpretation.
|
||||
*/
|
||||
private fun reread() {
|
||||
lookAhead = currentChar
|
||||
lookAheadSpecialToken = curSpecialToken
|
||||
index = lookAheadTokenIndex
|
||||
lookAheadTokenIndex = curTokenIndex
|
||||
movePointer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next character index to read and moves pointer to the next one.
|
||||
* If comments flag is on this method will skip comments and whitespaces.
|
||||
*
|
||||
* The following actions are equivalent if comments flag is off:
|
||||
* currentChar = pattern[index++] == currentChar = pattern[nextIndex]
|
||||
*/
|
||||
private fun nextIndex(): Int {
|
||||
prevNonWhitespaceIndex = index
|
||||
if (flags and Pattern.COMMENTS != 0) {
|
||||
skipComments()
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
return prevNonWhitespaceIndex
|
||||
}
|
||||
|
||||
/** Skips comments and whitespaces */
|
||||
private fun skipComments(): Int {
|
||||
val length = pattern.size - 2
|
||||
index++
|
||||
do {
|
||||
while (index < length && pattern[index].isWhitespace()) {
|
||||
index++
|
||||
}
|
||||
if (index < length && pattern[index] == '#') {
|
||||
index++
|
||||
while (index < length && !pattern[index].isLineSeparator()) {
|
||||
index++
|
||||
}
|
||||
} else {
|
||||
return index
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next code point in the pattern string.
|
||||
*/
|
||||
private fun nextCodePoint(): Int {
|
||||
val high = pattern[nextIndex()] // nextIndex skips comments and whitespaces if comments flag is on.
|
||||
if (high.isHighSurrogate()) {
|
||||
// Low and high chars may be delimited by spaces.
|
||||
val lowExpectedIndex = prevNonWhitespaceIndex + 1
|
||||
if (lowExpectedIndex < pattern.size) {
|
||||
val low = pattern[lowExpectedIndex]
|
||||
if (low.isLowSurrogate()) {
|
||||
nextIndex()
|
||||
return Char.toCodePoint(high, low)
|
||||
}
|
||||
}
|
||||
}
|
||||
return high.toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves pointer one position right. Saves the current character to [lookBack],
|
||||
* [lookAhead] to the current one and finally read one more to [lookAhead].
|
||||
*/
|
||||
private fun movePointer() {
|
||||
// swap pointers
|
||||
lookBack = currentChar
|
||||
currentChar = lookAhead
|
||||
curSpecialToken = lookAheadSpecialToken
|
||||
curTokenIndex = lookAheadTokenIndex
|
||||
lookAheadTokenIndex = index
|
||||
var reread: Boolean
|
||||
do {
|
||||
// Read the next character, analyze it and construct a token.
|
||||
lookAhead = if (index < pattern.size) nextCodePoint() else 0
|
||||
lookAheadSpecialToken = null
|
||||
|
||||
if (mode == Mode.ESCAPE) {
|
||||
processInEscapeMode()
|
||||
}
|
||||
|
||||
reread = when (mode) {
|
||||
Mode.PATTERN -> processInPatternMode()
|
||||
Mode.RANGE -> processInRangeMode()
|
||||
else -> false
|
||||
}
|
||||
} while (reread)
|
||||
}
|
||||
|
||||
// Special functions called from [movePointer] function to process chars in different modes ========================
|
||||
/**
|
||||
* Processing an escaped sequence like "\Q foo \E". Just skip a character if it is not \E.
|
||||
* Returns whether we need to reread the character or not
|
||||
*/
|
||||
private fun processInEscapeMode(): Boolean {
|
||||
if (lookAhead == '\\'.toInt()) {
|
||||
// Need not care about supplementary code points here.
|
||||
val lookAheadChar: Char = if (index < pattern.size) pattern[nextIndex()] else '\u0000'
|
||||
lookAhead = lookAheadChar.toInt()
|
||||
|
||||
if (lookAheadChar == 'E') {
|
||||
// If \E found - change the mode to the previous one and shift to the next char.
|
||||
mode = savedMode
|
||||
lookAhead = if (index <= pattern.size - 2) nextCodePoint() else 0
|
||||
} else {
|
||||
// If \ have no E - make a step back and return.
|
||||
lookAhead = '\\'.toInt()
|
||||
index = prevNonWhitespaceIndex
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Processes a next character in [Mode.PATTERN] mode. Returns whether we need to reread the character or not */
|
||||
private fun processInPatternMode(): Boolean {
|
||||
if (lookAhead.isSurrogatePair()) {
|
||||
return false
|
||||
}
|
||||
val lookAheadChar = lookAhead.toChar()
|
||||
|
||||
if (lookAheadChar == '\\') {
|
||||
return processEscapedChar()
|
||||
}
|
||||
|
||||
// TODO: Look like we can create a quantifier here.
|
||||
when (lookAheadChar) {
|
||||
// Quantifier (*, +, ?).
|
||||
'+', '*', '?' -> {
|
||||
val mode = if (index < pattern.size) pattern[index] else '*'
|
||||
// look at the next character to determine if the mode is greedy, reluctant or possessive.
|
||||
when (mode) {
|
||||
'+' -> { lookAhead = lookAhead or Lexer.QMOD_POSSESSIVE; nextIndex() }
|
||||
'?' -> { lookAhead = lookAhead or Lexer.QMOD_RELUCTANT; nextIndex() }
|
||||
else -> lookAhead = lookAhead or Lexer.QMOD_GREEDY
|
||||
}
|
||||
}
|
||||
|
||||
// Quantifier ({x,y}).
|
||||
'{' -> lookAheadSpecialToken = processQuantifier()
|
||||
|
||||
// $.
|
||||
'$' -> lookAhead = CHAR_DOLLAR
|
||||
|
||||
// A group or a special construction.
|
||||
'(' -> {
|
||||
if (pattern[index] != '?') {
|
||||
// Group
|
||||
lookAhead = CHAR_LEFT_PARENTHESIS
|
||||
} else {
|
||||
// Special constructs (non-capturing groups, look ahead/look behind etc).
|
||||
nextIndex()
|
||||
var char = pattern[index]
|
||||
var isLookBehind = false
|
||||
do {
|
||||
if (!isLookBehind) {
|
||||
when (char) {
|
||||
// Look ahead or an atomic group.
|
||||
'!' -> { lookAhead = CHAR_NEG_LOOKAHEAD; nextIndex() }
|
||||
'=' -> { lookAhead = CHAR_POS_LOOKAHEAD; nextIndex() }
|
||||
'>' -> { lookAhead = CHAR_ATOMIC_GROUP; nextIndex() }
|
||||
// Positive / negaitve look behind - need to check the next char.
|
||||
'<' -> {
|
||||
nextIndex()
|
||||
char = pattern[index]
|
||||
isLookBehind = true
|
||||
}
|
||||
// Flags.
|
||||
else -> {
|
||||
lookAhead = readFlags()
|
||||
|
||||
// We return `res = res or 1 shl 8` from readFlags() if we read (?idmsux-idmsux)
|
||||
if (lookAhead >= 256) {
|
||||
// Just flags (no non-capturing group with them). Erase auxiliary bit.
|
||||
lookAhead = lookAhead and 0xff
|
||||
flags = lookAhead
|
||||
lookAhead = lookAhead shl 16
|
||||
lookAhead = CHAR_FLAGS or lookAhead
|
||||
} else {
|
||||
// A non-capturing group with flags: (?<flags>:Foo)
|
||||
flags = lookAhead
|
||||
lookAhead = lookAhead shl 16
|
||||
lookAhead = CHAR_NONCAP_GROUP or lookAhead
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Process the second char for look behind construction.
|
||||
isLookBehind = false
|
||||
when (char) {
|
||||
'!' -> { lookAhead = CHAR_NEG_LOOKBEHIND; nextIndex() }
|
||||
'=' -> { lookAhead = CHAR_POS_LOOKBEHIND; nextIndex() }
|
||||
else -> throw PatternSyntaxException("Unknown look behind", patternString, curTokenIndex)
|
||||
}
|
||||
}
|
||||
} while (isLookBehind)
|
||||
}
|
||||
}
|
||||
|
||||
')' -> lookAhead = CHAR_RIGHT_PARENTHESIS
|
||||
'[' -> { lookAhead = CHAR_LEFT_SQUARE_BRACKET; mode = Mode.RANGE }
|
||||
'^' -> lookAhead = CHAR_CARET
|
||||
'|' -> lookAhead = CHAR_VERTICAL_BAR
|
||||
'.' -> lookAhead = CHAR_DOT
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Processes a character inside a range. Returns whether we need to reread the character or not */
|
||||
private fun processInRangeMode(): Boolean {
|
||||
if (lookAhead.isSurrogatePair()) {
|
||||
return false
|
||||
}
|
||||
val lookAheadChar = lookAhead.toChar()
|
||||
|
||||
when (lookAheadChar) {
|
||||
'\\' -> return processEscapedChar()
|
||||
'[' -> lookAhead = CHAR_LEFT_SQUARE_BRACKET
|
||||
']' -> lookAhead = CHAR_RIGHT_SQUARE_BRACKET
|
||||
'^' -> lookAhead = CHAR_CARET
|
||||
'&' -> lookAhead = CHAR_AMPERSAND
|
||||
'-' -> lookAhead = CHAR_HYPHEN
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Processes an escaped (\x) character in any mode. Returns whether we need to reread the character or not */
|
||||
private fun processEscapedChar() : Boolean {
|
||||
lookAhead = if (index < pattern.size - 2) {
|
||||
nextCodePoint()
|
||||
} else {
|
||||
throw PatternSyntaxException("Trailing \\", patternString, curTokenIndex)
|
||||
}
|
||||
|
||||
// The current code point cannot be a surrogate pair because it is an escaped special one.
|
||||
// Cast it to char or just skip it as if we pass through the else branch of the when below.
|
||||
if (lookAhead.isSurrogatePair()) {
|
||||
return false
|
||||
}
|
||||
val lookAheadChar = lookAhead.toChar()
|
||||
|
||||
when (lookAheadChar) {
|
||||
// Character class.
|
||||
'P', 'p' -> {
|
||||
val cs = parseCharClassName()
|
||||
val negative = lookAheadChar == 'P'
|
||||
|
||||
lookAheadSpecialToken = AbstractCharClass.getPredefinedClass(cs, negative)
|
||||
lookAhead = 0
|
||||
}
|
||||
|
||||
// Word/whitespace/digit.
|
||||
'w', 's', 'd', 'W', 'S', 'D' -> {
|
||||
lookAheadSpecialToken = AbstractCharClass.getPredefinedClass(
|
||||
pattern.concatToString(prevNonWhitespaceIndex, prevNonWhitespaceIndex + 1),
|
||||
false
|
||||
)
|
||||
lookAhead = 0
|
||||
}
|
||||
|
||||
// Enter in ESCAPE mode. Skip this \Q symbol.
|
||||
'Q' -> {
|
||||
savedMode = mode
|
||||
mode = Mode.ESCAPE
|
||||
return true
|
||||
}
|
||||
|
||||
// Special characters like tab, new line etc.
|
||||
't' -> lookAhead = '\t'.toInt()
|
||||
'n' -> lookAhead = '\n'.toInt()
|
||||
'r' -> lookAhead = '\r'.toInt()
|
||||
'f' -> lookAhead = '\u000C'.toInt()
|
||||
'a' -> lookAhead = '\u0007'.toInt()
|
||||
'e' -> lookAhead = '\u001B'.toInt()
|
||||
|
||||
// Back references to capturing groups.
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
|
||||
if (mode == Mode.PATTERN) {
|
||||
lookAhead = 0x80000000.toInt() or lookAhead // Captured group reference is 0x80...<group number>
|
||||
}
|
||||
}
|
||||
|
||||
// A literal: octal, hex, or hex unicode.
|
||||
'0' -> lookAhead = readOctals()
|
||||
'x' -> lookAhead = readHex("hexadecimal", 2)
|
||||
'u' -> lookAhead = readHex("Unicode", 4)
|
||||
|
||||
// Special characters like EOL, EOI etc
|
||||
'b' -> lookAhead = CHAR_WORD_BOUND
|
||||
'B' -> lookAhead = CHAR_NONWORD_BOUND
|
||||
'A' -> lookAhead = CHAR_START_OF_INPUT
|
||||
'G' -> lookAhead = CHAR_PREVIOUS_MATCH
|
||||
'Z' -> lookAhead = CHAR_END_OF_LINE
|
||||
'z' -> lookAhead = CHAR_END_OF_INPUT
|
||||
|
||||
// \cx - A control character corresponding to x.
|
||||
'c' -> {
|
||||
if (index < pattern.size - 2) {
|
||||
// Need not care about supplementary codepoints here.
|
||||
lookAhead = pattern[nextIndex()].toInt() and 0x1f
|
||||
} else {
|
||||
throw PatternSyntaxException("Illegal control sequence", patternString, curTokenIndex)
|
||||
}
|
||||
}
|
||||
|
||||
'C', 'E', 'F', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'R', 'T', 'U', 'V', 'X', 'Y', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'o', 'q', 'y' ->
|
||||
throw PatternSyntaxException("Illegal escape sequence", patternString, curTokenIndex)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Process [lookAhead] in assumption that it's quantifier. */
|
||||
private fun processQuantifier(): Quantifier {
|
||||
assert(lookAhead == '{'.toInt())
|
||||
val sb = StringBuilder(4)
|
||||
var min = -1
|
||||
var max = -1
|
||||
|
||||
// Obtain a min value.
|
||||
var char: Char = if (index < pattern.size) {
|
||||
pattern[nextIndex()]
|
||||
} else {
|
||||
throw PatternSyntaxException("Incorrect Quantifier Syntax", patternString, curTokenIndex)
|
||||
}
|
||||
while (char != '}') {
|
||||
|
||||
if (char == ',' && min < 0) {
|
||||
try {
|
||||
val minParsed = sb.toString().toInt()
|
||||
min = if (minParsed >= 0) minParsed else throw PatternSyntaxException("Incorrect Quantifier Syntax", patternString, curTokenIndex)
|
||||
sb.setLength(0)
|
||||
} catch (nfe: NumberFormatException) {
|
||||
throw PatternSyntaxException("Incorrect Quantifier Syntax", patternString, curTokenIndex)
|
||||
}
|
||||
} else {
|
||||
sb.append(char)
|
||||
}
|
||||
char = if (index < pattern.size) pattern[nextIndex()] else break
|
||||
}
|
||||
|
||||
if (char != '}') {
|
||||
throw PatternSyntaxException("Incorrect Quantifier Syntax", patternString, curTokenIndex)
|
||||
}
|
||||
|
||||
// Obtain a max value, if it exists
|
||||
if (sb.isNotEmpty()) {
|
||||
try {
|
||||
val maxParsed = sb.toString().toInt()
|
||||
max = if (maxParsed >= 0) maxParsed else throw PatternSyntaxException("Incorrect Quantifier Syntax", patternString, curTokenIndex)
|
||||
if (min < 0) {
|
||||
min = max
|
||||
}
|
||||
} catch (nfe: NumberFormatException) {
|
||||
throw PatternSyntaxException("Incorrect Quantifier Syntax", patternString, curTokenIndex)
|
||||
}
|
||||
}
|
||||
|
||||
if (min < 0 || max >=0 && max < min) {
|
||||
throw PatternSyntaxException("Incorrect Quantifier Syntax", patternString, curTokenIndex)
|
||||
}
|
||||
|
||||
val mod = if (index < pattern.size) pattern[index] else '*'
|
||||
when (mod) {
|
||||
'+' -> { lookAhead = Lexer.QUANT_COMP_P; nextIndex() }
|
||||
'?' -> { lookAhead = Lexer.QUANT_COMP_R; nextIndex() }
|
||||
else -> lookAhead = Lexer.QUANT_COMP
|
||||
}
|
||||
return Quantifier(min, max)
|
||||
}
|
||||
|
||||
// Reading methods for specific tokens =============================================================================
|
||||
/** Process expression flags given with (?idmsux-idmsux). Returns the flags processed. */
|
||||
private fun readFlags(): Int {
|
||||
var positive = true
|
||||
var result = flags
|
||||
|
||||
while (index < pattern.size) {
|
||||
val char = pattern[index]
|
||||
when (char) {
|
||||
'-' -> {
|
||||
if (!positive) {
|
||||
throw PatternSyntaxException("Illegal inline construct", patternString, curTokenIndex)
|
||||
}
|
||||
positive = false
|
||||
}
|
||||
|
||||
'i' -> result = if (positive)
|
||||
result or Pattern.CASE_INSENSITIVE
|
||||
else
|
||||
result xor Pattern.CASE_INSENSITIVE and result
|
||||
|
||||
'd' -> result = if (positive)
|
||||
result or Pattern.UNIX_LINES
|
||||
else
|
||||
result xor Pattern.UNIX_LINES and result
|
||||
|
||||
'm' -> result = if (positive)
|
||||
result or Pattern.MULTILINE
|
||||
else
|
||||
result xor Pattern.MULTILINE and result
|
||||
|
||||
's' -> result = if (positive)
|
||||
result or Pattern.DOTALL
|
||||
else
|
||||
result xor Pattern.DOTALL and result
|
||||
|
||||
// We don't support UNICODE_CASE.
|
||||
/*'u' -> result = if (positive)
|
||||
result or Pattern.UNICODE_CASE
|
||||
else
|
||||
result xor Pattern.UNICODE_CASE and result*/
|
||||
|
||||
'x' -> result = if (positive)
|
||||
result or Pattern.COMMENTS
|
||||
else
|
||||
result xor Pattern.COMMENTS and result
|
||||
|
||||
':' -> {
|
||||
nextIndex()
|
||||
return result
|
||||
}
|
||||
|
||||
')' -> {
|
||||
nextIndex()
|
||||
return result or (1 shl 8)
|
||||
}
|
||||
}
|
||||
nextIndex()
|
||||
}
|
||||
throw PatternSyntaxException("Illegal inline construct", patternString, curTokenIndex)
|
||||
}
|
||||
|
||||
/** Parse character classes names and verifies correction of the syntax */
|
||||
private fun parseCharClassName(): String {
|
||||
val sb = StringBuilder(10)
|
||||
if (index < pattern.size - 2) {
|
||||
// one symbol family
|
||||
if (pattern[index] != '{') {
|
||||
return "Is${pattern[nextIndex()]}"
|
||||
}
|
||||
|
||||
nextIndex() // Skip '{'
|
||||
var char = pattern[nextIndex()]
|
||||
while (index < pattern.size - 2 && char != '}') {
|
||||
sb.append(char)
|
||||
char = pattern[nextIndex()]
|
||||
}
|
||||
if (char != '}') throw PatternSyntaxException("Unclosed character family", patternString, curTokenIndex)
|
||||
}
|
||||
if (sb.isEmpty()) throw PatternSyntaxException("Empty character family", patternString, curTokenIndex)
|
||||
|
||||
val res = sb.toString()
|
||||
return when {
|
||||
res.length == 1 -> "Is$res"
|
||||
res.length > 3 && (res.startsWith("Is") || res.startsWith("In")) -> res.substring(2)
|
||||
else -> res
|
||||
}
|
||||
}
|
||||
|
||||
/** Process hexadecimal integer. */
|
||||
private fun readHex(radixName: String, max: Int): Int {
|
||||
val builder = StringBuilder(max)
|
||||
val length = pattern.size - 2
|
||||
var i = 0
|
||||
while (i < max && index < length) {
|
||||
builder.append(pattern[nextIndex()])
|
||||
i++
|
||||
}
|
||||
if (i == max) {
|
||||
try {
|
||||
return builder.toString().toInt(16)
|
||||
} catch (e: NumberFormatException) {}
|
||||
}
|
||||
throw PatternSyntaxException("Invalid $radixName escape sequence", patternString, curTokenIndex)
|
||||
}
|
||||
|
||||
/** Process octal integer. */
|
||||
private fun readOctals(): Int {
|
||||
val length = pattern.size - 2
|
||||
var result = 0
|
||||
var digit = digitOf(pattern[index], 8)
|
||||
if (digit == -1) {
|
||||
throw PatternSyntaxException("Invalid octal escape sequence", patternString, curTokenIndex)
|
||||
}
|
||||
val max = if (digit > 3) 2 else 3
|
||||
var i = 0
|
||||
while (i < max && index < length && digit != -1) {
|
||||
result *= 8
|
||||
result += digit
|
||||
nextIndex()
|
||||
digit = digitOf(pattern[index], 8)
|
||||
i++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Special characters.
|
||||
val CHAR_DOLLAR = 0xe0000000.toInt() or '$'.toInt()
|
||||
val CHAR_RIGHT_PARENTHESIS = 0xe0000000.toInt() or ')'.toInt()
|
||||
val CHAR_LEFT_SQUARE_BRACKET = 0xe0000000.toInt() or '['.toInt()
|
||||
val CHAR_RIGHT_SQUARE_BRACKET = 0xe0000000.toInt() or ']'.toInt()
|
||||
val CHAR_CARET = 0xe0000000.toInt() or '^'.toInt()
|
||||
val CHAR_VERTICAL_BAR = 0xe0000000.toInt() or '|'.toInt()
|
||||
val CHAR_AMPERSAND = 0xe0000000.toInt() or '&'.toInt()
|
||||
val CHAR_HYPHEN = 0xe0000000.toInt() or '-'.toInt()
|
||||
val CHAR_DOT = 0xe0000000.toInt() or '.'.toInt()
|
||||
val CHAR_LEFT_PARENTHESIS = 0x80000000.toInt() or '('.toInt()
|
||||
val CHAR_NONCAP_GROUP = 0xc0000000.toInt() or '('.toInt()
|
||||
val CHAR_POS_LOOKAHEAD = 0xe0000000.toInt() or '('.toInt()
|
||||
val CHAR_NEG_LOOKAHEAD = 0xf0000000.toInt() or '('.toInt()
|
||||
val CHAR_POS_LOOKBEHIND = 0xf8000000.toInt() or '('.toInt()
|
||||
val CHAR_NEG_LOOKBEHIND = 0xfc000000.toInt() or '('.toInt()
|
||||
val CHAR_ATOMIC_GROUP = 0xfe000000.toInt() or '('.toInt()
|
||||
val CHAR_FLAGS = 0xff000000.toInt() or '('.toInt()
|
||||
val CHAR_START_OF_INPUT = 0x80000000.toInt() or 'A'.toInt()
|
||||
val CHAR_WORD_BOUND = 0x80000000.toInt() or 'b'.toInt()
|
||||
val CHAR_NONWORD_BOUND = 0x80000000.toInt() or 'B'.toInt()
|
||||
val CHAR_PREVIOUS_MATCH = 0x80000000.toInt() or 'G'.toInt()
|
||||
val CHAR_END_OF_INPUT = 0x80000000.toInt() or 'z'.toInt()
|
||||
val CHAR_END_OF_LINE = 0x80000000.toInt() or 'Z'.toInt()
|
||||
|
||||
// Quantifier modes.
|
||||
val QMOD_GREEDY = 0xe0000000.toInt()
|
||||
val QMOD_RELUCTANT = 0xc0000000.toInt()
|
||||
val QMOD_POSSESSIVE = 0x80000000.toInt()
|
||||
|
||||
// Quantifiers.
|
||||
val QUANT_STAR = QMOD_GREEDY or '*'.toInt()
|
||||
val QUANT_STAR_P = QMOD_POSSESSIVE or '*'.toInt()
|
||||
val QUANT_STAR_R = QMOD_RELUCTANT or '*'.toInt()
|
||||
val QUANT_PLUS = QMOD_GREEDY or '+'.toInt()
|
||||
val QUANT_PLUS_P = QMOD_POSSESSIVE or '+'.toInt()
|
||||
val QUANT_PLUS_R = QMOD_RELUCTANT or '+'.toInt()
|
||||
val QUANT_ALT = QMOD_GREEDY or '?'.toInt()
|
||||
val QUANT_ALT_P = QMOD_POSSESSIVE or '?'.toInt()
|
||||
val QUANT_ALT_R = QMOD_RELUCTANT or '?'.toInt()
|
||||
val QUANT_COMP = QMOD_GREEDY or '{'.toInt()
|
||||
val QUANT_COMP_P = QMOD_POSSESSIVE or '{'.toInt()
|
||||
val QUANT_COMP_R = QMOD_RELUCTANT or '{'.toInt()
|
||||
|
||||
/** Returns true if [ch] is a plain token. */
|
||||
fun isLetter(ch: Int): Boolean {
|
||||
// All supplementary codepoints have integer value that is >= 0.
|
||||
return ch >= 0
|
||||
}
|
||||
|
||||
private fun String.codePointAt(index: Int): Int {
|
||||
val high = this[index]
|
||||
if (high.isHighSurrogate() && index + 1 < this.length) {
|
||||
val low = this[index + 1]
|
||||
if (low.isLowSurrogate()) {
|
||||
return Char.toCodePoint(high, low)
|
||||
}
|
||||
}
|
||||
return high.toInt()
|
||||
}
|
||||
|
||||
// Decomposition ===============================================================================================
|
||||
// Maximum length of decomposition.
|
||||
val MAX_DECOMPOSITION_LENGTH = 4
|
||||
// Maximum length of Hangul decomposition. Note that MAX_HANGUL_DECOMPOSITION_LENGTH <= MAX_DECOMPOSITION_LENGTH.
|
||||
val MAX_HANGUL_DECOMPOSITION_LENGTH = 3
|
||||
|
||||
/*
|
||||
* Following constants are needed for Hangul canonical decomposition.
|
||||
* Hangul decomposition algorithm and constants are taken according
|
||||
* to description at http://www.unicode.org/versions/Unicode4.0.0/ch03.pdf
|
||||
* "3.12 Conjoining Jamo Behavior"
|
||||
*/
|
||||
const val SBase = 0xAC00
|
||||
const val LBase = 0x1100
|
||||
const val VBase = 0x1161
|
||||
const val TBase = 0x11A7
|
||||
const val SCount = 11172
|
||||
const val LCount = 19
|
||||
const val VCount = 21
|
||||
const val TCount = 28
|
||||
const val NCount = 588
|
||||
|
||||
// Access to the decomposition tables. =========================================================================
|
||||
/** Gets canonical class for given codepoint from decomposition mappings table. */
|
||||
fun getCanonicalClass(ch: Int): Int = getCanonicalClassInternal(ch)
|
||||
|
||||
/** Tests Unicode codepoint if it is a boundary of decomposed Unicode codepoint. */
|
||||
fun isDecomposedCharBoundary(ch: Int): Boolean = getCanonicalClass(ch) == 0
|
||||
|
||||
/** Tests if given codepoint is a canonical decomposition of another codepoint. */
|
||||
fun hasSingleCodepointDecomposition(ch: Int): Boolean = hasSingleCodepointDecompositionInternal(ch)
|
||||
|
||||
/** Tests if given codepoint has canonical decomposition and given codepoint's canonical class is not 0. */
|
||||
fun hasDecompositionNonNullCanClass(ch: Int): Boolean =
|
||||
(ch == 0x0340) or (ch == 0x0341) or (ch == 0x0343) or (ch == 0x0344)
|
||||
|
||||
/** Gets decomposition for given codepoint from decomposition mappings table. */
|
||||
fun getDecomposition(ch: Int): IntArray? = getDecompositionInternal(ch)
|
||||
|
||||
// =============================================================================================================
|
||||
|
||||
/**
|
||||
* Normalize given string.
|
||||
*/
|
||||
fun normalize(input: String): String {
|
||||
val inputChars = input.toCharArray()
|
||||
val inputLength = inputChars.size
|
||||
var inputCodePointsIndex = 0
|
||||
var decompHangulIndex = 0
|
||||
|
||||
//codePoints of input
|
||||
val inputCodePoints = IntArray(inputLength)
|
||||
|
||||
//result of canonical decomposition of input
|
||||
var resCodePoints = IntArray(inputLength * MAX_DECOMPOSITION_LENGTH)
|
||||
|
||||
//current symbol's codepoint
|
||||
var ch: Int
|
||||
|
||||
//current symbol's decomposition
|
||||
var decomp: IntArray?
|
||||
|
||||
//result of canonical and Hangul decomposition of input
|
||||
val decompHangul: IntArray
|
||||
|
||||
//result of canonical decomposition of input in UTF-16 encoding
|
||||
val result = StringBuilder()
|
||||
|
||||
var i = 0
|
||||
while (i < inputLength) {
|
||||
ch = input.codePointAt(i)
|
||||
inputCodePoints[inputCodePointsIndex++] = ch
|
||||
i += if (Char.isSupplementaryCodePoint(ch)) 2 else 1
|
||||
}
|
||||
|
||||
// Canonical decomposition based on mappings in decomposition table.
|
||||
var resCodePointsIndex = decomposeString(inputCodePoints, inputCodePointsIndex, resCodePoints)
|
||||
|
||||
// Canonical ordering.
|
||||
// See http://www.unicode.org/reports/tr15/#Decomposition for details
|
||||
resCodePoints = Lexer.getCanonicalOrder(resCodePoints, resCodePointsIndex)
|
||||
|
||||
// Decomposition for Hangul syllables.
|
||||
// See http://www.unicode.org/reports/tr15/#Hangul for details
|
||||
decompHangul = IntArray(resCodePoints.size)
|
||||
@Suppress("NAME_SHADOWING")
|
||||
for (i in 0..resCodePointsIndex - 1) {
|
||||
val curSymb = resCodePoints[i]
|
||||
|
||||
decomp = getHangulDecomposition(curSymb)
|
||||
if (decomp == null) {
|
||||
decompHangul[decompHangulIndex++] = curSymb
|
||||
} else {
|
||||
// Note that Hangul decompositions have length that is equal 2 or 3.
|
||||
decompHangul[decompHangulIndex++] = decomp[0]
|
||||
decompHangul[decompHangulIndex++] = decomp[1]
|
||||
if (decomp.size == 3) {
|
||||
decompHangul[decompHangulIndex++] = decomp[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Translating into UTF-16 encoding
|
||||
@Suppress("NAME_SHADOWING")
|
||||
for (i in 0..decompHangulIndex - 1) {
|
||||
result.append(Char.toChars(decompHangul[i]))
|
||||
}
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Rearrange codepoints in [inputInts] according to canonical order. Return an array with rearranged codepoints.
|
||||
*/
|
||||
fun getCanonicalOrder(inputInts: IntArray, length: Int): IntArray {
|
||||
val inputLength = if (length < inputInts.size)
|
||||
length
|
||||
else
|
||||
inputInts.size
|
||||
|
||||
/*
|
||||
* Simple bubble-sort algorithm. Note that many codepoints have 0 canonical class, so this algorithm works
|
||||
* almost lineary in overwhelming majority of cases. This is due to specific of Unicode combining
|
||||
* classes and codepoints.
|
||||
*/
|
||||
for (i in 1..inputLength - 1) {
|
||||
var j = i - 1
|
||||
val iCanonicalClass = getCanonicalClass(inputInts[i])
|
||||
val ch: Int
|
||||
|
||||
if (iCanonicalClass == 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
while (j > -1) {
|
||||
if (getCanonicalClass(inputInts[j]) > iCanonicalClass) {
|
||||
j = j - 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ch = inputInts[i]
|
||||
for (k in i downTo j + 1 + 1) {
|
||||
inputInts[k] = inputInts[k - 1]
|
||||
}
|
||||
inputInts[j + 1] = ch
|
||||
}
|
||||
|
||||
return inputInts
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets decomposition for given Hangul syllable.
|
||||
* This is an implementation of Hangul decomposition algorithm
|
||||
* according to http://www.unicode.org/versions/Unicode4.0.0/ch03.pdf "3.12 Conjoining Jamo Behavior".
|
||||
*/
|
||||
fun getHangulDecomposition(ch: Int): IntArray? {
|
||||
val SIndex = ch - SBase
|
||||
|
||||
if (SIndex < 0 || SIndex >= SCount) {
|
||||
return null
|
||||
} else {
|
||||
val L = LBase + SIndex / NCount
|
||||
val V = VBase + SIndex % NCount / TCount
|
||||
var T = SIndex % TCount
|
||||
val decomp: IntArray
|
||||
|
||||
if (T == 0) {
|
||||
decomp = intArrayOf(L, V)
|
||||
} else {
|
||||
T = TBase + T
|
||||
decomp = intArrayOf(L, V, T)
|
||||
}
|
||||
return decomp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Match result implementation
|
||||
*/
|
||||
|
||||
internal class MatchResultImpl
|
||||
/**
|
||||
* @param input an input sequence for matching/searching.
|
||||
* @param regex a [Regex] instance used for matching/searching.
|
||||
* @param rightBound index in the [input] used as a right bound for matching/searching. Exclusive.
|
||||
*/
|
||||
constructor (internal val input: CharSequence,
|
||||
internal val regex: Regex) : MatchResult {
|
||||
|
||||
// Harmony's implementation ========================================================================================
|
||||
private val nativePattern = regex.nativePattern
|
||||
private val groupCount = nativePattern.capturingGroupCount
|
||||
private val groupBounds = IntArray(groupCount * 2) { -1 }
|
||||
|
||||
private val consumers = IntArray(nativePattern.consumersCount + 1) { -1 }
|
||||
|
||||
// Used by quantifiers to store a count of a quantified expression occurrences.
|
||||
val enterCounters: IntArray = IntArray( maxOf(nativePattern.groupQuantifierCount, 0) )
|
||||
|
||||
var startIndex: Int = 0
|
||||
set (startIndex: Int) {
|
||||
field = startIndex
|
||||
if (previousMatch < 0) {
|
||||
previousMatch = startIndex
|
||||
}
|
||||
}
|
||||
|
||||
var previousMatch = -1
|
||||
var mode = Regex.Mode.MATCH
|
||||
|
||||
private data class MatchResultState(val groupBounds: IntArray, val consumers: IntArray, val enterCounters: IntArray,
|
||||
val startIndex: Int, val previousMatch: Int)
|
||||
|
||||
private var state: MatchResultState? = null
|
||||
|
||||
internal fun saveState() {
|
||||
state = MatchResultState(groupBounds.copyOf(), consumers.copyOf(), enterCounters.copyOf(), startIndex, previousMatch)
|
||||
}
|
||||
|
||||
internal fun rollbackState(): Boolean {
|
||||
return state?.let {
|
||||
it.groupBounds.copyInto(groupBounds)
|
||||
it.consumers.copyInto(consumers)
|
||||
it.enterCounters.copyInto(enterCounters)
|
||||
startIndex = it.startIndex
|
||||
previousMatch = it.previousMatch
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
// MatchResult interface ===========================================================================================
|
||||
/** The range of indices in the original string where match was captured. */
|
||||
override val range: IntRange
|
||||
get() = getStart(0) until getEnd(0)
|
||||
|
||||
/** The substring from the input string captured by this match. */
|
||||
override val value: String
|
||||
get() = group(0) ?: throw AssertionError("No groupIndex #0 in the match result.")
|
||||
|
||||
/**
|
||||
* A collection of groups matched by the regular expression.
|
||||
*
|
||||
* This collection has size of `groupCount + 1` where `groupCount` is the count of groups in the regular expression.
|
||||
* Groups are indexed from 1 to `groupCount` and group with the index 0 corresponds to the entire match.
|
||||
*/
|
||||
// Create one object or several ones?
|
||||
override val groups: MatchGroupCollection = object: MatchGroupCollection, AbstractCollection<MatchGroup?>() {
|
||||
override val size: Int
|
||||
get() = this@MatchResultImpl.groupCount
|
||||
|
||||
|
||||
override fun iterator(): Iterator<MatchGroup?> {
|
||||
return object: Iterator<MatchGroup?> {
|
||||
var nextIndex: Int = 0
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return nextIndex < size
|
||||
}
|
||||
|
||||
override fun next(): MatchGroup? {
|
||||
if (!hasNext()) {
|
||||
throw NoSuchElementException()
|
||||
}
|
||||
return get(nextIndex++)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun get(index: Int): MatchGroup? {
|
||||
val value = group(index) ?: return null
|
||||
return MatchGroup(value, getStart(index) until getEnd(index))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of matched indexed group values.
|
||||
*
|
||||
* This list has size of `groupCount + 1` where `groupCount` is the count of groups in the regular expression.
|
||||
* Groups are indexed from 1 to `groupCount` and group with the index 0 corresponds to the entire match.
|
||||
*
|
||||
* If the group in the regular expression is optional and there were no match captured by that group,
|
||||
* corresponding item in [groupValues] is an empty string.
|
||||
*
|
||||
* @sample: samples.text.Regexps.matchDestructuringToGroupValues
|
||||
*/
|
||||
override val groupValues: List<String>
|
||||
get() = mutableListOf<String>().apply {
|
||||
for (i in 0 until groupCount) {
|
||||
this.add(group(i) ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
override fun next(): MatchResult? {
|
||||
var nextStart = range.endInclusive + 1
|
||||
// If the current match is empty - shift by 1.
|
||||
if (nextStart == range.start) {
|
||||
nextStart++
|
||||
}
|
||||
if (nextStart > input.length) {
|
||||
return null
|
||||
}
|
||||
return regex.find(input, nextStart)
|
||||
}
|
||||
// =================================================================================================================
|
||||
|
||||
|
||||
// Harmony's implementation ========================================================================================
|
||||
fun setConsumed(counter: Int, value: Int) {
|
||||
this.consumers[counter] = value
|
||||
}
|
||||
|
||||
fun getConsumed(counter: Int): Int {
|
||||
return this.consumers[counter]
|
||||
}
|
||||
|
||||
fun isCaptured(group: Int): Boolean = getStart(group) >= 0
|
||||
|
||||
// Setters and getters for starts and ends of groups ===============================================================
|
||||
internal fun setStart(group: Int, offset: Int) {
|
||||
checkGroup(group)
|
||||
groupBounds[group * 2] = offset
|
||||
}
|
||||
|
||||
internal fun setEnd(group: Int, offset: Int) {
|
||||
checkGroup(group)
|
||||
groupBounds[group * 2 + 1] = offset
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the first character of the text that matched a given group.
|
||||
*
|
||||
* @param group the group, ranging from 0 to groupCount() - 1, with 0 representing the whole pattern.
|
||||
* @return the character index.
|
||||
*/
|
||||
fun getStart(group: Int = 0): Int {
|
||||
checkGroup(group)
|
||||
return groupBounds[group * 2]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the first character following the text that matched a given group.
|
||||
*
|
||||
* @param group the group, ranging from 0 to groupCount() - 1, with 0 representing the whole pattern.
|
||||
* @return the character index.
|
||||
*/
|
||||
fun getEnd(group: Int = 0): Int {
|
||||
checkGroup(group)
|
||||
return groupBounds[group * 2 + 1]
|
||||
}
|
||||
|
||||
// ==================================================================================================
|
||||
|
||||
/**
|
||||
* Returns the text that matched a given group of the regular expression.
|
||||
*
|
||||
* @param group the group, ranging from 0 to groupCount() - 1, with 0 representing the whole pattern.
|
||||
* @return the text that matched the group.
|
||||
*/
|
||||
fun group(group: Int = 0): String? {
|
||||
val start = getStart(group)
|
||||
val end = getEnd(group)
|
||||
if (start < 0 || end < 0) {
|
||||
return null
|
||||
}
|
||||
return input.subSequence(getStart(group), getEnd(group)).toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of groups in the result, which is always equal to
|
||||
* the number of groups in the original regular expression.
|
||||
*
|
||||
* @return the number of groups.
|
||||
*/
|
||||
fun groupCount(): Int {
|
||||
return groupCount - 1
|
||||
}
|
||||
|
||||
/*
|
||||
* This method being called after any successful match; For now it's being
|
||||
* used to check zero group for empty match;
|
||||
*/
|
||||
fun finalizeMatch() {
|
||||
if (this.groupBounds[0] == -1) {
|
||||
this.groupBounds[0] = this.startIndex
|
||||
this.groupBounds[1] = this.startIndex
|
||||
}
|
||||
|
||||
previousMatch = getEnd()
|
||||
}
|
||||
|
||||
private fun checkGroup(group: Int) {
|
||||
if (group < 0 || group > groupCount) {
|
||||
throw IndexOutOfBoundsException("Group index out of bounds: $group")
|
||||
}
|
||||
}
|
||||
|
||||
fun updateGroup(index: Int, srtOffset: Int, endOffset: Int) {
|
||||
checkGroup(index)
|
||||
groupBounds[index * 2] = srtOffset
|
||||
groupBounds[index * 2 + 1] = endOffset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,879 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION") // Char.toInt()
|
||||
package kotlin.text.regex
|
||||
|
||||
/** Represents a compiled pattern used by [Regex] for matching, searching, or replacing strings. */
|
||||
internal class Pattern(val pattern: String, flags: Int = 0) {
|
||||
|
||||
var flags = flags
|
||||
private set
|
||||
|
||||
/** A lexer instance used to get tokens from the pattern. */
|
||||
private val lexemes = Lexer(pattern, flags)
|
||||
|
||||
/* All back references that may be used in pattern. */
|
||||
private val backRefs = arrayOfNulls<FSet>(BACK_REF_NUMBER)
|
||||
|
||||
/** Is true if back referenced sets replacement by second compilation pass is needed.*/
|
||||
private var needsBackRefReplacement = false
|
||||
|
||||
/** Global count of found capturing groups. */
|
||||
var capturingGroupCount = 0
|
||||
private set
|
||||
|
||||
/** A number of group quantifiers in the pattern */
|
||||
var groupQuantifierCount = 0
|
||||
private set
|
||||
|
||||
/**
|
||||
* A number of consumers found in the pattern.
|
||||
* Consumer is any expression ending with an FSet except capturing groups (they are counted by [capturingGroupCount])
|
||||
*/
|
||||
var consumersCount = 0
|
||||
private set
|
||||
|
||||
/** A node to start a matching/searching process by call startNode.matches/startNode.find. */
|
||||
internal val startNode: AbstractSet
|
||||
|
||||
/** Compiles the given pattern */
|
||||
init {
|
||||
if (flags != 0 && flags or flagsBitMask != flagsBitMask) {
|
||||
throw IllegalArgumentException("Invalid match flags value")
|
||||
}
|
||||
startNode = processExpression(-1, this.flags, null)
|
||||
|
||||
if (!lexemes.isEmpty()) {
|
||||
throw PatternSyntaxException("Trailing characters", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
|
||||
// Finalize compilation
|
||||
if (needsBackRefReplacement) {
|
||||
startNode.processSecondPass()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = pattern
|
||||
|
||||
/** Return true if the pattern has the specified flag */
|
||||
private fun hasFlag(flag: Int): Boolean = flags and flag == flag
|
||||
|
||||
// Compilation methods. ============================================================================================
|
||||
/** A->(a|)+ */
|
||||
private fun processAlternations(last: AbstractSet): AbstractSet {
|
||||
val auxRange = CharClass(hasFlag(Pattern.CASE_INSENSITIVE))
|
||||
while (!lexemes.isEmpty() && lexemes.isLetter()
|
||||
&& (lexemes.lookAhead == 0
|
||||
|| lexemes.lookAhead == Lexer.CHAR_VERTICAL_BAR
|
||||
|| lexemes.lookAhead == Lexer.CHAR_RIGHT_PARENTHESIS)) {
|
||||
auxRange.add(lexemes.next())
|
||||
if (lexemes.currentChar == Lexer.CHAR_VERTICAL_BAR) {
|
||||
lexemes.next()
|
||||
}
|
||||
}
|
||||
val rangeSet = processRangeSet(auxRange)
|
||||
rangeSet.next = last
|
||||
|
||||
return rangeSet
|
||||
}
|
||||
|
||||
/** E->AE; E->S|E; E->S; A->(a|)+ E->S(|S)* */
|
||||
private fun processExpression(ch: Int, newFlags: Int, last: AbstractSet?): AbstractSet {
|
||||
val children = ArrayList<AbstractSet>()
|
||||
val savedFlags = flags
|
||||
var saveChangedFlags = false
|
||||
|
||||
if (newFlags != flags) {
|
||||
flags = newFlags
|
||||
}
|
||||
|
||||
// Create a right finalizing set.
|
||||
val fSet: FSet
|
||||
when (ch) {
|
||||
// Special groups: non-capturing, look ahead/behind etc.
|
||||
Lexer.CHAR_NONCAP_GROUP -> fSet = NonCapFSet(consumersCount++)
|
||||
Lexer.CHAR_POS_LOOKAHEAD,
|
||||
Lexer.CHAR_NEG_LOOKAHEAD -> fSet = AheadFSet()
|
||||
Lexer.CHAR_POS_LOOKBEHIND,
|
||||
Lexer.CHAR_NEG_LOOKBEHIND -> fSet = BehindFSet(consumersCount++)
|
||||
Lexer.CHAR_ATOMIC_GROUP -> fSet = AtomicFSet(consumersCount++)
|
||||
// A Capturing group.
|
||||
else -> {
|
||||
if (last == null) {
|
||||
// Whole pattern - group #0.
|
||||
fSet = FinalSet()
|
||||
saveChangedFlags = true
|
||||
} else {
|
||||
fSet = FSet(capturingGroupCount)
|
||||
}
|
||||
if (capturingGroupCount < BACK_REF_NUMBER) {
|
||||
backRefs[capturingGroupCount] = fSet
|
||||
}
|
||||
capturingGroupCount++
|
||||
}
|
||||
}
|
||||
|
||||
//Process to EOF or ')'
|
||||
do {
|
||||
val child: AbstractSet
|
||||
when {
|
||||
// a|...
|
||||
lexemes.isLetter() && lexemes.lookAhead == Lexer.CHAR_VERTICAL_BAR -> child = processAlternations(fSet)
|
||||
// ..|.., e.g. in "a||||b"
|
||||
lexemes.currentChar == Lexer.CHAR_VERTICAL_BAR -> {
|
||||
child = EmptySet(fSet)
|
||||
lexemes.next()
|
||||
}
|
||||
else -> {
|
||||
child = processSubExpression(fSet)
|
||||
if (lexemes.currentChar == Lexer.CHAR_VERTICAL_BAR) {
|
||||
lexemes.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
children.add(child)
|
||||
} while (!(lexemes.isEmpty() || lexemes.currentChar == Lexer.CHAR_RIGHT_PARENTHESIS))
|
||||
|
||||
// |) or |<EOF> - add an empty node.
|
||||
if (lexemes.lookBack == Lexer.CHAR_VERTICAL_BAR) {
|
||||
children.add(EmptySet(fSet))
|
||||
}
|
||||
|
||||
// Restore flags.
|
||||
if (flags != savedFlags && !saveChangedFlags) {
|
||||
flags = savedFlags
|
||||
lexemes.restoreFlags(flags)
|
||||
}
|
||||
|
||||
when (ch) {
|
||||
Lexer.CHAR_NONCAP_GROUP -> return NonCapturingJointSet(children, fSet)
|
||||
Lexer.CHAR_POS_LOOKAHEAD -> return PositiveLookAheadSet(children, fSet)
|
||||
Lexer.CHAR_NEG_LOOKAHEAD -> return NegativeLookAheadSet(children, fSet)
|
||||
Lexer.CHAR_POS_LOOKBEHIND -> return PositiveLookBehindSet(children, fSet)
|
||||
Lexer.CHAR_NEG_LOOKBEHIND -> return NegativeLookBehindSet(children, fSet)
|
||||
Lexer.CHAR_ATOMIC_GROUP -> return AtomicJointSet(children, fSet)
|
||||
|
||||
else -> when (children.size) {
|
||||
0 -> return EmptySet(fSet)
|
||||
1 -> return SingleSet(children[0], fSet)
|
||||
else -> return JointSet(children, fSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* T->aaa
|
||||
*/
|
||||
private fun processSequence(): AbstractSet {
|
||||
val substring = StringBuilder()
|
||||
while (!lexemes.isEmpty()
|
||||
&& lexemes.isLetter()
|
||||
&& !lexemes.isSurrogate()
|
||||
&& (!lexemes.isNextSpecial && lexemes.lookAhead == 0 // End of a pattern.
|
||||
|| !lexemes.isNextSpecial && Lexer.isLetter(lexemes.lookAhead)
|
||||
|| lexemes.lookAhead == Lexer.CHAR_RIGHT_PARENTHESIS
|
||||
|| lexemes.lookAhead and 0x8000ffff.toInt() == Lexer.CHAR_LEFT_PARENTHESIS
|
||||
|| lexemes.lookAhead == Lexer.CHAR_VERTICAL_BAR
|
||||
|| lexemes.lookAhead == Lexer.CHAR_DOLLAR)) {
|
||||
val ch = lexemes.next()
|
||||
|
||||
if (Char.isSupplementaryCodePoint(ch)) {
|
||||
substring.append(Char.toChars(ch))
|
||||
} else {
|
||||
substring.append(ch.toChar())
|
||||
}
|
||||
}
|
||||
return SequenceSet(substring, hasFlag(CASE_INSENSITIVE))
|
||||
}
|
||||
|
||||
/**
|
||||
* D->a
|
||||
*/
|
||||
private fun processDecomposedChar(): AbstractSet {
|
||||
val codePoints = IntArray(Lexer.MAX_DECOMPOSITION_LENGTH)
|
||||
val codePointsHangul: CharArray
|
||||
var readCodePoints = 0
|
||||
var curSymb = -1
|
||||
var curSymbIndex = -1
|
||||
|
||||
if (!lexemes.isEmpty() && lexemes.isLetter()) {
|
||||
curSymb = lexemes.next()
|
||||
codePoints[readCodePoints] = curSymb
|
||||
curSymbIndex = curSymb - Lexer.LBase
|
||||
}
|
||||
|
||||
/*
|
||||
* We process decomposed Hangul syllable LV or LVT or process jamo L.
|
||||
* See http://www.unicode.org/versions/Unicode4.0.0/ch03.pdf
|
||||
* "3.12 Conjoining Jamo Behavior"
|
||||
*/
|
||||
if (curSymbIndex >= 0 && curSymbIndex < Lexer.LCount) {
|
||||
codePointsHangul = CharArray(Lexer.MAX_HANGUL_DECOMPOSITION_LENGTH)
|
||||
codePointsHangul[readCodePoints++] = curSymb.toChar()
|
||||
|
||||
curSymb = lexemes.currentChar
|
||||
curSymbIndex = curSymb - Lexer.VBase
|
||||
if (curSymbIndex >= 0 && curSymbIndex < Lexer.VCount) {
|
||||
codePointsHangul[readCodePoints++] = curSymb.toChar()
|
||||
lexemes.next()
|
||||
curSymb = lexemes.currentChar
|
||||
curSymbIndex = curSymb - Lexer.TBase
|
||||
if (curSymbIndex >= 0 && curSymbIndex < Lexer.TCount) {
|
||||
codePointsHangul[@Suppress("UNUSED_CHANGED_VALUE")readCodePoints++] = curSymb.toChar()
|
||||
lexemes.next()
|
||||
|
||||
//LVT syllable
|
||||
return HangulDecomposedCharSet(codePointsHangul, 3)
|
||||
} else {
|
||||
|
||||
//LV syllable
|
||||
return HangulDecomposedCharSet(codePointsHangul, 2)
|
||||
}
|
||||
} else {
|
||||
|
||||
//L jamo
|
||||
return CharSet(codePointsHangul[0], hasFlag(CASE_INSENSITIVE))
|
||||
}
|
||||
|
||||
/*
|
||||
* We process single codepoint or decomposed codepoint.
|
||||
* We collect decomposed codepoint and obtain
|
||||
* one DecomposedCharSet.
|
||||
*/
|
||||
} else {
|
||||
readCodePoints++
|
||||
|
||||
while (readCodePoints < Lexer.MAX_DECOMPOSITION_LENGTH
|
||||
&& !lexemes.isEmpty() && lexemes.isLetter()
|
||||
&& !Lexer.isDecomposedCharBoundary(lexemes.currentChar)) {
|
||||
codePoints[readCodePoints++] = lexemes.next()
|
||||
}
|
||||
|
||||
/*
|
||||
* We have read an ordinary symbol.
|
||||
*/
|
||||
if (readCodePoints == 1 && !Lexer.hasSingleCodepointDecomposition(codePoints[0])) {
|
||||
return processCharSet(codePoints[0])
|
||||
} else {
|
||||
return DecomposedCharSet(codePoints, readCodePoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* S->BS; S->QS; S->Q; B->a+
|
||||
*/
|
||||
private fun processSubExpression(last: AbstractSet): AbstractSet {
|
||||
var cur: AbstractSet
|
||||
when {
|
||||
lexemes.isLetter() && !lexemes.isNextSpecial && Lexer.isLetter(lexemes.lookAhead) -> {
|
||||
when {
|
||||
hasFlag(Pattern.CANON_EQ) -> {
|
||||
cur = processDecomposedChar()
|
||||
if (!lexemes.isEmpty()
|
||||
&& (lexemes.currentChar != Lexer.CHAR_RIGHT_PARENTHESIS || last is FinalSet)
|
||||
&& lexemes.currentChar != Lexer.CHAR_VERTICAL_BAR
|
||||
&& !lexemes.isLetter()) {
|
||||
|
||||
cur = processQuantifier(last, cur)
|
||||
}
|
||||
}
|
||||
lexemes.isHighSurrogate() || lexemes.isLowSurrogate() -> {
|
||||
val term = processTerminal(last)
|
||||
cur = processQuantifier(last, term)
|
||||
}
|
||||
else -> {
|
||||
cur = processSequence()
|
||||
}
|
||||
}
|
||||
}
|
||||
lexemes.currentChar == Lexer.CHAR_RIGHT_PARENTHESIS -> {
|
||||
if (last is FinalSet) {
|
||||
throw PatternSyntaxException("unmatched )", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
cur = EmptySet(last)
|
||||
}
|
||||
else -> {
|
||||
val term = processTerminal(last)
|
||||
cur = processQuantifier(last, term)
|
||||
}
|
||||
}
|
||||
|
||||
if (!lexemes.isEmpty()
|
||||
&& (lexemes.currentChar != Lexer.CHAR_RIGHT_PARENTHESIS || last is FinalSet)
|
||||
&& lexemes.currentChar != Lexer.CHAR_VERTICAL_BAR) {
|
||||
|
||||
val next = processSubExpression(last)
|
||||
if (cur is LeafQuantifierSet
|
||||
// '*' or '{0,}' quantifier
|
||||
&& cur.max == Quantifier.INF
|
||||
&& cur.min == 0
|
||||
&& !next.first(cur.innerSet)) {
|
||||
// An Optimizer node for the case where there is no intersection with the next node
|
||||
cur = UnifiedQuantifierSet(cur)
|
||||
}
|
||||
cur.next = next
|
||||
} else {
|
||||
cur.next = last
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
/**
|
||||
* Q->T(*|+|?...) also do some optimizations.
|
||||
*/
|
||||
private fun processQuantifier(last: AbstractSet, term: AbstractSet): AbstractSet {
|
||||
val quant = lexemes.currentChar
|
||||
|
||||
if (term !is LeafSet) {
|
||||
return when (quant) {
|
||||
Lexer.QUANT_STAR, Lexer.QUANT_PLUS -> {
|
||||
val q: QuantifierSet
|
||||
|
||||
lexemes.next()
|
||||
if (term.type == AbstractSet.TYPE_DOTSET) {
|
||||
q = DotQuantifierSet(term, last, quant, AbstractLineTerminator.getInstance(flags), hasFlag(Pattern.DOTALL))
|
||||
} else {
|
||||
q = GroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
|
||||
}
|
||||
term.next = q
|
||||
q
|
||||
}
|
||||
|
||||
Lexer.QUANT_ALT -> {
|
||||
lexemes.next()
|
||||
val q = GroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
|
||||
term.next = q
|
||||
q
|
||||
}
|
||||
|
||||
Lexer.QUANT_STAR_R, Lexer.QUANT_PLUS_R, Lexer.QUANT_ALT_R -> {
|
||||
lexemes.next()
|
||||
val q = ReluctantGroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
|
||||
term.next = q
|
||||
q
|
||||
}
|
||||
|
||||
Lexer.QUANT_PLUS_P, Lexer.QUANT_STAR_P, Lexer.QUANT_ALT_P -> {
|
||||
lexemes.next()
|
||||
PossessiveGroupQuantifierSet(Quantifier.fromLexerToken(quant), term, last, quant, groupQuantifierCount++)
|
||||
}
|
||||
|
||||
Lexer.QUANT_COMP -> {
|
||||
val q = GroupQuantifierSet(lexemes.nextSpecial() as Quantifier, term, last, Lexer.QUANT_ALT, groupQuantifierCount++)
|
||||
term.next = q
|
||||
q
|
||||
}
|
||||
|
||||
Lexer.QUANT_COMP_R -> {
|
||||
val q = ReluctantGroupQuantifierSet(lexemes.nextSpecial() as Quantifier, term, last, Lexer.QUANT_ALT, groupQuantifierCount++)
|
||||
term.next = q
|
||||
q
|
||||
}
|
||||
|
||||
Lexer.QUANT_COMP_P -> {
|
||||
return PossessiveGroupQuantifierSet(lexemes.nextSpecial() as Quantifier, term, last, Lexer.QUANT_ALT, groupQuantifierCount++)
|
||||
}
|
||||
|
||||
else -> term
|
||||
}
|
||||
} else {
|
||||
val leaf: LeafSet = term
|
||||
return when (quant) {
|
||||
Lexer.QUANT_STAR, Lexer.QUANT_PLUS -> {
|
||||
lexemes.next()
|
||||
val q = LeafQuantifierSet(Quantifier.fromLexerToken(quant), leaf, last, quant)
|
||||
leaf.next = q
|
||||
q
|
||||
}
|
||||
|
||||
Lexer.QUANT_STAR_R, Lexer.QUANT_PLUS_R -> {
|
||||
lexemes.next()
|
||||
val q = ReluctantLeafQuantifierSet(Quantifier.fromLexerToken(quant), leaf, last, quant)
|
||||
leaf.next = q
|
||||
q
|
||||
}
|
||||
|
||||
Lexer.QUANT_PLUS_P, Lexer.QUANT_STAR_P -> {
|
||||
lexemes.next()
|
||||
val q = PossessiveLeafQuantifierSet(Quantifier.fromLexerToken(quant), leaf, last, quant)
|
||||
leaf.next = q
|
||||
q
|
||||
}
|
||||
|
||||
Lexer.QUANT_ALT -> {
|
||||
lexemes.next()
|
||||
LeafQuantifierSet(Quantifier.altQuantifier, leaf, last, Lexer.QUANT_ALT)
|
||||
}
|
||||
|
||||
Lexer.QUANT_ALT_R -> {
|
||||
lexemes.next()
|
||||
ReluctantLeafQuantifierSet(Quantifier.altQuantifier, leaf, last, Lexer.QUANT_ALT_R)
|
||||
}
|
||||
|
||||
Lexer.QUANT_ALT_P -> {
|
||||
lexemes.next()
|
||||
PossessiveLeafQuantifierSet(Quantifier.altQuantifier, leaf, last, Lexer.QUANT_ALT_P)
|
||||
}
|
||||
|
||||
Lexer.QUANT_COMP -> {
|
||||
LeafQuantifierSet(lexemes.nextSpecial() as Quantifier, leaf, last, Lexer.QUANT_COMP)
|
||||
}
|
||||
Lexer.QUANT_COMP_R -> {
|
||||
ReluctantLeafQuantifierSet(lexemes.nextSpecial() as Quantifier, leaf, last, Lexer.QUANT_COMP_R)
|
||||
}
|
||||
Lexer.QUANT_COMP_P -> {
|
||||
ReluctantLeafQuantifierSet(lexemes.nextSpecial() as Quantifier, leaf, last, Lexer.QUANT_COMP_P)
|
||||
}
|
||||
|
||||
else -> term
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* T-> letter|[range]|{char-class}|(E)
|
||||
*/
|
||||
private fun processTerminal(last: AbstractSet): AbstractSet {
|
||||
val term: AbstractSet
|
||||
var char = lexemes.currentChar
|
||||
// Process flags: (?...)(?...)...
|
||||
while (char and 0xff00ffff.toInt() == Lexer.CHAR_FLAGS) {
|
||||
lexemes.next()
|
||||
flags = (char shr 16) and flagsBitMask
|
||||
char = lexemes.currentChar
|
||||
}
|
||||
// The terminal is some kind of group: (E). Call processExpression for it.
|
||||
if (char and 0x8000ffff.toInt() == Lexer.CHAR_LEFT_PARENTHESIS) {
|
||||
lexemes.next()
|
||||
var newFlags = flags
|
||||
if (char and 0xff00ffff.toInt() == Lexer.CHAR_NONCAP_GROUP) {
|
||||
newFlags = (char shr 16) and flagsBitMask
|
||||
}
|
||||
term = processExpression(char and 0xff00ffff.toInt(), newFlags, last) // Remove flags from the token.
|
||||
if (lexemes.currentChar != Lexer.CHAR_RIGHT_PARENTHESIS) {
|
||||
throw PatternSyntaxException("unmatched (", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
lexemes.next()
|
||||
} else {
|
||||
// Other terminals.
|
||||
when (char) {
|
||||
Lexer.CHAR_LEFT_SQUARE_BRACKET -> { // Range: [...]
|
||||
lexemes.next()
|
||||
var negative = false
|
||||
if (lexemes.currentChar == Lexer.CHAR_CARET) {
|
||||
negative = true
|
||||
lexemes.next()
|
||||
}
|
||||
|
||||
term = processRange(negative, last)
|
||||
if (lexemes.currentChar != Lexer.CHAR_RIGHT_SQUARE_BRACKET) {
|
||||
throw PatternSyntaxException("unmatched [", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
lexemes.setModeWithReread(Lexer.Mode.PATTERN)
|
||||
lexemes.next()
|
||||
}
|
||||
|
||||
Lexer.CHAR_DOT -> { // Dot: .
|
||||
lexemes.next()
|
||||
term = DotSet(AbstractLineTerminator.getInstance(flags), hasFlag(DOTALL))
|
||||
}
|
||||
|
||||
Lexer.CHAR_CARET -> { // Beginning of the string: ^
|
||||
lexemes.next()
|
||||
term = SOLSet(AbstractLineTerminator.getInstance(flags), hasFlag(MULTILINE))
|
||||
consumersCount++
|
||||
}
|
||||
|
||||
Lexer.CHAR_DOLLAR -> { // End of the string: $
|
||||
lexemes.next()
|
||||
term = EOLSet(consumersCount++, AbstractLineTerminator.getInstance(flags), hasFlag(MULTILINE))
|
||||
|
||||
}
|
||||
|
||||
// Word / non-word boundary.
|
||||
Lexer.CHAR_WORD_BOUND -> {
|
||||
lexemes.next()
|
||||
term = WordBoundarySet(true)
|
||||
}
|
||||
|
||||
Lexer.CHAR_NONWORD_BOUND -> {
|
||||
lexemes.next()
|
||||
term = WordBoundarySet(false)
|
||||
}
|
||||
|
||||
Lexer.CHAR_END_OF_INPUT -> { // End of an input: \z
|
||||
lexemes.next()
|
||||
term = EOISet()
|
||||
}
|
||||
|
||||
Lexer.CHAR_END_OF_LINE -> { // End of a line: \Z
|
||||
lexemes.next()
|
||||
term = EOLSet(consumersCount++, AbstractLineTerminator.getInstance(flags))
|
||||
}
|
||||
|
||||
Lexer.CHAR_START_OF_INPUT -> { // Start if an input: \A
|
||||
lexemes.next()
|
||||
term = SOLSet(AbstractLineTerminator.getInstance(flags))
|
||||
}
|
||||
|
||||
Lexer.CHAR_PREVIOUS_MATCH -> { // A previous match: \G
|
||||
lexemes.next()
|
||||
term = PreviousMatchSet()
|
||||
}
|
||||
|
||||
// Back references: \1, \2 etc.
|
||||
0x80000000.toInt() or '1'.toInt(),
|
||||
0x80000000.toInt() or '2'.toInt(),
|
||||
0x80000000.toInt() or '3'.toInt(),
|
||||
0x80000000.toInt() or '4'.toInt(),
|
||||
0x80000000.toInt() or '5'.toInt(),
|
||||
0x80000000.toInt() or '6'.toInt(),
|
||||
0x80000000.toInt() or '7'.toInt(),
|
||||
0x80000000.toInt() or '8'.toInt(),
|
||||
0x80000000.toInt() or '9'.toInt() -> {
|
||||
val number = (char and 0x7FFFFFFF) - '0'.toInt()
|
||||
if (number < capturingGroupCount) { // All is ok - the group exists.
|
||||
lexemes.next()
|
||||
term = BackReferenceSet(number, consumersCount++, hasFlag(CASE_INSENSITIVE))
|
||||
// backRefs[number] is proved to be not null because the group is already created (number < capturingGroupCount)
|
||||
backRefs[number]!!.isBackReferenced = true
|
||||
needsBackRefReplacement = true // And process back references in the second pass.
|
||||
} else {
|
||||
throw PatternSyntaxException("No such group yet exists at this point in the pattern", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// A special token (\D, \w etc), 'u0000' or the end of the pattern.
|
||||
0 -> {
|
||||
val cc: AbstractCharClass? = lexemes.curSpecialToken as AbstractCharClass?
|
||||
when {
|
||||
cc != null -> {
|
||||
term = processRangeSet(cc)
|
||||
lexemes.next()
|
||||
}
|
||||
!lexemes.isEmpty() -> {
|
||||
term = CharSet(char.toChar())
|
||||
lexemes.next()
|
||||
}
|
||||
else -> term = EmptySet(last)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
when {
|
||||
// A regular character.
|
||||
char >= 0 && !lexemes.isSpecial -> {
|
||||
term = processCharSet(char)
|
||||
lexemes.next()
|
||||
}
|
||||
char == Lexer.CHAR_VERTICAL_BAR -> {
|
||||
term = EmptySet(last)
|
||||
}
|
||||
char == Lexer.CHAR_RIGHT_PARENTHESIS -> {
|
||||
if (last is FinalSet) {
|
||||
throw PatternSyntaxException("unmatched )", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
term = EmptySet(last)
|
||||
}
|
||||
else -> {
|
||||
val current = if (lexemes.isSpecial) lexemes.curSpecialToken.toString() else char.toString()
|
||||
throw PatternSyntaxException("Dangling meta construction: $current", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return term
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process [...] ranges
|
||||
*/
|
||||
private fun processRange(negative: Boolean, last: AbstractSet): AbstractSet {
|
||||
val res = processRangeExpression(negative)
|
||||
val rangeSet = processRangeSet(res)
|
||||
rangeSet.next = last
|
||||
|
||||
return rangeSet
|
||||
}
|
||||
|
||||
private fun processRangeExpression(alt: Boolean): CharClass {
|
||||
var result = CharClass(hasFlag(Pattern.CASE_INSENSITIVE), alt)
|
||||
var buffer = -1
|
||||
var intersection = false
|
||||
var firstInClass = true
|
||||
|
||||
var notClosed = lexemes.currentChar != Lexer.CHAR_RIGHT_SQUARE_BRACKET
|
||||
while (!lexemes.isEmpty() && (notClosed || firstInClass)) {
|
||||
when (lexemes.currentChar) {
|
||||
|
||||
Lexer.CHAR_RIGHT_SQUARE_BRACKET -> {
|
||||
if (buffer >= 0) {
|
||||
result.add(buffer)
|
||||
}
|
||||
buffer = ']'.toInt()
|
||||
lexemes.next()
|
||||
}
|
||||
|
||||
Lexer.CHAR_LEFT_SQUARE_BRACKET -> {
|
||||
if (buffer >= 0) {
|
||||
result.add(buffer)
|
||||
buffer = -1
|
||||
}
|
||||
lexemes.next()
|
||||
var negative = false
|
||||
if (lexemes.currentChar == Lexer.CHAR_CARET) {
|
||||
lexemes.next()
|
||||
negative = true
|
||||
}
|
||||
|
||||
if (intersection)
|
||||
result.intersection(processRangeExpression(negative))
|
||||
else
|
||||
result.union(processRangeExpression(negative))
|
||||
intersection = false
|
||||
lexemes.next()
|
||||
}
|
||||
|
||||
Lexer.CHAR_AMPERSAND -> {
|
||||
if (buffer >= 0) {
|
||||
result.add(buffer)
|
||||
}
|
||||
buffer = lexemes.next() // buffer == Lexer.CHAR_AMPERSAND since next() returns currentChar.
|
||||
|
||||
/*
|
||||
* If there is a start for subrange we will do an intersection
|
||||
* otherwise treat '&' as a normal character
|
||||
*/
|
||||
if (lexemes.currentChar == Lexer.CHAR_AMPERSAND) {
|
||||
if (lexemes.lookAhead == Lexer.CHAR_LEFT_SQUARE_BRACKET) {
|
||||
lexemes.next()
|
||||
intersection = true
|
||||
buffer = -1
|
||||
} else {
|
||||
lexemes.next()
|
||||
if (firstInClass) {
|
||||
// Skip "&&" at "[&&...]" or "[^&&...]"
|
||||
result = processRangeExpression(false)
|
||||
} else {
|
||||
// Ignore "&&" at "[X&&]" ending where X != empty string
|
||||
if (lexemes.currentChar != Lexer.CHAR_RIGHT_SQUARE_BRACKET) {
|
||||
result.intersection(processRangeExpression(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//treat '&' as a normal character
|
||||
buffer = '&'.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.CHAR_HYPHEN -> {
|
||||
if (firstInClass
|
||||
|| lexemes.lookAhead == Lexer.CHAR_RIGHT_SQUARE_BRACKET
|
||||
|| lexemes.lookAhead == Lexer.CHAR_LEFT_SQUARE_BRACKET
|
||||
|| buffer < 0) {
|
||||
// Treat the hypen as a normal character.
|
||||
if (buffer >= 0) {
|
||||
result.add(buffer)
|
||||
}
|
||||
buffer = '-'.toInt()
|
||||
lexemes.next()
|
||||
} else {
|
||||
// A range.
|
||||
lexemes.next()
|
||||
var cur = lexemes.currentChar
|
||||
|
||||
if (!lexemes.isSpecial
|
||||
&& (cur >= 0
|
||||
|| lexemes.lookAhead == Lexer.CHAR_RIGHT_SQUARE_BRACKET
|
||||
|| lexemes.lookAhead == Lexer.CHAR_LEFT_SQUARE_BRACKET
|
||||
|| buffer < 0)) {
|
||||
|
||||
try {
|
||||
if (!Lexer.isLetter(cur)) {
|
||||
cur = cur and 0xFFFF
|
||||
}
|
||||
result.add(buffer, cur)
|
||||
} catch (e: Exception) {
|
||||
throw PatternSyntaxException("Illegal character range", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
|
||||
lexemes.next()
|
||||
buffer = -1
|
||||
} else {
|
||||
throw PatternSyntaxException("Illegal character range", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.CHAR_CARET -> {
|
||||
if (buffer >= 0) {
|
||||
result.add(buffer)
|
||||
}
|
||||
buffer = '^'.toInt()
|
||||
lexemes.next()
|
||||
}
|
||||
|
||||
0 -> {
|
||||
if (buffer >= 0) {
|
||||
result.add(buffer)
|
||||
}
|
||||
val cs = lexemes.curSpecialToken as AbstractCharClass?
|
||||
if (cs != null) {
|
||||
result.add(cs)
|
||||
buffer = -1
|
||||
} else {
|
||||
buffer = 0
|
||||
}
|
||||
|
||||
lexemes.next()
|
||||
}
|
||||
|
||||
else -> {
|
||||
if (buffer >= 0) {
|
||||
result.add(buffer)
|
||||
}
|
||||
buffer = lexemes.next()
|
||||
}
|
||||
}
|
||||
|
||||
firstInClass = false
|
||||
notClosed = lexemes.currentChar != Lexer.CHAR_RIGHT_SQUARE_BRACKET
|
||||
}
|
||||
if (notClosed) {
|
||||
throw PatternSyntaxException("Missing ']'", pattern, lexemes.curTokenIndex)
|
||||
}
|
||||
if (buffer >= 0) {
|
||||
result.add(buffer)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun processRangeSet(charClass: AbstractCharClass): AbstractSet {
|
||||
if (charClass.hasLowHighSurrogates()) {
|
||||
val lowHighSurrRangeSet = SurrogateRangeSet(charClass.classWithSurrogates())
|
||||
|
||||
if (charClass.mayContainSupplCodepoints) {
|
||||
return CompositeRangeSet(SupplementaryRangeSet(charClass.classWithoutSurrogates(), hasFlag(CASE_INSENSITIVE)), lowHighSurrRangeSet)
|
||||
}
|
||||
|
||||
return CompositeRangeSet(RangeSet(charClass.classWithoutSurrogates(), hasFlag(CASE_INSENSITIVE)), lowHighSurrRangeSet)
|
||||
}
|
||||
|
||||
if (charClass.mayContainSupplCodepoints) {
|
||||
return SupplementaryRangeSet(charClass, hasFlag(CASE_INSENSITIVE))
|
||||
}
|
||||
|
||||
return RangeSet(charClass, hasFlag(CASE_INSENSITIVE))
|
||||
}
|
||||
|
||||
private fun processCharSet(ch: Int): AbstractSet {
|
||||
val isSupplCodePoint = Char.isSupplementaryCodePoint(ch)
|
||||
|
||||
return when {
|
||||
isSupplCodePoint -> SequenceSet(Char.toChars(ch).concatToString(0, 2), hasFlag(CASE_INSENSITIVE))
|
||||
ch.toChar().isLowSurrogate() -> LowSurrogateCharSet(ch.toChar())
|
||||
ch.toChar().isHighSurrogate() -> HighSurrogateCharSet(ch.toChar())
|
||||
else -> CharSet(ch.toChar(), hasFlag(CASE_INSENSITIVE))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
//TODO: Use RegexOption enum here.
|
||||
// Flags.
|
||||
/**
|
||||
* This constant specifies that a pattern matches Unix line endings ('\n')
|
||||
* only against the '.', '^', and '$' meta characters.
|
||||
*/
|
||||
val UNIX_LINES = 1 shl 0
|
||||
|
||||
/**
|
||||
* This constant specifies that a `Pattern` is matched
|
||||
* case-insensitively. That is, the patterns "a+" and "A+" would both match
|
||||
* the string "aAaAaA".
|
||||
*/
|
||||
val CASE_INSENSITIVE = 1 shl 1
|
||||
|
||||
/**
|
||||
* This constant specifies that a `Pattern` may contain whitespace or
|
||||
* comments. Otherwise comments and whitespace are taken as literal
|
||||
* characters.
|
||||
*/
|
||||
val COMMENTS = 1 shl 2
|
||||
|
||||
/**
|
||||
* This constant specifies that the meta characters '^' and '$' match only
|
||||
* the beginning and end end of an input line, respectively. Normally, they
|
||||
* match the beginning and the end of the complete input.
|
||||
*/
|
||||
val MULTILINE = 1 shl 3
|
||||
|
||||
/**
|
||||
* This constant specifies that the whole `Pattern` is to be taken
|
||||
* literally, that is, all meta characters lose their meanings.
|
||||
*/
|
||||
val LITERAL = 1 shl 4
|
||||
|
||||
/**
|
||||
* This constant specifies that the '.' meta character matches arbitrary
|
||||
* characters, including line endings, which is normally not the case.
|
||||
*/
|
||||
val DOTALL = 1 shl 5
|
||||
|
||||
/**
|
||||
* This constant specifies that a character in a `Pattern` and a
|
||||
* character in the input string only match if they are canonically
|
||||
* equivalent.
|
||||
*/
|
||||
val CANON_EQ = 1 shl 6
|
||||
|
||||
/** Max number of back references supported. */
|
||||
internal val BACK_REF_NUMBER = 10
|
||||
|
||||
/** A bit mask that includes all defined match flags */
|
||||
internal val flagsBitMask = Pattern.UNIX_LINES or
|
||||
Pattern.CASE_INSENSITIVE or
|
||||
Pattern.COMMENTS or
|
||||
Pattern.MULTILINE or
|
||||
Pattern.LITERAL or
|
||||
Pattern.DOTALL or
|
||||
Pattern.CANON_EQ
|
||||
|
||||
|
||||
/**
|
||||
* Quotes a given string using "\Q" and "\E", so that all other meta-characters lose their special meaning.
|
||||
* If the string is used for a `Pattern` afterwards, it can only be matched literally.
|
||||
*/
|
||||
fun quote(s: String): String {
|
||||
return StringBuilder()
|
||||
.append("\\Q")
|
||||
.append(s.replace("\\E", "\\E\\\\E\\Q"))
|
||||
.append("\\E").toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
import kotlin.IllegalArgumentException
|
||||
|
||||
/**
|
||||
* Represents RE quantifier; contains two fields responsible for min and max number of repetitions.
|
||||
* -1 as a maximum number of repetition represents infinity(i.e. +,*).
|
||||
*/
|
||||
internal class Quantifier(val min: Int, val max: Int = min) : SpecialToken() {
|
||||
|
||||
init {
|
||||
if (min < 0 || max < -1) {
|
||||
throw IllegalArgumentException("Incorrect quantifier value: $this")
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = "{$min, ${if (max == INF) "" else max}}"
|
||||
|
||||
override val type: Type = SpecialToken.Type.QUANTIFIER
|
||||
|
||||
companion object {
|
||||
val starQuantifier = Quantifier(0, -1)
|
||||
val plusQuantifier = Quantifier(1, -1)
|
||||
val altQuantifier = Quantifier(0, 1)
|
||||
|
||||
val INF = -1
|
||||
|
||||
fun fromLexerToken(token: Int) = when(token) {
|
||||
Lexer.QUANT_STAR, Lexer.QUANT_STAR_P, Lexer.QUANT_STAR_R -> starQuantifier
|
||||
Lexer.QUANT_ALT, Lexer.QUANT_ALT_P, Lexer.QUANT_ALT_R -> altQuantifier
|
||||
Lexer.QUANT_PLUS, Lexer.QUANT_PLUS_P, Lexer.QUANT_PLUS_R -> plusQuantifier
|
||||
else -> throw IllegalArgumentException("Unknown quantifier token: $token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
import kotlin.AssertionError
|
||||
|
||||
/** Basic class for sets which have no complex next node handling. */
|
||||
internal abstract class SimpleSet : AbstractSet {
|
||||
override var next: AbstractSet = dummyNext
|
||||
constructor()
|
||||
constructor(type : Int): super(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic class for nodes, representing given regular expression.
|
||||
* Note: (Almost) All the classes representing nodes has 'set' suffix.
|
||||
*/
|
||||
internal abstract class AbstractSet(val type: Int = 0) {
|
||||
|
||||
companion object {
|
||||
const val TYPE_LEAF = 1 shl 0
|
||||
const val TYPE_FSET = 1 shl 1
|
||||
const val TYPE_QUANT = 1 shl 3
|
||||
@Suppress("DEPRECATION")
|
||||
const val TYPE_DOTSET = 0x80000000.toInt() or '.'.toInt()
|
||||
|
||||
val dummyNext = object : AbstractSet() {
|
||||
override var next: AbstractSet
|
||||
get() = throw AssertionError("This method is not expected to be called.")
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
set(value) {}
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl) =
|
||||
throw AssertionError("This method is not expected to be called.")
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean =
|
||||
throw AssertionError("This method is not expected to be called.")
|
||||
override fun processSecondPassInternal(): AbstractSet = this
|
||||
override fun processSecondPass(): AbstractSet = this
|
||||
}
|
||||
}
|
||||
|
||||
var secondPassVisited = false
|
||||
abstract var next: AbstractSet
|
||||
|
||||
protected open val name: String
|
||||
get() = ""
|
||||
|
||||
/**
|
||||
* Checks if this node matches in given position and recursively call
|
||||
* next node matches on positive self match. Returns positive integer if
|
||||
* entire match succeed, negative otherwise.
|
||||
* @param startIndex - string index to start from.
|
||||
* @param testString - input string.
|
||||
* @param matchResult - MatchResult to sore result into.
|
||||
* @return -1 if match fails or n > 0;
|
||||
*/
|
||||
abstract fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int
|
||||
|
||||
/**
|
||||
* Attempts to apply pattern starting from this set/startIndex; returns
|
||||
* index this search was started from, if value is negative, this means that
|
||||
* this search didn't succeed, additional information could be obtained via
|
||||
* matchResult.
|
||||
*
|
||||
* Note: this is default implementation for find method, it's based on
|
||||
* matches, subclasses do not have to override find method unless
|
||||
* more effective find method exists for a particular node type
|
||||
* (sequence, i.e. substring, for example). Same applies for find back
|
||||
* method.
|
||||
*
|
||||
* @param startIndex - starting index.
|
||||
* @param testString - string to search in.
|
||||
* @param matchResult - result of the match.
|
||||
* @return last searched index.
|
||||
*/
|
||||
open fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int
|
||||
= (startIndex..testString.length).firstOrNull { index -> matches(index, testString, matchResult) >= 0 }
|
||||
?: -1
|
||||
|
||||
/**
|
||||
* @param leftLimit - an index, to finish search back (left limit).
|
||||
* @param rightLimit - an index to start search from (right limit).
|
||||
* @param testString - test string.
|
||||
* @param matchResult - match result.
|
||||
* @return an index to start back search next time if this search fails(new left bound);
|
||||
* if this search fails the value is negative.
|
||||
*/
|
||||
open fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int
|
||||
= (rightLimit downTo leftLimit).firstOrNull { index -> matches(index, testString, matchResult) >= 0 }
|
||||
?: -1
|
||||
|
||||
/**
|
||||
* Returns true, if this node has consumed any characters during
|
||||
* positive match attempt, for example node representing character always
|
||||
* consumes one character if it matches. If particular node matches
|
||||
* empty sting this method will return false.
|
||||
*
|
||||
* @param matchResult - match result;
|
||||
* @return true if the node consumes any character and false otherwise.
|
||||
*/
|
||||
abstract fun hasConsumed(matchResult: MatchResultImpl): Boolean
|
||||
|
||||
/**
|
||||
* Returns true if the given node intersects with this one, false otherwise.
|
||||
* This method is being used for quantifiers construction, lets consider the
|
||||
* following regular expression (a|b)*ccc. (a|b) does not intersects with "ccc"
|
||||
* and thus can be quantified greedily (w/o kickbacks), like *+ instead of *.
|
||||
|
||||
* @param set - A node the intersection is checked for. Usually a previous node.
|
||||
* @return true if the given node intersects with this one, false otherwise.
|
||||
*/
|
||||
open fun first(set: AbstractSet): Boolean = true
|
||||
|
||||
/**
|
||||
* This method is used for replacement backreferenced sets.
|
||||
*
|
||||
* @return null if current node need not to be replaced,
|
||||
* [JointSet] which is replacement of current node otherwise.
|
||||
*/
|
||||
open fun processBackRefReplacement(): JointSet? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* This method performs the second pass without checking if it's already performed or not.
|
||||
*/
|
||||
protected open fun processSecondPassInternal(): AbstractSet {
|
||||
if (!next.secondPassVisited) {
|
||||
this.next = next.processSecondPass()
|
||||
}
|
||||
return processBackRefReplacement() ?: this
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used for traversing nodes after the first stage of compilation.
|
||||
*/
|
||||
open fun processSecondPass(): AbstractSet {
|
||||
secondPassVisited = true
|
||||
return processSecondPassInternal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* This class represent atomic group (?>X), once X matches, this match become unchangeable till the end of the match.
|
||||
*/
|
||||
open internal class AtomicJointSet(children: List<AbstractSet>, fSet: FSet) : NonCapturingJointSet(children, fSet) {
|
||||
|
||||
/** Returns startIndex+shift, the next position to match */
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val start = matchResult.getConsumed(groupIndex)
|
||||
matchResult.setConsumed(groupIndex, startIndex)
|
||||
children.forEach {
|
||||
val shift = it.matches(startIndex, testString, matchResult)
|
||||
if (shift >= 0) {
|
||||
// AtomicFset always returns true, but saves the index to run this next.match() from;
|
||||
return next.matches((fSet as AtomicFSet).index, testString, matchResult)
|
||||
}
|
||||
}
|
||||
|
||||
matchResult.setConsumed(groupIndex, start)
|
||||
return -1
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "AtomicJointSet"
|
||||
|
||||
override var next: AbstractSet = dummyNext
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Back reference node;
|
||||
*/
|
||||
open internal class BackReferenceSet(val referencedGroup: Int, val consCounter: Int, val ignoreCase: Boolean = false)
|
||||
: SimpleSet() {
|
||||
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val groupValue = getReferencedGroupValue(matchResult)
|
||||
|
||||
if (groupValue == null || startIndex + groupValue.length > testString.length) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (testString.startsWith(groupValue, startIndex, ignoreCase)) {
|
||||
matchResult.setConsumed(consCounter, groupValue.length)
|
||||
return next.matches(startIndex + groupValue.length, testString, matchResult)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val groupValue = getReferencedGroupValue(matchResult)
|
||||
if (groupValue == null || startIndex + groupValue.length > testString.length) {
|
||||
return -1
|
||||
}
|
||||
|
||||
var index = startIndex
|
||||
while (index <= testString.length) {
|
||||
index = testString.indexOf(groupValue, index, ignoreCase)
|
||||
if (index < 0) {
|
||||
return -1
|
||||
}
|
||||
if (index < testString.length
|
||||
&& next.matches(index + groupValue.length, testString, matchResult) >=0) {
|
||||
return index
|
||||
}
|
||||
index++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val groupValue = getReferencedGroupValue(matchResult)
|
||||
if (groupValue == null || leftLimit + groupValue.length > rightLimit) {
|
||||
return -1
|
||||
}
|
||||
|
||||
var index = rightLimit
|
||||
while (index >= leftLimit) {
|
||||
index = testString.lastIndexOf(groupValue, index, ignoreCase)
|
||||
if (index < 0) {
|
||||
return -1
|
||||
}
|
||||
if (index >= 0 && next.matches(index + groupValue.length, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
index--
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
|
||||
protected fun getReferencedGroupValue(matchResult: MatchResultImpl) = matchResult.group(referencedGroup)
|
||||
override val name: String
|
||||
get() = "back reference: $referencedGroup"
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
val result = matchResult.getConsumed(consCounter) != 0
|
||||
matchResult.setConsumed(consCounter, -1)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Represents node accepting single character.
|
||||
*/
|
||||
open internal class CharSet(char: Char, val ignoreCase: Boolean = false) : LeafSet() {
|
||||
|
||||
// We use only low case characters when working in case insensitive mode.
|
||||
val char: Char = if (ignoreCase) char.lowercaseChar() else char
|
||||
|
||||
// Overrides =======================================================================================================
|
||||
|
||||
override fun accepts(startIndex: Int, testString: CharSequence): Int {
|
||||
if (ignoreCase) {
|
||||
return if (this.char == testString[startIndex].lowercaseChar()) 1 else -1
|
||||
} else {
|
||||
return if (this.char == testString[startIndex]) 1 else -1
|
||||
}
|
||||
}
|
||||
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = startIndex
|
||||
while (index < testString.length) {
|
||||
index = testString.indexOf(char, index, ignoreCase)
|
||||
if (index < 0) {
|
||||
return -1
|
||||
}
|
||||
if (next.matches(index + charCount, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
index++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = rightLimit
|
||||
while (index >= leftLimit) {
|
||||
index = testString.lastIndexOf(char, index, ignoreCase)
|
||||
if (index < 0) {
|
||||
return -1
|
||||
}
|
||||
if (next.matches(index + charCount, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
index--
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get()= char.toString()
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
if (ignoreCase) {
|
||||
return super.first(set)
|
||||
}
|
||||
return when (set) {
|
||||
is CharSet -> set.char == char
|
||||
is RangeSet -> set.accepts(0, char.toString()) > 0
|
||||
is SupplementaryCharSet -> false
|
||||
is SupplementaryRangeSet -> set.contains(char)
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
*
|
||||
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
|
||||
*
|
||||
* COPYRIGHT AND PERMISSION NOTICE
|
||||
*
|
||||
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
|
||||
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
|
||||
* hereby granted, free of charge, to any person obtaining a copy of the
|
||||
* Unicode data files and any associated documentation (the "Data Files")
|
||||
* or Unicode software and any associated documentation (the "Software")
|
||||
* to deal in the Data Files or Software without restriction, including without
|
||||
* limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
* and/or sell copies of the Data Files or Software, and to permit persons
|
||||
* to whom the Data Files or Software are furnished to do so, provided that
|
||||
* (a) the above copyright notice(s) and this permission notice appear with
|
||||
* all copies of the Data Files or Software, (b) both the above copyright
|
||||
* notice(s) and this permission notice appear in associated documentation,
|
||||
* and (c) there is clear notice in each modified Data File or in the Software
|
||||
* as well as in the documentation associated with the Data File(s) or Software
|
||||
* that the data or software has been modified.
|
||||
|
||||
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
|
||||
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
|
||||
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
|
||||
*
|
||||
* Except as contained in this notice, the name of a copyright holder shall
|
||||
* not be used in advertising or otherwise to promote the sale, use or other
|
||||
* dealings in these Data Files or Software without prior written
|
||||
* authorization of the copyright holder.
|
||||
*
|
||||
* 2. Additional terms from the Database:
|
||||
*
|
||||
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
|
||||
*
|
||||
* Disclaimer
|
||||
*
|
||||
* The Unicode Character Database is provided as is by Unicode, Inc.
|
||||
* No claims are made as to fitness for any particular purpose. No warranties
|
||||
* of any kind are expressed or implied. The recipient agrees to determine
|
||||
* applicability of information provided. If this file has been purchased
|
||||
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
|
||||
* will be exchange of defective media within 90 days of receipt. This disclaimer
|
||||
* is applicable for all other data files accompanying the Unicode Character Database,
|
||||
* some of which have been compiled by the Unicode Consortium, and some of which
|
||||
* have been supplied by other sources.
|
||||
*
|
||||
* Limitations on Rights to Redistribute This Data
|
||||
*
|
||||
* Recipient is granted the right to make copies in any form for internal
|
||||
* distribution and to freely use the information supplied in the creation of
|
||||
* products supporting the UnicodeTM Standard. The files in
|
||||
* the Unicode Character Database can be redistributed to third parties or other
|
||||
* organizations (whether for profit or not) as long as this notice and the disclaimer
|
||||
* notice are retained. Information can be extracted from these files and used
|
||||
* in documentation or programs, as long as there is an accompanying notice
|
||||
* indicating the source.
|
||||
*/
|
||||
|
||||
package kotlin.text.regex
|
||||
|
||||
/**
|
||||
* This class is used to split the range that contains surrogate characters into two ranges:
|
||||
* the first consisting of these surrogate characters and the second consisting of all others characters
|
||||
* from the parent range. This class represents the parent range split in such a manner.
|
||||
*/
|
||||
internal class CompositeRangeSet(/* range without surrogates */ val withoutSurrogates: AbstractSet,
|
||||
/* range containing surrogates only */ val surrogates: AbstractSet) : SimpleSet() {
|
||||
|
||||
override var next: AbstractSet = dummyNext
|
||||
get() = field
|
||||
set(next) {
|
||||
field = next
|
||||
surrogates.next = next
|
||||
withoutSurrogates.next = next
|
||||
}
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var result = withoutSurrogates.matches(startIndex, testString, matchResult)
|
||||
if (result < 0) {
|
||||
result = surrogates.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "CompositeRangeSet: " + " <nonsurrogate> " + withoutSurrogates + " <surrogate> " + surrogates
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/** Represents canonical decomposition of Unicode character. Is used when CANON_EQ flag of Pattern class is specified. */
|
||||
open internal class DecomposedCharSet(
|
||||
/** Decomposition of the Unicode codepoint */
|
||||
private val decomposedChar: IntArray,
|
||||
/** Length of useful part of decomposedChar decomposedCharLength <= decomposedChar.length */
|
||||
private val decomposedCharLength: Int) : SimpleSet() {
|
||||
|
||||
/** Contains information about number of chars that were read for a codepoint last time */
|
||||
private var readCharsForCodePoint = 1
|
||||
|
||||
/** UTF-16 encoding of decomposedChar */
|
||||
private val decomposedCharUTF16: String by lazy {
|
||||
val strBuff = StringBuilder()
|
||||
|
||||
for (i in 0..decomposedCharLength - 1) {
|
||||
strBuff.append(Char.toChars(decomposedChar[i]))
|
||||
}
|
||||
return@lazy strBuff.toString()
|
||||
}
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var strIndex = startIndex
|
||||
val rightBound = testString.length
|
||||
|
||||
if (strIndex >= rightBound) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// We read testString and decompose it gradually to compare with this decomposedChar at position strIndex
|
||||
var curChar = codePointAt(strIndex, testString, rightBound)
|
||||
strIndex += readCharsForCodePoint
|
||||
var readCodePoints = 0
|
||||
var i = 0
|
||||
// All decompositions have length that is less or equal Lexer.MAX_DECOMPOSITION_LENGTH
|
||||
var decomposedCodePoint: IntArray = IntArray(Lexer.MAX_DECOMPOSITION_LENGTH)
|
||||
readCodePoints += decomposeCodePoint(curChar, decomposedCodePoint, readCodePoints)
|
||||
|
||||
if (strIndex < rightBound) {
|
||||
curChar = codePointAt(strIndex, testString, rightBound)
|
||||
|
||||
// Read testString until we met a decomposed char boundary and decompose obtained portion of testString.
|
||||
while (readCodePoints < Lexer.MAX_DECOMPOSITION_LENGTH && !Lexer.isDecomposedCharBoundary(curChar)) {
|
||||
|
||||
if (!Lexer.hasDecompositionNonNullCanClass(curChar)) {
|
||||
decomposedCodePoint[readCodePoints++] = curChar
|
||||
} else {
|
||||
/*
|
||||
* A few codepoints have decompositions and non null canonical classes, we have to take them into
|
||||
* consideration, but general rule is: if canonical class != 0 then no decomposition
|
||||
*/
|
||||
readCodePoints += decomposeCodePoint(curChar, decomposedCodePoint, readCodePoints)
|
||||
}
|
||||
|
||||
strIndex += readCharsForCodePoint
|
||||
|
||||
if (strIndex < rightBound) {
|
||||
curChar = codePointAt(strIndex, testString, rightBound)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some optimization since length of decomposed char is <= 3 usually
|
||||
when (readCodePoints) {
|
||||
0, 1, 2 -> {}
|
||||
|
||||
3 -> {
|
||||
var i1 = Lexer.getCanonicalClass(decomposedCodePoint[1])
|
||||
val i2 = Lexer.getCanonicalClass(decomposedCodePoint[2])
|
||||
|
||||
if (i2 != 0 && i1 > i2) {
|
||||
i1 = decomposedCodePoint[1]
|
||||
decomposedCodePoint[1] = decomposedCodePoint[2]
|
||||
decomposedCodePoint[2] = i1
|
||||
}
|
||||
}
|
||||
|
||||
else -> decomposedCodePoint = Lexer.getCanonicalOrder(decomposedCodePoint, readCodePoints)
|
||||
}
|
||||
|
||||
// Compare decomposedChar with decomposed char that was just read from testString
|
||||
if (readCodePoints != decomposedCharLength) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if ((0 until readCodePoints).firstOrNull { decomposedCodePoint[i] != decomposedChar[i] } != null) {
|
||||
return -1
|
||||
}
|
||||
return next.matches(strIndex, testString, matchResult)
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "decomposed char: $decomposedChar"
|
||||
|
||||
/** Reads Unicode codepoint from [testString] starting from [strIndex] until [rightBound]. */
|
||||
fun codePointAt(strIndex: Int, testString: CharSequence, rightBound: Int): Int {
|
||||
var index = strIndex
|
||||
|
||||
// We store information about number of codepoints we read at variable readCharsForCodePoint.
|
||||
val curChar: Int
|
||||
readCharsForCodePoint = 1
|
||||
if (index < rightBound - 1) {
|
||||
val high = testString[index++]
|
||||
val low = testString[index]
|
||||
|
||||
if (Char.isSurrogatePair(high, low)) {
|
||||
curChar = Char.toCodePoint(high, low)
|
||||
readCharsForCodePoint = 2
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
curChar = high.toInt()
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
curChar = testString[index].toInt()
|
||||
}
|
||||
|
||||
return curChar
|
||||
}
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
return if (set is DecomposedCharSet)
|
||||
set.decomposedChar.contentEquals(decomposedChar)
|
||||
else
|
||||
true
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Special node for ".*" construction.
|
||||
* The main idea here is to find line terminator and try to find the rest of the construction from this point.
|
||||
*/
|
||||
// TODO: Add optimized implementation for '.+' case
|
||||
internal class DotQuantifierSet(
|
||||
innerSet: AbstractSet,
|
||||
next: AbstractSet,
|
||||
type: Int,
|
||||
val lineTerminator: AbstractLineTerminator,
|
||||
val matchLineTerminator: Boolean = false
|
||||
) : QuantifierSet(innerSet, next, type) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val rightBound = testString.length
|
||||
val startSearch = if (matchLineTerminator) rightBound else testString.findLineTerminator(startIndex, rightBound)
|
||||
|
||||
if (startSearch <= startIndex) {
|
||||
return if (type.toChar() == '+') {
|
||||
-1
|
||||
} else {
|
||||
next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
}
|
||||
val result = next.findBack(startIndex, startSearch, testString, matchResult)
|
||||
if (type.toChar() == '+' && result == startIndex) {
|
||||
return -1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val rightBound = testString.length
|
||||
if (matchLineTerminator) {
|
||||
val foundIndex = next.findBack(startIndex, rightBound, testString, matchResult)
|
||||
if (foundIndex >= 0 && !(type.toChar() == '+' && foundIndex == startIndex)) {
|
||||
return startIndex
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
} else {
|
||||
// 1. find first occurrence of the searched pattern.
|
||||
var nextFound = next.find(startIndex, testString, matchResult)
|
||||
if (nextFound < 0) {
|
||||
return -1
|
||||
}
|
||||
|
||||
// 2. Check if we have other occurrences till the end of line (because .* is greedy and we need the last one).
|
||||
val nextFoundLast = next.findBack(nextFound,
|
||||
testString.findLineTerminator(nextFound, rightBound),
|
||||
testString, matchResult)
|
||||
nextFound = maxOf(nextFound, nextFoundLast)
|
||||
|
||||
// 3. Find the left boundary of this search.
|
||||
val leftBound = findBackLineTerminator(startIndex, nextFound, testString)
|
||||
if (type.toChar() == '+' && leftBound + 1 == nextFound) {
|
||||
return -1
|
||||
}
|
||||
return leftBound + 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first line terminator between [from] (inclusive) and [to] (exclusive) indices.
|
||||
* Returns [to] if no terminator found.
|
||||
*/
|
||||
private fun CharSequence.findLineTerminator(from: Int, to: Int): Int =
|
||||
(from until to).firstOrNull { lineTerminator.isLineTerminator(this[it]) } ?: to
|
||||
|
||||
/**
|
||||
* Find the first line terminator between [from] (inclusive) and [to] (exclusive) indices.
|
||||
* Returns [from - 1] if no terminator found.
|
||||
*/
|
||||
private fun findBackLineTerminator(from: Int, to: Int, testString: CharSequence): Int =
|
||||
(from until to).lastOrNull { lineTerminator.isLineTerminator(testString[it]) } ?: from - 1
|
||||
|
||||
override val name: String
|
||||
get() = ".*"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Node accepting any character except line terminators.
|
||||
*/
|
||||
internal class DotSet(val lt: AbstractLineTerminator, val matchLineTerminator: Boolean)
|
||||
: SimpleSet(AbstractSet.TYPE_DOTSET) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val rightBound = testString.length
|
||||
if (startIndex >= rightBound) {
|
||||
return -1
|
||||
}
|
||||
|
||||
val high = testString[startIndex]
|
||||
if (high.isHighSurrogate() && startIndex + 2 <= rightBound) {
|
||||
val low = testString[startIndex + 1]
|
||||
if (Char.isSurrogatePair(high, low)) {
|
||||
if (!matchLineTerminator && lt.isLineTerminator(Char.toCodePoint(high, low))) {
|
||||
return -1
|
||||
} else {
|
||||
return next.matches(startIndex + 2, testString, matchResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!matchLineTerminator && lt.isLineTerminator(high)) {
|
||||
return -1
|
||||
} else {
|
||||
return next.matches(startIndex + 1, testString, matchResult)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
|
||||
override val name: String
|
||||
get() = "."
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Represents end of input '\z', i.e. matches only character after the last one;
|
||||
*/
|
||||
internal class EOISet : SimpleSet() {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
if (startIndex < testString.length) {
|
||||
return -1
|
||||
}
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
|
||||
override val name: String
|
||||
get() = "EOI"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Represents a node for a '$' sign.
|
||||
* Note: In Kotlin we use only the "anchoring bounds" mode when "$" matches the end of a match region.
|
||||
* See: http://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#useAnchoringBounds-boolean-
|
||||
*/
|
||||
internal class EOLSet(val consCounter: Int, val lt: AbstractLineTerminator, val multiline: Boolean = false) : SimpleSet() {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val rightBound = testString.length
|
||||
val remainingChars = rightBound - startIndex
|
||||
|
||||
when {
|
||||
startIndex >= rightBound ||
|
||||
remainingChars == 1 && lt.isLineTerminator(testString[startIndex]) ||
|
||||
remainingChars == 2 && lt.isLineTerminatorPair(testString[startIndex], testString[startIndex+1]) ||
|
||||
multiline && lt.isLineTerminator(testString[startIndex]) -> {
|
||||
matchResult.setConsumed(consCounter, 0)
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
val result = matchResult.getConsumed(consCounter) != 0
|
||||
matchResult.setConsumed(consCounter, -1)
|
||||
return result
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get()= "<EOL>"
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Valid constant zero character match.
|
||||
*/
|
||||
internal class EmptySet(override var next: AbstractSet) : LeafSet() {
|
||||
|
||||
override val charCount = 0
|
||||
|
||||
override fun accepts(startIndex: Int, testString: CharSequence): Int = 0
|
||||
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
for (index in startIndex..testString.length) {
|
||||
if (index < testString.length) {
|
||||
if (testString[index].isLowSurrogate() &&
|
||||
index > 0 && testString[index - 1].isHighSurrogate()) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (next.matches(index, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
for (index in rightLimit downTo leftLimit) {
|
||||
if (index < testString.length) {
|
||||
if (testString[index].isLowSurrogate() &&
|
||||
index > 0 && testString[index - 1].isHighSurrogate()) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (next.matches(index, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
|
||||
override val name: String
|
||||
get()= "<Empty set>"
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* The node which marks end of the particular group.
|
||||
*/
|
||||
open internal class FSet(val groupIndex: Int) : SimpleSet() {
|
||||
|
||||
var isBackReferenced = false
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val oldEnd = matchResult.getEnd(groupIndex)
|
||||
matchResult.setEnd(groupIndex, startIndex)
|
||||
val shift = next.matches(startIndex, testString, matchResult)
|
||||
if (shift < 0) {
|
||||
matchResult.setEnd(groupIndex, oldEnd)
|
||||
}
|
||||
return shift
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
|
||||
override val name: String
|
||||
get() = "fSet"
|
||||
|
||||
override fun processSecondPass(): FSet {
|
||||
val result = super.processSecondPass()
|
||||
assert(result == this)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the end of the particular group and not take into account possible
|
||||
* kickbacks (required for atomic groups, for instance)
|
||||
*/
|
||||
internal class PossessiveFSet : SimpleSet() {
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
return startIndex
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "possessiveFSet"
|
||||
}
|
||||
|
||||
companion object {
|
||||
val possessiveFSet = PossessiveFSet()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Special construction which marks end of pattern.
|
||||
*/
|
||||
internal class FinalSet : FSet(0) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence,
|
||||
matchResult: MatchResultImpl): Int {
|
||||
if (matchResult.mode == Regex.Mode.FIND || startIndex == testString.length) {
|
||||
matchResult.setEnd(0, startIndex)
|
||||
return startIndex
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "FinalSet"
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-capturing group closing node.
|
||||
*/
|
||||
internal class NonCapFSet(groupIndex: Int) : FSet(groupIndex) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
matchResult.setConsumed(groupIndex, startIndex - matchResult.getConsumed(groupIndex))
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "NonCapFSet"
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* LookAhead FSet, always returns true
|
||||
*/
|
||||
internal class AheadFSet : FSet(-1) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
return startIndex
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "AheadFSet"
|
||||
}
|
||||
|
||||
/**
|
||||
* FSet for lookbehind constructs. Checks if string index saved by corresponding
|
||||
* jointSet in "consumers" equals to current index and return current string
|
||||
* index, return -1 otherwise.
|
||||
|
||||
*/
|
||||
internal class BehindFSet(groupIndex: Int) : FSet(groupIndex) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val rightBound = matchResult.getConsumed(groupIndex)
|
||||
return if (rightBound == startIndex) startIndex else -1
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "BehindFSet"
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an end of an atomic group.
|
||||
*/
|
||||
internal class AtomicFSet(groupIndex: Int) : FSet(groupIndex) {
|
||||
|
||||
var index: Int = 0
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
matchResult.setConsumed(groupIndex, startIndex - matchResult.getConsumed(groupIndex))
|
||||
index = startIndex
|
||||
return startIndex
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "AtomicFSet"
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Default quantifier over groups, in fact this type of quantifier is
|
||||
* generally used for constructions we cant identify number of characters they
|
||||
* consume.
|
||||
|
||||
*/
|
||||
open internal class GroupQuantifierSet(
|
||||
val quantifier: Quantifier,
|
||||
innerSet: AbstractSet,
|
||||
next: AbstractSet,
|
||||
type: Int,
|
||||
val groupQuantifierIndex: Int // It's used to remember a number of the innerSet occurrences during the recursive search.
|
||||
) : QuantifierSet(innerSet, next, type) {
|
||||
|
||||
val max: Int get() = quantifier.max
|
||||
val min: Int get() = quantifier.min
|
||||
|
||||
// We call innerSet.matches here, if it succeeds it call next.matches where next is this QuantifierSet.
|
||||
// So we have a recursive searching procedure.
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var enterCount = matchResult.enterCounters[groupQuantifierIndex]
|
||||
|
||||
fun matchNext(): Int {
|
||||
matchResult.enterCounters[groupQuantifierIndex] = 0
|
||||
val result = next.matches(startIndex, testString, matchResult)
|
||||
matchResult.enterCounters[groupQuantifierIndex] = enterCount
|
||||
return result
|
||||
}
|
||||
|
||||
if (!innerSet.hasConsumed(matchResult)) {
|
||||
return matchNext()
|
||||
}
|
||||
|
||||
// Fast case: '*' or {0, } - no need to count occurrences.
|
||||
if (min == 0 && max == Quantifier.INF) {
|
||||
val nextIndex = innerSet.matches(startIndex, testString, matchResult)
|
||||
return if (nextIndex < 0) {
|
||||
matchNext()
|
||||
} else {
|
||||
nextIndex
|
||||
}
|
||||
}
|
||||
|
||||
// can't go inner set;
|
||||
if (max != Quantifier.INF && enterCount >= max) {
|
||||
return matchNext()
|
||||
}
|
||||
|
||||
// go inner set;
|
||||
matchResult.enterCounters[groupQuantifierIndex] = ++enterCount
|
||||
val nextIndex = innerSet.matches(startIndex, testString, matchResult)
|
||||
|
||||
return if (nextIndex < 0) {
|
||||
matchResult.enterCounters[groupQuantifierIndex] = --enterCount
|
||||
if (enterCount >= min) {
|
||||
matchNext()
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
} else {
|
||||
nextIndex
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = quantifier.toString()
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION") // Char.toInt()
|
||||
package kotlin.text.regex
|
||||
|
||||
import kotlin.text.*
|
||||
|
||||
/**
|
||||
* Represents canonical decomposition of Hangul syllable. Is used when
|
||||
* CANON_EQ flag of Pattern class is specified.
|
||||
*/
|
||||
internal class HangulDecomposedCharSet(
|
||||
/**
|
||||
* Decomposed Hangul syllable.
|
||||
*/
|
||||
private val decomposedChar: CharArray,
|
||||
/**
|
||||
* Length of useful part of decomposedChar
|
||||
* decomposedCharLength <= decomposedChar.length
|
||||
*/
|
||||
private val decomposedCharLength: Int) : SimpleSet() {
|
||||
|
||||
/**
|
||||
* String representing syllable
|
||||
*/
|
||||
private val decomposedCharUTF16: String by lazy {
|
||||
decomposedChar.concatToString(0, decomposedChar.size)
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "decomposed Hangul syllable: $decomposedCharUTF16"
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = startIndex
|
||||
|
||||
/*
|
||||
* All decompositions for Hangul syllables have length that
|
||||
* is less or equal Lexer.MAX_DECOMPOSITION_LENGTH
|
||||
*/
|
||||
val rightBound = testString.length
|
||||
var SyllIndex = 0
|
||||
val decompSyllable = IntArray(Lexer
|
||||
.MAX_HANGUL_DECOMPOSITION_LENGTH)
|
||||
val decompCurSymb: IntArray?
|
||||
var curSymb: Char
|
||||
|
||||
/*
|
||||
* For details about Hangul composition and decomposition see
|
||||
* http://www.unicode.org/versions/Unicode4.0.0/ch03.pdf
|
||||
* "3.12 Conjoining Jamo Behavior"
|
||||
*/
|
||||
var LIndex: Int
|
||||
var VIndex = -1
|
||||
var TIndex = -1
|
||||
|
||||
if (index >= rightBound) {
|
||||
return -1
|
||||
}
|
||||
curSymb = testString[index++]
|
||||
decompCurSymb = Lexer.getHangulDecomposition(curSymb.toInt())
|
||||
|
||||
if (decompCurSymb == null) {
|
||||
|
||||
/*
|
||||
* We deal with ordinary letter or sequence of jamos
|
||||
* at index at testString.
|
||||
*/
|
||||
decompSyllable[SyllIndex++] = curSymb.toInt()
|
||||
LIndex = curSymb.toInt() - Lexer.LBase
|
||||
|
||||
if (LIndex < 0 || LIndex >= Lexer.LCount) {
|
||||
|
||||
/*
|
||||
* Ordinary letter, that doesn't match this
|
||||
*/
|
||||
return -1
|
||||
}
|
||||
|
||||
if (index < rightBound) {
|
||||
curSymb = testString[index]
|
||||
VIndex = curSymb.toInt() - Lexer.VBase
|
||||
}
|
||||
|
||||
if (VIndex < 0 || VIndex >= Lexer.VCount) {
|
||||
|
||||
/*
|
||||
* Single L jamo doesn't compose Hangul syllable,
|
||||
* so doesn't match
|
||||
*/
|
||||
return -1
|
||||
}
|
||||
index++
|
||||
decompSyllable[SyllIndex++] = curSymb.toInt()
|
||||
|
||||
if (index < rightBound) {
|
||||
curSymb = testString[index]
|
||||
TIndex = curSymb.toInt() - Lexer.TBase
|
||||
}
|
||||
|
||||
if (TIndex < 0 || TIndex >= Lexer.TCount) {
|
||||
|
||||
/*
|
||||
* We deal with LV syllable at testString, so
|
||||
* compare it to this
|
||||
*/
|
||||
return if (decomposedCharLength == 2
|
||||
&& decompSyllable[0] == decomposedChar[0].toInt()
|
||||
&& decompSyllable[1] == decomposedChar[1].toInt())
|
||||
next.matches(index, testString, matchResult)
|
||||
else
|
||||
-1
|
||||
}
|
||||
index++
|
||||
decompSyllable[@Suppress("UNUSED_CHANGED_VALUE")SyllIndex++] = curSymb.toInt()
|
||||
|
||||
/*
|
||||
* We deal with LVT syllable at testString, so
|
||||
* compare it to this
|
||||
*/
|
||||
return if (decomposedCharLength == 3
|
||||
&& decompSyllable[0] == decomposedChar[0].toInt()
|
||||
&& decompSyllable[1] == decomposedChar[1].toInt()
|
||||
&& decompSyllable[2] == decomposedChar[2].toInt())
|
||||
next.matches(index, testString, matchResult)
|
||||
else
|
||||
-1
|
||||
} else {
|
||||
|
||||
/*
|
||||
* We deal with Hangul syllable at index at testString.
|
||||
* So we decomposed it to compare with this.
|
||||
*/
|
||||
var i = 0
|
||||
|
||||
if (decompCurSymb.size != decomposedCharLength) {
|
||||
return -1
|
||||
}
|
||||
|
||||
while (i < decomposedCharLength) {
|
||||
if (decompCurSymb[i] != decomposedChar[i].toInt()) {
|
||||
return -1
|
||||
}
|
||||
i++
|
||||
}
|
||||
return next.matches(index, testString, matchResult)
|
||||
}
|
||||
}
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
return if (set is HangulDecomposedCharSet)
|
||||
set.decomposedCharUTF16 == decomposedCharUTF16
|
||||
else
|
||||
true
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Represents group, which is alternation of other subexpression.
|
||||
* One should think about "group" in this model as JointSet opening group and corresponding FSet closing group.
|
||||
*/
|
||||
open internal class JointSet(children: List<AbstractSet>, fSet: FSet) : AbstractSet() {
|
||||
|
||||
protected var children: MutableList<AbstractSet> = mutableListOf<AbstractSet>().apply { addAll(children) }
|
||||
|
||||
var fSet: FSet = fSet
|
||||
protected set
|
||||
|
||||
var groupIndex: Int = fSet.groupIndex
|
||||
protected set
|
||||
|
||||
/**
|
||||
* Returns startIndex+shift, the next position to match
|
||||
*/
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
if (children.isEmpty()) {
|
||||
return -1
|
||||
}
|
||||
val oldStart = matchResult.getStart(groupIndex)
|
||||
matchResult.setStart(groupIndex, startIndex)
|
||||
children.forEach {
|
||||
val shift = it.matches(startIndex, testString, matchResult)
|
||||
if (shift >= 0) {
|
||||
return shift
|
||||
}
|
||||
}
|
||||
matchResult.setStart(groupIndex, oldStart)
|
||||
return -1
|
||||
}
|
||||
|
||||
override var next: AbstractSet
|
||||
get() = fSet.next
|
||||
set(next) {
|
||||
fSet.next = next
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "JointSet"
|
||||
override fun first(set: AbstractSet): Boolean = children.any { it.first(set) }
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
return !(matchResult.getEnd(groupIndex) >= 0 && matchResult.getStart(groupIndex) == matchResult.getEnd(groupIndex))
|
||||
}
|
||||
|
||||
override fun processSecondPassInternal(): AbstractSet {
|
||||
val fSet = this.fSet
|
||||
if (!fSet.secondPassVisited) {
|
||||
val newFSet = fSet.processSecondPass()
|
||||
assert(newFSet == fSet)
|
||||
}
|
||||
|
||||
children.replaceAll { child -> if (!child.secondPassVisited) child.processSecondPass() else child }
|
||||
return super.processSecondPassInternal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
import kotlin.RuntimeException
|
||||
|
||||
/**
|
||||
* Generalized greedy quantifier node over the leaf nodes.
|
||||
* - a{n,m};
|
||||
* - a* == a{0, <inf>};
|
||||
* - a? == a{0, 1};
|
||||
* - a+ == a{1, <inf>};
|
||||
*/
|
||||
open internal class LeafQuantifierSet(var quantifier: Quantifier,
|
||||
innerSet: LeafSet,
|
||||
next: AbstractSet,
|
||||
type: Int
|
||||
) : QuantifierSet(innerSet, next, type) {
|
||||
|
||||
val leaf: LeafSet get() = super.innerSet as LeafSet
|
||||
val min: Int get() = quantifier.min
|
||||
val max: Int get() = quantifier.max
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = startIndex
|
||||
var occurrences = 0
|
||||
|
||||
// Process first <min> occurrences of the sequence being looked for.
|
||||
while (occurrences < min) {
|
||||
if (index + leaf.charCount > testString.length) {
|
||||
return -1
|
||||
}
|
||||
|
||||
val shift = leaf.accepts(index, testString)
|
||||
if (shift < 1) {
|
||||
return -1
|
||||
}
|
||||
index += shift
|
||||
occurrences++
|
||||
}
|
||||
|
||||
// Process occurrences between min and max.
|
||||
while ((max == Quantifier.INF || occurrences < max) && index + leaf.charCount <= testString.length) {
|
||||
val shift = leaf.accepts(index, testString)
|
||||
if (shift < 1) {
|
||||
break
|
||||
}
|
||||
index += shift
|
||||
occurrences++
|
||||
}
|
||||
|
||||
// Roll back if the next node does't match the remaining string.
|
||||
while (occurrences >= min) {
|
||||
val shift = next.matches(index, testString, matchResult)
|
||||
if (shift >= 0) {
|
||||
return shift
|
||||
}
|
||||
index -= leaf.charCount
|
||||
occurrences--
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = quantifier.toString()
|
||||
|
||||
override var innerSet: AbstractSet
|
||||
get() = super.innerSet
|
||||
set(innerSet) {
|
||||
if (innerSet !is LeafSet)
|
||||
throw RuntimeException("Internal Error")
|
||||
super.innerSet = innerSet
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Base class for nodes representing leaf tokens of the RE, those who consumes fixed number of characters.
|
||||
*/
|
||||
internal abstract class LeafSet : SimpleSet(AbstractSet.TYPE_LEAF) {
|
||||
|
||||
open val charCount = 1
|
||||
|
||||
/** Returns "shift", the number of accepted chars. Commonly internal function, but called by quantifiers. */
|
||||
abstract fun accepts(startIndex: Int, testString: CharSequence): Int
|
||||
|
||||
/**
|
||||
* Checks if we can enter this state and pass the control to the next one.
|
||||
* Return positive value if match succeeds, negative otherwise.
|
||||
*/
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
if (startIndex + charCount > testString.length) {
|
||||
return -1
|
||||
}
|
||||
|
||||
val shift = accepts(startIndex, testString) // TODO: may be move the check above in accept function.
|
||||
if (shift < 0) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return next.matches(startIndex + shift, testString, matchResult)
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Positive lookahead node.
|
||||
*/
|
||||
internal class PositiveLookAheadSet(children: List<AbstractSet>, fSet: FSet) : LookAroundSet(children, fSet) {
|
||||
|
||||
/** Returns startIndex+shift, the next position to match */
|
||||
override fun tryToMatch(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
children.forEach {
|
||||
val shift = it.matches(startIndex, testString, matchResult)
|
||||
if (shift >= 0) {
|
||||
// PosLookaheadFset always returns true, position remains the same next.match() from;
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "PositiveLookaheadJointSet"
|
||||
}
|
||||
|
||||
/**
|
||||
* Negative look ahead node.
|
||||
*/
|
||||
internal class NegativeLookAheadSet(children: List<AbstractSet>, fSet: FSet) : LookAroundSet(children, fSet) {
|
||||
|
||||
/** Returns startIndex+shift, the next position to match */
|
||||
override fun tryToMatch(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
children.forEach {
|
||||
if (it.matches(startIndex, testString, matchResult) >= 0) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "NegativeLookaheadJointSet"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
|
||||
package kotlin.text.regex
|
||||
|
||||
/**
|
||||
* Abstract class for lookahead and lookbehind nodes.
|
||||
*/
|
||||
internal abstract class LookAroundSet(children: List<AbstractSet>, fSet: FSet) : AtomicJointSet(children, fSet) {
|
||||
protected abstract fun tryToMatch(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int
|
||||
|
||||
/** Returns startIndex+shift, the next position to match */
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
matchResult.saveState()
|
||||
return tryToMatch(startIndex, testString, matchResult).also { if (it < 0) matchResult.rollbackState() }
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Positive lookbehind node.
|
||||
*/
|
||||
internal class PositiveLookBehindSet(children: List<AbstractSet>, fSet: FSet) : LookAroundSet(children, fSet) {
|
||||
|
||||
/** Returns startIndex+shift, the next position to match */
|
||||
override fun tryToMatch(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
matchResult.setConsumed(groupIndex, startIndex)
|
||||
children.forEach {
|
||||
if (it.findBack(0, startIndex, testString, matchResult) >= 0) {
|
||||
matchResult.setConsumed(groupIndex, -1)
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "PositiveBehindJointSet"
|
||||
}
|
||||
|
||||
/**
|
||||
* Negative look behind node.
|
||||
*/
|
||||
internal class NegativeLookBehindSet(children: List<AbstractSet>, fSet: FSet) : LookAroundSet(children, fSet) {
|
||||
|
||||
/** Returns startIndex+shift, the next position to match */
|
||||
override fun tryToMatch(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
matchResult.setConsumed(groupIndex, startIndex)
|
||||
|
||||
children.forEach {
|
||||
val shift = it.findBack(0, startIndex, testString, matchResult)
|
||||
if (shift >= 0) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "NegativeBehindJointSet"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Node representing non-capturing group
|
||||
*/
|
||||
open internal class NonCapturingJointSet(children: List<AbstractSet>, fSet: FSet) : JointSet(children, fSet) {
|
||||
|
||||
/**
|
||||
* Returns startIndex+shift, the next position to match
|
||||
*/
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val start = matchResult.getConsumed(groupIndex)
|
||||
matchResult.setConsumed(groupIndex, startIndex)
|
||||
|
||||
children.forEach {
|
||||
val shift = it.matches(startIndex, testString, matchResult)
|
||||
if (shift >= 0) {
|
||||
return shift
|
||||
}
|
||||
}
|
||||
|
||||
matchResult.setConsumed(groupIndex, start)
|
||||
return -1
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "NonCapturingJointSet"
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean {
|
||||
return matchResult.getConsumed(groupIndex) != 0
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Possessive quantifier set over groups.
|
||||
*/
|
||||
internal class PossessiveGroupQuantifierSet(
|
||||
quantifier: Quantifier,
|
||||
innerSet: AbstractSet,
|
||||
next: AbstractSet,
|
||||
type: Int,
|
||||
setCounter: Int
|
||||
): GroupQuantifierSet(quantifier, innerSet, next, type, setCounter) {
|
||||
|
||||
init {
|
||||
innerSet.next = FSet.possessiveFSet
|
||||
}
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
|
||||
var index = startIndex
|
||||
var nextIndex: Int = innerSet.matches(index, testString, matchResult)
|
||||
var occurrences = 0
|
||||
while (nextIndex > index && (max == Quantifier.INF || occurrences < max)) {
|
||||
occurrences++
|
||||
index = nextIndex
|
||||
nextIndex = innerSet.matches(index, testString, matchResult)
|
||||
}
|
||||
|
||||
if (occurrences < quantifier.min) {
|
||||
return -1
|
||||
} else {
|
||||
return next.matches(index, testString, matchResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Possessive quantifier node over a leaf node.
|
||||
* - a{n,m}+;
|
||||
* - a*+ == a{0, <inf>}+;
|
||||
* - a?+ == a{0, 1}+;
|
||||
* - a++ == a{1, <inf>}+;
|
||||
*/
|
||||
internal class PossessiveLeafQuantifierSet(
|
||||
quant: Quantifier,
|
||||
innerSet: LeafSet,
|
||||
next: AbstractSet, type: Int
|
||||
) : LeafQuantifierSet(quant, innerSet, next, type) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = startIndex
|
||||
var occurrences = 0
|
||||
|
||||
while (occurrences < min) {
|
||||
if (index + leaf.charCount > testString.length) {
|
||||
return -1
|
||||
}
|
||||
|
||||
val shift = leaf.accepts(index, testString)
|
||||
if (shift < 1) {
|
||||
return -1
|
||||
}
|
||||
index += shift
|
||||
occurrences++
|
||||
}
|
||||
|
||||
while ((max == Quantifier.INF || occurrences < max) && index + leaf.charCount <= testString.length) {
|
||||
val shift = leaf.accepts(index, testString)
|
||||
if (shift < 1) {
|
||||
break
|
||||
}
|
||||
index += shift
|
||||
occurrences++
|
||||
}
|
||||
|
||||
return next.matches(index, testString, matchResult)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Node representing previous match (\G).
|
||||
*/
|
||||
internal class PreviousMatchSet : SimpleSet() {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
if (startIndex == matchResult.previousMatch) {
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
|
||||
override val name: String
|
||||
get() = "PreviousMatchSet"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Base class for quantifiers.
|
||||
*/
|
||||
internal abstract class QuantifierSet(open var innerSet: AbstractSet, override var next: AbstractSet, type: Int)
|
||||
: SimpleSet(type) {
|
||||
|
||||
override fun first(set: AbstractSet): Boolean =
|
||||
innerSet.first(set) || next.first(set)
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
|
||||
|
||||
override fun processSecondPassInternal(): AbstractSet {
|
||||
val innerSet = this.innerSet
|
||||
if (innerSet.secondPassVisited) {
|
||||
this.innerSet = innerSet.processSecondPass()
|
||||
}
|
||||
|
||||
return super.processSecondPassInternal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Represents node accepting single character from the given char class.
|
||||
*/
|
||||
open internal class RangeSet(charClass: AbstractCharClass, val ignoreCase: Boolean = false) : LeafSet() {
|
||||
|
||||
val chars: AbstractCharClass = charClass.instance
|
||||
|
||||
override fun accepts(startIndex: Int, testString: CharSequence): Int {
|
||||
if (ignoreCase) {
|
||||
val char = testString[startIndex]
|
||||
return if (chars.contains(char.uppercaseChar()) || chars.contains(char.lowercaseChar())) 1 else -1
|
||||
} else {
|
||||
return if (chars.contains(testString[startIndex])) 1 else -1
|
||||
}
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "range:" + (if (chars.alt) "^ " else " ") + chars.toString()
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
@Suppress("DEPRECATION")
|
||||
return when (set) {
|
||||
is CharSet -> AbstractCharClass.intersects(chars, set.char.toInt())
|
||||
is RangeSet -> AbstractCharClass.intersects(chars, set.chars)
|
||||
is SupplementaryCharSet -> false
|
||||
is SupplementaryRangeSet -> AbstractCharClass.intersects(chars, set.chars)
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Reluctant version of the group quantifier set.
|
||||
*/
|
||||
internal class ReluctantGroupQuantifierSet(
|
||||
quantifier: Quantifier,
|
||||
innerSet: AbstractSet,
|
||||
next: AbstractSet,
|
||||
type: Int,
|
||||
setCounter: Int
|
||||
) : GroupQuantifierSet(quantifier, innerSet, next, type, setCounter) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var enterCount = matchResult.enterCounters[groupQuantifierIndex]
|
||||
|
||||
fun matchNext(): Int {
|
||||
matchResult.enterCounters[groupQuantifierIndex] = 0
|
||||
val result = next.matches(startIndex, testString, matchResult)
|
||||
matchResult.enterCounters[groupQuantifierIndex] = enterCount
|
||||
return result
|
||||
}
|
||||
|
||||
if (!innerSet.hasConsumed(matchResult)) {
|
||||
return matchNext()
|
||||
}
|
||||
|
||||
// Fast case: '*' or {0, } - no need to count occurrences.
|
||||
if (min == 0 && max == Quantifier.INF) {
|
||||
val res = next.matches(startIndex, testString, matchResult)
|
||||
return if (res < 0) {
|
||||
innerSet.matches(startIndex, testString, matchResult)
|
||||
} else {
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
// can't go inner set;
|
||||
if (max != Quantifier.INF && enterCount >= max) {
|
||||
return matchNext()
|
||||
}
|
||||
|
||||
return if (enterCount >= min) {
|
||||
val nextIndex = matchNext()
|
||||
if (nextIndex < 0) {
|
||||
matchResult.enterCounters[groupQuantifierIndex] = ++enterCount
|
||||
innerSet.matches(startIndex, testString, matchResult)
|
||||
} else {
|
||||
nextIndex
|
||||
}
|
||||
} else {
|
||||
matchResult.enterCounters[groupQuantifierIndex] = ++enterCount
|
||||
innerSet.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Reluctant quantifier node over a leaf node.
|
||||
* - a{n,m}?;
|
||||
* - a*? == a{0, <inf>}?;
|
||||
* - a?? == a{0, 1}?;
|
||||
* - a+? == a{1, <inf>}?;
|
||||
*/
|
||||
internal class ReluctantLeafQuantifierSet(
|
||||
quant: Quantifier,
|
||||
innerSet: LeafSet,
|
||||
next: AbstractSet,
|
||||
type: Int
|
||||
) : LeafQuantifierSet(quant, innerSet, next, type) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = startIndex
|
||||
var occurrences = 0
|
||||
|
||||
while (occurrences < min) {
|
||||
if (index + leaf.charCount > testString.length) {
|
||||
return -1
|
||||
}
|
||||
|
||||
val shift = leaf.accepts(index, testString)
|
||||
if (shift < 1) {
|
||||
return -1
|
||||
}
|
||||
index += shift
|
||||
occurrences++
|
||||
}
|
||||
|
||||
do {
|
||||
var shift = next.matches(index, testString, matchResult)
|
||||
if (shift >= 0) {
|
||||
return shift
|
||||
}
|
||||
|
||||
if (index + leaf.charCount <= testString.length) {
|
||||
shift = leaf.accepts(index, testString)
|
||||
index += shift
|
||||
occurrences++
|
||||
}
|
||||
|
||||
} while (shift >= 1 && (max == Quantifier.INF || occurrences <= max))
|
||||
|
||||
return -1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* A node representing a '^' sign.
|
||||
* Note: In Kotlin we use only the "anchoring bounds" mode when "^" matches beginning of a match region.
|
||||
* See: http://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#useAnchoringBounds-boolean-
|
||||
*/
|
||||
internal class SOLSet(val lt: AbstractLineTerminator, val multiline: Boolean = false) : SimpleSet() {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
if (!multiline) {
|
||||
if (startIndex == 0) {
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
} else {
|
||||
if (startIndex != testString.length
|
||||
&& (startIndex == 0
|
||||
|| lt.isAfterLineTerminator(testString[startIndex - 1], testString[startIndex]))) {
|
||||
return next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
|
||||
override val name: String
|
||||
get() = "^"
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* This class represents nodes constructed with character sequences. For
|
||||
* example, lets consider regular expression: ".*word.*". During regular
|
||||
* expression compilation phase character sequence w-o-r-d, will be represented
|
||||
* with single node for the entire word.
|
||||
*/
|
||||
open internal class SequenceSet(substring: CharSequence, val ignoreCase: Boolean = false) : LeafSet() {
|
||||
|
||||
/** Represents a character sequence used for matching/searching. */
|
||||
protected val patternString: String = substring.toString()
|
||||
|
||||
override val name: String= "sequence: " + patternString
|
||||
|
||||
override val charCount = substring.length
|
||||
|
||||
// Overrides =======================================================================================================
|
||||
|
||||
/** Returns true if [index] points to a low surrogate following a high surrogate */
|
||||
private fun isLowSurrogateOfSupplement(string: CharSequence, index: Int): Boolean =
|
||||
index < string.length && string[index].isLowSurrogate() && index > 0 && string[index - 1].isHighSurrogate()
|
||||
|
||||
override fun accepts(startIndex: Int, testString: CharSequence): Int {
|
||||
return if (testString.startsWith(patternString, startIndex, ignoreCase)
|
||||
&& !isLowSurrogateOfSupplement(testString, startIndex)
|
||||
&& !isLowSurrogateOfSupplement(testString, startIndex + patternString.length)) {
|
||||
charCount
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = startIndex
|
||||
while (index < testString.length) {
|
||||
index = testString.indexOf(patternString, index, ignoreCase)
|
||||
if (index < 0) {
|
||||
return -1
|
||||
}
|
||||
// Check if we have a supplementary code point at the beginning or at the end of the string.
|
||||
if (!isLowSurrogateOfSupplement(testString, index)
|
||||
&& !isLowSurrogateOfSupplement(testString, index + patternString.length)
|
||||
&& next.matches(index + charCount, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
index++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = rightLimit
|
||||
while (index >= leftLimit) {
|
||||
index = testString.lastIndexOf(patternString, index, ignoreCase)
|
||||
if (index < 0 || index < leftLimit) {
|
||||
return -1
|
||||
}
|
||||
// Check if we have a supplementary code point at the beginning or at the end of the string.
|
||||
if (!isLowSurrogateOfSupplement(testString, index)
|
||||
&& !isLowSurrogateOfSupplement(testString, index + patternString.length)
|
||||
&& next.matches(index + charCount, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
index--
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
if (ignoreCase) {
|
||||
return super.first(set)
|
||||
}
|
||||
return when (set) {
|
||||
is CharSet -> set.char == patternString[0]
|
||||
is RangeSet -> set.accepts(0, patternString.substring(0, 1)) > 0
|
||||
is SupplementaryRangeSet -> set.contains(patternString[0]) || patternString.length > 1 && set.contains(Char.toCodePoint(patternString[0], patternString[1]))
|
||||
is SupplementaryCharSet -> if (patternString.length > 1) set.codePoint == Char.toCodePoint(patternString[0], patternString[1])
|
||||
else false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Group node over subexpression without alternations.
|
||||
*/
|
||||
open internal class SingleSet(var kid: AbstractSet, fSet: FSet) : JointSet(listOf(), fSet) {
|
||||
|
||||
var backReferencedSet: BackReferencedSingleSet? = null
|
||||
|
||||
// Overrides (API) =================================================================================================
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val start = matchResult.getStart(groupIndex)
|
||||
matchResult.setStart(groupIndex, startIndex)
|
||||
val shift = kid.matches(startIndex, testString, matchResult)
|
||||
if (shift >= 0) {
|
||||
return shift
|
||||
}
|
||||
matchResult.setStart(groupIndex, start)
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val res = kid.find(startIndex, testString, matchResult)
|
||||
if (res >= 0)
|
||||
matchResult.setStart(groupIndex, res)
|
||||
return res
|
||||
}
|
||||
|
||||
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val res = kid.findBack(leftLimit, rightLimit, testString, matchResult)
|
||||
if (res >= 0)
|
||||
matchResult.setStart(groupIndex, res)
|
||||
return res
|
||||
}
|
||||
|
||||
override fun first(set: AbstractSet): Boolean = kid.first(set)
|
||||
|
||||
// Second pass processing ==========================================================================================
|
||||
|
||||
override fun processBackRefReplacement(): JointSet? {
|
||||
/**
|
||||
* We will store a reference to created BackReferencedSingleSet
|
||||
* in [backReferencedSet] field. This is needed to process replacement
|
||||
* of sets correctly since sometimes we cannot renew all references to
|
||||
* detachable set in the current point of traverse. See
|
||||
* QuantifierSet and AbstractSet processSecondPass() methods for
|
||||
* more details.
|
||||
*/
|
||||
val result = BackReferencedSingleSet(this)
|
||||
backReferencedSet = result
|
||||
return result
|
||||
}
|
||||
|
||||
override fun processSecondPassInternal(): AbstractSet {
|
||||
fSet = fSet.processSecondPass()
|
||||
kid = kid.processSecondPass()
|
||||
return processBackRefReplacement() ?: this
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used for traversing nodes after the first stage of compilation.
|
||||
*/
|
||||
override fun processSecondPass(): AbstractSet {
|
||||
if (secondPassVisited) {
|
||||
if (fSet.isBackReferenced) {
|
||||
assert(backReferencedSet != null) // secondPassVisited
|
||||
return backReferencedSet!!
|
||||
}
|
||||
}
|
||||
secondPassVisited = true
|
||||
return processSecondPassInternal()
|
||||
}
|
||||
|
||||
// Backreferenced version of the class =============================================================================
|
||||
|
||||
/**
|
||||
* Group node over subexpression without alternations.
|
||||
* This node is used if current group is referenced via a backreference.
|
||||
*/
|
||||
internal class BackReferencedSingleSet(node: SingleSet) : SingleSet(node.kid, node.fSet) {
|
||||
|
||||
/*
|
||||
* This class is needed only for overwriting find() and findBack() methods of SingleSet class, which is being
|
||||
* back referenced. The following example explains the need for such substitution:
|
||||
*
|
||||
* Let's consider the pattern ".*(.)\\1".
|
||||
* Leading .* works as follows: finds line terminator and runs findBack from that point.
|
||||
* `findBack` method in its turn (in contrast to matches) sets group boundaries on the back trace.
|
||||
* Thus at the point we try to match back reference(\\1) groups are not yet set.
|
||||
*
|
||||
* To fix this problem we replace backreferenced groups with instances of this class,
|
||||
* which will use matches instead of find; this will affect performance, but ensure correctness of the match.
|
||||
*/
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
for (index in startIndex..testString.length) {
|
||||
val oldStart = matchResult.getStart(groupIndex)
|
||||
matchResult.setStart(groupIndex, index)
|
||||
|
||||
val res = kid.matches(index, testString, matchResult)
|
||||
if (res >= 0) {
|
||||
return index
|
||||
} else {
|
||||
matchResult.setStart(groupIndex, oldStart)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun findBack(leftLimit: Int, rightLimit: Int,
|
||||
testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
for (index in rightLimit downTo leftLimit) {
|
||||
val oldStart = matchResult.getStart(groupIndex)
|
||||
matchResult.setStart(groupIndex, index)
|
||||
|
||||
val res = kid.matches(index, testString, matchResult)
|
||||
if (res >= 0) {
|
||||
return index
|
||||
} else {
|
||||
matchResult.setStart(groupIndex, oldStart)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun processBackRefReplacement(): JointSet? = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
*
|
||||
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
|
||||
*
|
||||
* COPYRIGHT AND PERMISSION NOTICE
|
||||
*
|
||||
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
|
||||
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
|
||||
* hereby granted, free of charge, to any person obtaining a copy of the
|
||||
* Unicode data files and any associated documentation (the "Data Files")
|
||||
* or Unicode software and any associated documentation (the "Software")
|
||||
* to deal in the Data Files or Software without restriction, including without
|
||||
* limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
* and/or sell copies of the Data Files or Software, and to permit persons
|
||||
* to whom the Data Files or Software are furnished to do so, provided that
|
||||
* (a) the above copyright notice(s) and this permission notice appear with
|
||||
* all copies of the Data Files or Software, (b) both the above copyright
|
||||
* notice(s) and this permission notice appear in associated documentation,
|
||||
* and (c) there is clear notice in each modified Data File or in the Software
|
||||
* as well as in the documentation associated with the Data File(s) or Software
|
||||
* that the data or software has been modified.
|
||||
|
||||
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
|
||||
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
|
||||
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
|
||||
*
|
||||
* Except as contained in this notice, the name of a copyright holder shall
|
||||
* not be used in advertising or otherwise to promote the sale, use or other
|
||||
* dealings in these Data Files or Software without prior written
|
||||
* authorization of the copyright holder.
|
||||
*
|
||||
* 2. Additional terms from the Database:
|
||||
*
|
||||
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
|
||||
*
|
||||
* Disclaimer
|
||||
*
|
||||
* The Unicode Character Database is provided as is by Unicode, Inc.
|
||||
* No claims are made as to fitness for any particular purpose. No warranties
|
||||
* of any kind are expressed or implied. The recipient agrees to determine
|
||||
* applicability of information provided. If this file has been purchased
|
||||
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
|
||||
* will be exchange of defective media within 90 days of receipt. This disclaimer
|
||||
* is applicable for all other data files accompanying the Unicode Character Database,
|
||||
* some of which have been compiled by the Unicode Consortium, and some of which
|
||||
* have been supplied by other sources.
|
||||
*
|
||||
* Limitations on Rights to Redistribute This Data
|
||||
*
|
||||
* Recipient is granted the right to make copies in any form for internal
|
||||
* distribution and to freely use the information supplied in the creation of
|
||||
* products supporting the UnicodeTM Standard. The files in
|
||||
* the Unicode Character Database can be redistributed to third parties or other
|
||||
* organizations (whether for profit or not) as long as this notice and the disclaimer
|
||||
* notice are retained. Information can be extracted from these files and used
|
||||
* in documentation or programs, as long as there is an accompanying notice
|
||||
* indicating the source.
|
||||
*/
|
||||
|
||||
package kotlin.text.regex
|
||||
|
||||
/**
|
||||
* Represents node accepting single supplementary codepoint.
|
||||
*/
|
||||
internal class SupplementaryCharSet(val codePoint: Int, ignoreCase: Boolean)
|
||||
: SequenceSet(Char.toChars(codePoint).concatToString(0, 2), ignoreCase) {
|
||||
|
||||
override val name: String
|
||||
get() = patternString
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
return when (set) {
|
||||
is SupplementaryCharSet -> set.codePoint == codePoint
|
||||
is SupplementaryRangeSet -> set.contains(codePoint)
|
||||
is CharSet,
|
||||
is RangeSet -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
*
|
||||
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
|
||||
*
|
||||
* COPYRIGHT AND PERMISSION NOTICE
|
||||
*
|
||||
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
|
||||
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
|
||||
* hereby granted, free of charge, to any person obtaining a copy of the
|
||||
* Unicode data files and any associated documentation (the "Data Files")
|
||||
* or Unicode software and any associated documentation (the "Software")
|
||||
* to deal in the Data Files or Software without restriction, including without
|
||||
* limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
* and/or sell copies of the Data Files or Software, and to permit persons
|
||||
* to whom the Data Files or Software are furnished to do so, provided that
|
||||
* (a) the above copyright notice(s) and this permission notice appear with
|
||||
* all copies of the Data Files or Software, (b) both the above copyright
|
||||
* notice(s) and this permission notice appear in associated documentation,
|
||||
* and (c) there is clear notice in each modified Data File or in the Software
|
||||
* as well as in the documentation associated with the Data File(s) or Software
|
||||
* that the data or software has been modified.
|
||||
|
||||
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
|
||||
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
|
||||
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
|
||||
*
|
||||
* Except as contained in this notice, the name of a copyright holder shall
|
||||
* not be used in advertising or otherwise to promote the sale, use or other
|
||||
* dealings in these Data Files or Software without prior written
|
||||
* authorization of the copyright holder.
|
||||
*
|
||||
* 2. Additional terms from the Database:
|
||||
*
|
||||
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
|
||||
*
|
||||
* Disclaimer
|
||||
*
|
||||
* The Unicode Character Database is provided as is by Unicode, Inc.
|
||||
* No claims are made as to fitness for any particular purpose. No warranties
|
||||
* of any kind are expressed or implied. The recipient agrees to determine
|
||||
* applicability of information provided. If this file has been purchased
|
||||
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
|
||||
* will be exchange of defective media within 90 days of receipt. This disclaimer
|
||||
* is applicable for all other data files accompanying the Unicode Character Database,
|
||||
* some of which have been compiled by the Unicode Consortium, and some of which
|
||||
* have been supplied by other sources.
|
||||
*
|
||||
* Limitations on Rights to Redistribute This Data
|
||||
*
|
||||
* Recipient is granted the right to make copies in any form for internal
|
||||
* distribution and to freely use the information supplied in the creation of
|
||||
* products supporting the UnicodeTM Standard. The files in
|
||||
* the Unicode Character Database can be redistributed to third parties or other
|
||||
* organizations (whether for profit or not) as long as this notice and the disclaimer
|
||||
* notice are retained. Information can be extracted from these files and used
|
||||
* in documentation or programs, as long as there is an accompanying notice
|
||||
* indicating the source.
|
||||
*/
|
||||
|
||||
package kotlin.text.regex
|
||||
|
||||
/**
|
||||
* Represents node accepting single character from the given char class.
|
||||
* This character can be supplementary (2 chars needed to represent) or from
|
||||
* basic multilingual pane (1 needed char to represent it).
|
||||
*/
|
||||
open internal class SupplementaryRangeSet(charClass: AbstractCharClass, val ignoreCase: Boolean = false): SimpleSet() {
|
||||
|
||||
val chars = charClass.instance
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val rightBound = testString.length
|
||||
if (startIndex >= rightBound) {
|
||||
return -1
|
||||
}
|
||||
|
||||
var index = startIndex
|
||||
|
||||
val high = testString[index++]
|
||||
if (contains(high)) {
|
||||
val result = next.matches(index, testString, matchResult)
|
||||
if (result >= 0) return result
|
||||
}
|
||||
|
||||
if (index < rightBound) {
|
||||
val low = testString[index++]
|
||||
if (Char.isSurrogatePair(high, low) && contains(Char.toCodePoint(high, low))) {
|
||||
return next.matches(index, testString, matchResult)
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
fun contains(char: Char): Boolean {
|
||||
if (ignoreCase) {
|
||||
return chars.contains(char.uppercaseChar()) || chars.contains(char.lowercaseChar())
|
||||
} else {
|
||||
return chars.contains(char)
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(char: Int): Boolean {
|
||||
return chars.contains(char)
|
||||
}
|
||||
|
||||
override val name: String
|
||||
get() = "range:" + (if (chars.alt) "^ " else " ") + chars.toString()
|
||||
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
@Suppress("DEPRECATION")
|
||||
return when(set) {
|
||||
is SupplementaryCharSet -> AbstractCharClass.intersects(chars, set.codePoint)
|
||||
is CharSet -> AbstractCharClass.intersects(chars, set.char.toInt())
|
||||
is SupplementaryRangeSet -> AbstractCharClass.intersects(chars, set.chars)
|
||||
is RangeSet -> AbstractCharClass.intersects(chars, set.chars)
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
*
|
||||
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
|
||||
*
|
||||
* COPYRIGHT AND PERMISSION NOTICE
|
||||
*
|
||||
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
|
||||
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
|
||||
* hereby granted, free of charge, to any person obtaining a copy of the
|
||||
* Unicode data files and any associated documentation (the "Data Files")
|
||||
* or Unicode software and any associated documentation (the "Software")
|
||||
* to deal in the Data Files or Software without restriction, including without
|
||||
* limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
* and/or sell copies of the Data Files or Software, and to permit persons
|
||||
* to whom the Data Files or Software are furnished to do so, provided that
|
||||
* (a) the above copyright notice(s) and this permission notice appear with
|
||||
* all copies of the Data Files or Software, (b) both the above copyright
|
||||
* notice(s) and this permission notice appear in associated documentation,
|
||||
* and (c) there is clear notice in each modified Data File or in the Software
|
||||
* as well as in the documentation associated with the Data File(s) or Software
|
||||
* that the data or software has been modified.
|
||||
|
||||
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
|
||||
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
|
||||
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
|
||||
*
|
||||
* Except as contained in this notice, the name of a copyright holder shall
|
||||
* not be used in advertising or otherwise to promote the sale, use or other
|
||||
* dealings in these Data Files or Software without prior written
|
||||
* authorization of the copyright holder.
|
||||
*
|
||||
* 2. Additional terms from the Database:
|
||||
*
|
||||
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
|
||||
*
|
||||
* Disclaimer
|
||||
*
|
||||
* The Unicode Character Database is provided as is by Unicode, Inc.
|
||||
* No claims are made as to fitness for any particular purpose. No warranties
|
||||
* of any kind are expressed or implied. The recipient agrees to determine
|
||||
* applicability of information provided. If this file has been purchased
|
||||
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
|
||||
* will be exchange of defective media within 90 days of receipt. This disclaimer
|
||||
* is applicable for all other data files accompanying the Unicode Character Database,
|
||||
* some of which have been compiled by the Unicode Consortium, and some of which
|
||||
* have been supplied by other sources.
|
||||
*
|
||||
* Limitations on Rights to Redistribute This Data
|
||||
*
|
||||
* Recipient is granted the right to make copies in any form for internal
|
||||
* distribution and to freely use the information supplied in the creation of
|
||||
* products supporting the UnicodeTM Standard. The files in
|
||||
* the Unicode Character Database can be redistributed to third parties or other
|
||||
* organizations (whether for profit or not) as long as this notice and the disclaimer
|
||||
* notice are retained. Information can be extracted from these files and used
|
||||
* in documentation or programs, as long as there is an accompanying notice
|
||||
* indicating the source.
|
||||
*/
|
||||
|
||||
package kotlin.text.regex
|
||||
|
||||
/**
|
||||
* This class represents low surrogate character.
|
||||
*
|
||||
* Note that we can use high and low surrogate characters
|
||||
* that don't combine into supplementary code point.
|
||||
* See http://www.unicode.org/reports/tr18/#Supplementary_Characters
|
||||
*/
|
||||
internal class LowSurrogateCharSet(low: Char) : CharSet(low) {
|
||||
|
||||
override fun accepts(startIndex: Int, testString: CharSequence): Int {
|
||||
val result = super.accepts(startIndex, testString)
|
||||
if (result < 0 || testString.isHighSurrogate(startIndex - 1)) {
|
||||
return -1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun CharSequence.isHighSurrogate(index: Int, leftBound: Int = 0, rightBound: Int = length)
|
||||
= (index in leftBound until rightBound && this[index].isHighSurrogate())
|
||||
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = startIndex
|
||||
while (index < testString.length) {
|
||||
index = testString.indexOf(char, index, ignoreCase)
|
||||
if (index < 0) {
|
||||
return -1
|
||||
}
|
||||
if (!testString.isHighSurrogate(index - 1)
|
||||
&& next.matches(index + charCount, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
index++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = rightLimit
|
||||
while (index >= leftLimit) {
|
||||
index = testString.lastIndexOf(char, index, ignoreCase)
|
||||
if (index < 0) {
|
||||
return -1
|
||||
}
|
||||
if (!testString.isHighSurrogate(index - 1, leftLimit, rightLimit)
|
||||
&& next.matches(index + charCount, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
index--
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
return when(set) {
|
||||
is LowSurrogateCharSet -> set.char == this.char
|
||||
is CharSet,
|
||||
is RangeSet,
|
||||
is SupplementaryCharSet,
|
||||
is SupplementaryRangeSet -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
|
||||
}
|
||||
|
||||
/**
|
||||
* This class represents high surrogate character.
|
||||
*/
|
||||
internal class HighSurrogateCharSet(high: Char) : CharSet(high) {
|
||||
|
||||
override fun accepts(startIndex: Int, testString: CharSequence): Int {
|
||||
val result = super.accepts(startIndex, testString)
|
||||
if (result < 0 || testString.isLowSurrogate(startIndex + 1)) {
|
||||
return -1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun CharSequence.isLowSurrogate(index: Int, leftBound: Int = 0, rightBound: Int = length)
|
||||
= (index in leftBound until rightBound && this[index].isLowSurrogate())
|
||||
|
||||
// TODO: We have a similar code here, in LowSurrogateCharSet and in CharSet. Reuse it somehow.
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = startIndex
|
||||
while (index < testString.length) {
|
||||
index = testString.indexOf(char, index, ignoreCase)
|
||||
if (index < 0) {
|
||||
return -1
|
||||
}
|
||||
// Remove params.
|
||||
if (!testString.isLowSurrogate(index + 1)
|
||||
&& next.matches(index + charCount, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
index++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun findBack(leftLimit: Int, rightLimit: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = rightLimit
|
||||
while (index >= leftLimit) {
|
||||
index = testString.lastIndexOf(char, index, ignoreCase)
|
||||
if (index < 0) {
|
||||
return -1
|
||||
}
|
||||
if (!testString.isLowSurrogate(index + 1, leftLimit, rightLimit)
|
||||
&& next.matches(index + charCount, testString, matchResult) >= 0) {
|
||||
return index
|
||||
}
|
||||
index--
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
return when (set) {
|
||||
is HighSurrogateCharSet -> set.char == this.char
|
||||
is CharSet,
|
||||
is RangeSet,
|
||||
is SupplementaryCharSet,
|
||||
is SupplementaryRangeSet -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
*
|
||||
* Portions, Copyright © 1991-2005 Unicode, Inc. The following applies to Unicode.
|
||||
*
|
||||
* COPYRIGHT AND PERMISSION NOTICE
|
||||
*
|
||||
* Copyright © 1991-2005 Unicode, Inc. All rights reserved. Distributed under
|
||||
* the Terms of Use in http://www.unicode.org/copyright.html. Permission is
|
||||
* hereby granted, free of charge, to any person obtaining a copy of the
|
||||
* Unicode data files and any associated documentation (the "Data Files")
|
||||
* or Unicode software and any associated documentation (the "Software")
|
||||
* to deal in the Data Files or Software without restriction, including without
|
||||
* limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
* and/or sell copies of the Data Files or Software, and to permit persons
|
||||
* to whom the Data Files or Software are furnished to do so, provided that
|
||||
* (a) the above copyright notice(s) and this permission notice appear with
|
||||
* all copies of the Data Files or Software, (b) both the above copyright
|
||||
* notice(s) and this permission notice appear in associated documentation,
|
||||
* and (c) there is clear notice in each modified Data File or in the Software
|
||||
* as well as in the documentation associated with the Data File(s) or Software
|
||||
* that the data or software has been modified.
|
||||
|
||||
* THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
* OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
|
||||
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
|
||||
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
* PERFORMANCE OF THE DATA FILES OR SOFTWARE.
|
||||
*
|
||||
* Except as contained in this notice, the name of a copyright holder shall
|
||||
* not be used in advertising or otherwise to promote the sale, use or other
|
||||
* dealings in these Data Files or Software without prior written
|
||||
* authorization of the copyright holder.
|
||||
*
|
||||
* 2. Additional terms from the Database:
|
||||
*
|
||||
* Copyright © 1995-1999 Unicode, Inc. All Rights reserved.
|
||||
*
|
||||
* Disclaimer
|
||||
*
|
||||
* The Unicode Character Database is provided as is by Unicode, Inc.
|
||||
* No claims are made as to fitness for any particular purpose. No warranties
|
||||
* of any kind are expressed or implied. The recipient agrees to determine
|
||||
* applicability of information provided. If this file has been purchased
|
||||
* on magnetic or optical media from Unicode, Inc., the sole remedy for any claim
|
||||
* will be exchange of defective media within 90 days of receipt. This disclaimer
|
||||
* is applicable for all other data files accompanying the Unicode Character Database,
|
||||
* some of which have been compiled by the Unicode Consortium, and some of which
|
||||
* have been supplied by other sources.
|
||||
*
|
||||
* Limitations on Rights to Redistribute This Data
|
||||
*
|
||||
* Recipient is granted the right to make copies in any form for internal
|
||||
* distribution and to freely use the information supplied in the creation of
|
||||
* products supporting the UnicodeTM Standard. The files in
|
||||
* the Unicode Character Database can be redistributed to third parties or other
|
||||
* organizations (whether for profit or not) as long as this notice and the disclaimer
|
||||
* notice are retained. Information can be extracted from these files and used
|
||||
* in documentation or programs, as long as there is an accompanying notice
|
||||
* indicating the source.
|
||||
*/
|
||||
|
||||
package kotlin.text.regex
|
||||
|
||||
/*
|
||||
* This class is a range that contains only surrogate characters.
|
||||
*/
|
||||
internal class SurrogateRangeSet(surrChars: AbstractCharClass) : RangeSet(surrChars) {
|
||||
|
||||
override fun accepts(startIndex: Int, testString: CharSequence): Int {
|
||||
val result = super.accepts(startIndex, testString)
|
||||
when {
|
||||
result < 0 ||
|
||||
testString.isHighSurrogate(startIndex - 1) && testString.isLowSurrogate(startIndex) ||
|
||||
testString.isHighSurrogate(startIndex) && testString.isLowSurrogate(startIndex + 1) ->
|
||||
return -1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun CharSequence.isHighSurrogate(index: Int, leftBound: Int = 0, rightBound: Int = length)
|
||||
= (index in leftBound until rightBound && this[index].isHighSurrogate())
|
||||
|
||||
private fun CharSequence.isLowSurrogate(index: Int, leftBound: Int = 0, rightBound: Int = length)
|
||||
= (index in leftBound until rightBound && this[index].isLowSurrogate())
|
||||
|
||||
override fun first(set: AbstractSet): Boolean {
|
||||
return when (set) {
|
||||
is SurrogateRangeSet -> true
|
||||
is CharSet,
|
||||
is RangeSet,
|
||||
is SupplementaryCharSet,
|
||||
is SupplementaryRangeSet -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = true
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Optimized greedy quantifier node ('*') for the case where there is no intersection with
|
||||
* next node and normal quantifiers could be treated as greedy and possessive.
|
||||
*/
|
||||
internal class UnifiedQuantifierSet(quant: LeafQuantifierSet) : LeafQuantifierSet(Quantifier.starQuantifier, quant.leaf, quant.next, quant.type) {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var index = startIndex
|
||||
while (index + leaf.charCount <= testString.length && leaf.accepts(index, testString) > 0) {
|
||||
index += leaf.charCount
|
||||
}
|
||||
|
||||
return next.matches(index, testString, matchResult)
|
||||
}
|
||||
|
||||
override fun find(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
var startSearch = next.find(startIndex, testString, matchResult)
|
||||
if (startSearch < 0)
|
||||
return -1
|
||||
|
||||
var result = startSearch
|
||||
var index = startSearch - leaf.charCount
|
||||
while (index >= startIndex && leaf.accepts(index, testString) > 0) {
|
||||
result = index
|
||||
index -= leaf.charCount
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
init {
|
||||
innerSet.next = this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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.regex
|
||||
|
||||
/**
|
||||
* Represents word boundary, checks current character and previous one. If they have different types returns true;
|
||||
*/
|
||||
internal class WordBoundarySet(var positive: Boolean) : SimpleSet() {
|
||||
|
||||
override fun matches(startIndex: Int, testString: CharSequence, matchResult: MatchResultImpl): Int {
|
||||
val curChar = if (startIndex >= testString.length) ' ' else testString[startIndex]
|
||||
val prevChar = if (startIndex == 0) ' ' else testString[startIndex - 1]
|
||||
|
||||
val right = curChar == ' ' || isSpace(curChar, startIndex, testString)
|
||||
val left = prevChar == ' ' || isSpace(prevChar, startIndex - 1, testString)
|
||||
|
||||
return if (left xor right xor positive)
|
||||
-1
|
||||
else
|
||||
next.matches(startIndex, testString, matchResult)
|
||||
}
|
||||
|
||||
/** Returns false, because word boundary does not consumes any characters and do not move string index. */
|
||||
override fun hasConsumed(matchResult: MatchResultImpl): Boolean = false
|
||||
override val name: String
|
||||
get() = "WordBoundarySet"
|
||||
|
||||
private fun isSpace(char: Char, startIndex: Int, testString: CharSequence): Boolean {
|
||||
if (char.isLetterOrDigit() || char == '_') {
|
||||
return false
|
||||
}
|
||||
if (char.category == CharCategory.NON_SPACING_MARK) {
|
||||
var index = startIndex
|
||||
while (--index >= 0) {
|
||||
val ch = testString[index]
|
||||
when {
|
||||
ch.isLetterOrDigit() -> return false
|
||||
char.category != CharCategory.NON_SPACING_MARK -> return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 test.text.harmony_regex
|
||||
|
||||
import kotlin.text.*
|
||||
import kotlin.test.*
|
||||
|
||||
class AllCodePointsTest {
|
||||
|
||||
fun assertTrue(msg: String, value: Boolean) = assertTrue(value, msg)
|
||||
fun assertFalse(msg: String, value: Boolean) = assertFalse(value, msg)
|
||||
|
||||
fun codePointToString(codePoint: Int): String {
|
||||
val charArray = Char.toChars(codePoint)
|
||||
return charArray.concatToString(0, charArray.size)
|
||||
}
|
||||
|
||||
// TODO: Here is a performance problem: an execution of this test requires much more time than it in Kotlin/JVM.
|
||||
@Test fun test() {
|
||||
// Regression for HARMONY-3145
|
||||
var p = Regex("(\\p{all})+")
|
||||
var res = true
|
||||
var cnt = 0
|
||||
var s: String
|
||||
for (i in 0..1114111) {
|
||||
s = codePointToString(i)
|
||||
if (!s.matches(p)) {
|
||||
cnt++
|
||||
res = false
|
||||
}
|
||||
}
|
||||
assertTrue(res)
|
||||
assertEquals(0, cnt)
|
||||
|
||||
p = Regex("(\\P{all})+")
|
||||
res = true
|
||||
cnt = 0
|
||||
|
||||
for (i in 0..1114111) {
|
||||
s = codePointToString(i)
|
||||
if (!s.matches(p)) {
|
||||
cnt++
|
||||
res = false
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(res)
|
||||
assertEquals(0x110000, cnt)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
|
||||
* that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
package test.text.harmony_regex
|
||||
|
||||
import kotlin.text.*
|
||||
import kotlin.test.*
|
||||
|
||||
class FindAllTest {
|
||||
|
||||
internal fun Regex.allGroups(text: String) =
|
||||
findAll(text).map {
|
||||
it.groups.mapIndexed { index, it ->
|
||||
"$index => ${it?.value}"
|
||||
}.joinToString("; ")
|
||||
}.toList()
|
||||
|
||||
/**
|
||||
* Tests regular expressions with lookbehind asserts.
|
||||
*/
|
||||
@Test fun testLookBehind() {
|
||||
var regex: Regex
|
||||
var result: List<String>
|
||||
|
||||
regex = "(?<=^/nl(?:/nl)?/\\d{1,600}[\\d+]{0,600}/[\\d+]{0,600})(\\d+)".toRegex()
|
||||
result = regex.allGroups("/nl/nl/1+2/3+4/")
|
||||
assertEquals(2, result.count())
|
||||
assertEquals("0 => 3; 1 => 3", result[0])
|
||||
assertEquals("0 => 4; 1 => 4", result[1])
|
||||
|
||||
regex = "abe(?<=[ab][!be](.|\\b))(=|t)".toRegex()
|
||||
result = regex.allGroups("abet abe=")
|
||||
assertEquals(2, result.count())
|
||||
assertEquals("0 => abet; 1 => e; 2 => t", result[0])
|
||||
assertEquals("0 => abe=; 1 => ; 2 => =", result[1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests regular expressions with lookahead asserts.
|
||||
*/
|
||||
@Test fun testLookAheadBehind() {
|
||||
var regex: Regex
|
||||
var result: List<String>
|
||||
|
||||
regex = "a(?=b?)(\\w|)c".toRegex()
|
||||
result = regex.allGroups("abcfgac")
|
||||
assertEquals(2, result.count())
|
||||
assertEquals("0 => abc; 1 => b", result[0])
|
||||
assertEquals("0 => ac; 1 => ", result[1])
|
||||
|
||||
regex = "[a!](?=d|&)\\b[&d]".toRegex()
|
||||
result = regex.allGroups("ada& !d!&")
|
||||
assertEquals(2, result.count())
|
||||
assertEquals("0 => a&", result[0])
|
||||
assertEquals("0 => !d", result[1])
|
||||
|
||||
regex = "(?=ab)(a|^)b".toRegex()
|
||||
result = regex.allGroups("abcab")
|
||||
assertEquals(2, result.count())
|
||||
assertEquals("0 => ab; 1 => a", result[0])
|
||||
assertEquals("0 => ab; 1 => a", result[1])
|
||||
|
||||
regex = "(?=[a-k][a-z])(?=[a-d][c-x])[d-y][x-z]".toRegex()
|
||||
result = regex.allGroups("abdydx")
|
||||
assertEquals(1, result.count())
|
||||
assertEquals("0 => dx", result[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 test.text.harmony_regex
|
||||
|
||||
import kotlin.text.*
|
||||
import kotlin.test.*
|
||||
|
||||
class MatchResultTest {
|
||||
|
||||
fun assertTrue(msg: String, value: Boolean) = assertTrue(value, msg)
|
||||
fun assertFalse(msg: String, value: Boolean) = assertFalse(value, msg)
|
||||
|
||||
internal var testPatterns = arrayOf("(a|b)*abb", "(1*2*3*4*)*567", "(a|b|c|d)*aab", "(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)*", "(abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ)*", "(a|b)*(a|b)*A(a|b)*lice.*", "(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)(a|b|c|d|e|f|g|h|" + "i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)*(1|2|3|4|5|6|7|8|9|0)*|while|for|struct|if|do")
|
||||
|
||||
internal var groupPatterns = arrayOf("(a|b)*aabb", "((a)|b)*aabb", "((a|b)*)a(abb)", "(((a)|(b))*)aabb", "(((a)|(b))*)aa(b)b", "(((a)|(b))*)a(a(b)b)")
|
||||
|
||||
@Test fun testReplaceAll() {
|
||||
val input = "aabfooaabfooabfoob"
|
||||
val pattern = "a*b"
|
||||
val regex = Regex(pattern)
|
||||
|
||||
assertEquals("-foo-foo-foo-", regex.replace(input, "-"))
|
||||
}
|
||||
|
||||
@Test fun testReplaceFirst() {
|
||||
val input = "zzzdogzzzdogzzz"
|
||||
val pattern = "dog"
|
||||
val regex = Regex(pattern)
|
||||
|
||||
assertEquals("zzzcatzzzdogzzz", regex.replaceFirst(input, "cat"))
|
||||
}
|
||||
|
||||
/*
|
||||
* Class under test for String group(int)
|
||||
*/
|
||||
@Test fun testGroupint() {
|
||||
val positiveTestString = "ababababbaaabb"
|
||||
|
||||
// test IndexOutOfBoundsException
|
||||
// //
|
||||
for (i in groupPatterns.indices) {
|
||||
val regex = Regex(groupPatterns[i])
|
||||
val result = regex.matchEntire(positiveTestString)!!
|
||||
try {
|
||||
// groupPattern <index + 1> equals to number of groups
|
||||
// of the specified pattern
|
||||
// //
|
||||
result.groups[i + 2]
|
||||
fail("IndexOutBoundsException expected")
|
||||
result.groups[i + 100]
|
||||
fail("IndexOutBoundsException expected")
|
||||
result.groups[-1]
|
||||
fail("IndexOutBoundsException expected")
|
||||
result.groups[-100]
|
||||
fail("IndexOutBoundsException expected")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
}
|
||||
}
|
||||
|
||||
val groupResults = arrayOf(
|
||||
arrayOf("a"),
|
||||
arrayOf("a", "a"),
|
||||
arrayOf("ababababba", "a", "abb"),
|
||||
arrayOf("ababababba", "a", "a", "b"),
|
||||
arrayOf("ababababba", "a", "a", "b", "b"),
|
||||
arrayOf("ababababba", "a", "a", "b", "abb", "b")
|
||||
)
|
||||
|
||||
for (i in groupPatterns.indices) {
|
||||
val regex = Regex(groupPatterns[i])
|
||||
val result = regex.matchEntire(positiveTestString)!!
|
||||
for (j in 0..groupResults[i].size - 1) {
|
||||
assertEquals(groupResults[i][j], result.groupValues[j + 1], "i: $i j: $j")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test fun testGroup() {
|
||||
val positiveTestString = "ababababbaaabb"
|
||||
val negativeTestString = "gjhfgdsjfhgcbv"
|
||||
for (element in groupPatterns) {
|
||||
val regex = Regex(element)
|
||||
val result = regex.matchEntire(positiveTestString)!!
|
||||
assertEquals(positiveTestString, result.groupValues[0])
|
||||
assertEquals(positiveTestString, result.groups[0]!!.value)
|
||||
assertEquals(0 until positiveTestString.length, result.groups[0]!!.range)
|
||||
}
|
||||
|
||||
for (element in groupPatterns) {
|
||||
val regex = Regex(element)
|
||||
val result = regex.matchEntire(negativeTestString)
|
||||
assertEquals(result, null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun testGroupPossessive() {
|
||||
val regex = Regex("((a)|(b))++c")
|
||||
assertEquals("a", regex.matchEntire("aac")!!.groupValues[1])
|
||||
}
|
||||
|
||||
@Test fun testMatchesMisc() {
|
||||
val posSeq = arrayOf(
|
||||
arrayOf("abb", "ababb", "abababbababb", "abababbababbabababbbbbabb"),
|
||||
arrayOf("213567", "12324567", "1234567", "213213567", "21312312312567", "444444567"),
|
||||
arrayOf("abcdaab", "aab", "abaab", "cdaab", "acbdadcbaab"),
|
||||
arrayOf("213234567", "3458", "0987654", "7689546432", "0398576", "98432", "5"),
|
||||
arrayOf("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"),
|
||||
arrayOf("ababbaAabababblice", "ababbaAliceababab", "ababbAabliceaaa", "abbbAbbbliceaaa", "Alice"),
|
||||
arrayOf("a123", "bnxnvgds156", "for", "while", "if", "struct"))
|
||||
|
||||
for (i in testPatterns.indices) {
|
||||
val regex = Regex(testPatterns[i])
|
||||
for (j in 0..posSeq[i].size - 1) {
|
||||
assertTrue("Incorrect match: " + testPatterns[i] + " vs " + posSeq[i][j], regex.matches(posSeq[i][j]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun testMatchesQuantifiers() {
|
||||
val testPatternsSingles = arrayOf("a{5}", "a{2,4}", "a{3,}")
|
||||
val testPatternsMultiple = arrayOf("((a)|(b)){1,2}abb", "((a)|(b)){2,4}", "((a)|(b)){3,}")
|
||||
|
||||
val stringSingles = arrayOf(
|
||||
arrayOf("aaaaa", "aaa"),
|
||||
arrayOf("aa", "a", "aaa", "aaaaaa", "aaaa", "aaaaa"),
|
||||
arrayOf("aaa", "a", "aaaa", "aa")
|
||||
)
|
||||
|
||||
val stringMultiples = arrayOf(
|
||||
arrayOf("ababb", "aba"),
|
||||
arrayOf("ab", "b", "bab", "ababa", "abba", "abababbb"),
|
||||
arrayOf("aba", "b", "abaa", "ba")
|
||||
)
|
||||
|
||||
for (i in testPatternsSingles.indices) {
|
||||
val regex = Regex(testPatternsSingles[i])
|
||||
for (j in 0..stringSingles.size / 2 - 1) {
|
||||
assertTrue("Match expected, but failed: " + regex.pattern + " : " + stringSingles[i][j],
|
||||
regex.matches(stringSingles[i][j * 2])
|
||||
)
|
||||
assertFalse("Match failure expected, but match succeed: " + regex.pattern + " : " + stringSingles[i][j * 2 + 1],
|
||||
regex.matches(stringSingles[i][j * 2 + 1])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (i in testPatternsMultiple.indices) {
|
||||
val regex = Regex(testPatternsMultiple[i])
|
||||
for (j in 0..stringMultiples.size / 2 - 1) {
|
||||
assertTrue("Match expected, but failed: " + regex.pattern + " : " + stringMultiples[i][j],
|
||||
regex.matches(stringMultiples[i][j * 2])
|
||||
)
|
||||
assertFalse("Match failure expected, but match succeed: " + regex.pattern + " : " + stringMultiples[i][j * 2 + 1],
|
||||
regex.matches(stringMultiples[i][j * 2 + 1])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Test for the optimized '.+' quantifier node.
|
||||
assertFalse(Regex(".+abc").matches("abc"))
|
||||
assertFalse(Regex(".+\nabc", RegexOption.DOT_MATCHES_ALL).matches("\nabc"))
|
||||
assertFalse(Regex(".+").matches(""))
|
||||
assertFalse(Regex(".+\n", RegexOption.DOT_MATCHES_ALL).matches("\n"))
|
||||
assertFalse(Regex(".+abc").containsMatchIn("abc"))
|
||||
assertFalse(Regex(".+\nabc", RegexOption.DOT_MATCHES_ALL).containsMatchIn("\nabc"))
|
||||
assertFalse(Regex(".+").containsMatchIn(""))
|
||||
assertFalse(Regex(".+\n", RegexOption.DOT_MATCHES_ALL).containsMatchIn("\n"))
|
||||
|
||||
assertTrue(Regex(".+abc").matches("aabc"))
|
||||
assertTrue(Regex(".+\nabc", RegexOption.DOT_MATCHES_ALL).matches("a\nabc"))
|
||||
assertTrue(Regex(".+").matches("a"))
|
||||
assertTrue(Regex(".+\n", RegexOption.DOT_MATCHES_ALL).matches("a\n"))
|
||||
assertTrue(Regex(".+abc").containsMatchIn("aabc"))
|
||||
assertTrue(Regex(".+\nabc", RegexOption.DOT_MATCHES_ALL).containsMatchIn("a\nabc"))
|
||||
assertTrue(Regex(".+").containsMatchIn("a"))
|
||||
assertTrue(Regex(".+\n", RegexOption.DOT_MATCHES_ALL).containsMatchIn("a\n"))
|
||||
}
|
||||
|
||||
@Test fun testQuantVsGroup() {
|
||||
val patternString = "(d{1,3})((a|c)*)(d{1,3})((a|c)*)(d{1,3})"
|
||||
val testString = "dacaacaacaaddaaacaacaaddd"
|
||||
|
||||
val regex = Regex(patternString)
|
||||
|
||||
val result = regex.matchEntire(testString)!!
|
||||
assertEquals("dacaacaacaaddaaacaacaaddd", result.groupValues[0])
|
||||
assertEquals("d", result.groupValues[1])
|
||||
assertEquals("acaacaacaa", result.groupValues[2])
|
||||
assertEquals("dd", result.groupValues[4])
|
||||
assertEquals("aaacaacaa", result.groupValues[5])
|
||||
assertEquals("ddd", result.groupValues[7])
|
||||
}
|
||||
|
||||
/*
|
||||
* Class under test for boolean find()
|
||||
*/
|
||||
@Test fun testFind() {
|
||||
var testPattern = "(abb)"
|
||||
var testString = "cccabbabbabbabbabb"
|
||||
val regex = Regex(testPattern)
|
||||
var result = regex.find(testString)
|
||||
var start = 3
|
||||
var end = 6
|
||||
while (result != null) {
|
||||
assertEquals(start, result.groups[1]!!.range.start)
|
||||
assertEquals(end, result.groups[1]!!.range.endInclusive + 1)
|
||||
|
||||
start = end
|
||||
end += 3
|
||||
result = result.next()
|
||||
}
|
||||
|
||||
testPattern = "(\\d{1,3})"
|
||||
testString = "aaaa123456789045"
|
||||
|
||||
val regex2 = Regex(testPattern)
|
||||
var result2 = regex2.find(testString)
|
||||
start = 4
|
||||
val length = 3
|
||||
while (result2 != null) {
|
||||
assertEquals(testString.substring(start, start + length), result2.groupValues[1])
|
||||
start += length
|
||||
result2 = result2.next()
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun testSEOLsymbols() {
|
||||
val regex = Regex("^a\\(bb\\[$")
|
||||
assertTrue(regex.matches("a(bb["))
|
||||
}
|
||||
|
||||
@Test fun testGroupCount() {
|
||||
for (i in groupPatterns.indices) {
|
||||
val regex = Regex(groupPatterns[i])
|
||||
val result = regex.matchEntire("ababababbaaabb")!!
|
||||
assertEquals(i + 1, result.groups.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun testReluctantQuantifiers() {
|
||||
val regex = Regex("(ab*)*b")
|
||||
val result = regex.matchEntire("abbbb")
|
||||
if (result != null) {
|
||||
assertEquals("abbb", result.groupValues[1])
|
||||
} else {
|
||||
fail("Match expected: (ab*)*b vs abbbb")
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun testEnhancedFind() {
|
||||
val input = "foob"
|
||||
val pattern = "a*b"
|
||||
val regex = Regex(pattern)
|
||||
val result = regex.find(input)!!
|
||||
assertEquals("b", result.groupValues[0])
|
||||
}
|
||||
|
||||
@Test fun testPosCompositeGroup() {
|
||||
val posExamples = arrayOf("aabbcc", "aacc", "bbaabbcc")
|
||||
val negExamples = arrayOf("aabb", "bb", "bbaabb")
|
||||
val posPat = Regex("(aa|bb){1,3}+cc")
|
||||
val negPat = Regex("(aa|bb){1,3}+bb")
|
||||
|
||||
for (element in posExamples) {
|
||||
assertTrue(posPat.matches(element))
|
||||
}
|
||||
|
||||
for (element in negExamples) {
|
||||
assertFalse(negPat.matches(element))
|
||||
}
|
||||
|
||||
assertTrue(Regex("(aa|bb){1,3}+bb").matches("aabbaabb"))
|
||||
}
|
||||
|
||||
@Test fun testPosAltGroup() {
|
||||
val posExamples = arrayOf("aacc", "bbcc", "cc")
|
||||
val negExamples = arrayOf("bb", "aa")
|
||||
val posPat = Regex("(aa|bb)?+cc")
|
||||
val negPat = Regex("(aa|bb)?+bb")
|
||||
|
||||
for (element in posExamples) {
|
||||
assertTrue(posPat.toString() + " vs: " + element, posPat.matches(element))
|
||||
}
|
||||
|
||||
for (element in negExamples) {
|
||||
assertFalse(negPat.matches(element))
|
||||
}
|
||||
|
||||
assertTrue(Regex("(aa|bb)?+bb").matches("aabb"))
|
||||
}
|
||||
|
||||
@Test fun testRelCompGroup() {
|
||||
var res = ""
|
||||
for (i in 0..3) {
|
||||
val regex = Regex("((aa|bb){$i,3}?).*cc")
|
||||
val result = regex.matchEntire("aaaaaacc")
|
||||
assertTrue(regex.toString() + " vs: " + "aaaaaacc", result != null)
|
||||
assertEquals(res, result!!.groupValues[1])
|
||||
res += "aa"
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun testRelAltGroup() {
|
||||
var regex = Regex("((aa|bb)??).*cc")
|
||||
var result = regex.matchEntire("aacc")
|
||||
assertTrue(regex.toString() + " vs: " + "aacc", result != null)
|
||||
assertEquals("", result!!.groupValues[1])
|
||||
|
||||
regex = Regex("((aa|bb)??)cc")
|
||||
result = regex.matchEntire("aacc")
|
||||
assertTrue(regex.toString() + " vs: " + "aacc", result != null)
|
||||
assertEquals("aa", result!!.groupValues[1])
|
||||
}
|
||||
|
||||
@Test fun testIgnoreCase() {
|
||||
var regex = Regex("(aa|bb)*", RegexOption.IGNORE_CASE)
|
||||
assertTrue(regex.matches("aAbb"))
|
||||
|
||||
regex = Regex("(a|b|c|d|e)*", RegexOption.IGNORE_CASE)
|
||||
assertTrue(regex.matches("aAebbAEaEdebbedEccEdebbedEaedaebEbdCCdbBDcdcdADa"))
|
||||
|
||||
regex = Regex("[a-e]*", RegexOption.IGNORE_CASE)
|
||||
assertTrue(regex.matches("aAebbAEaEdebbedEccEdebbedEaedaebEbdCCdbBDcdcdADa"))
|
||||
}
|
||||
|
||||
@Test fun testQuoteReplacement() {
|
||||
assertEquals("\\\\aaCC\\$1", Regex.escapeReplacement("\\aaCC$1"))
|
||||
}
|
||||
|
||||
@Test fun testOverFlow() {
|
||||
var regex = Regex("(a*)*")
|
||||
var result = regex.matchEntire("aaa")
|
||||
assertTrue(result != null)
|
||||
assertEquals("", result!!.groupValues[1])
|
||||
|
||||
assertTrue(Regex("(1+)\\1+").matches("11"))
|
||||
assertTrue(Regex("(1+)(2*)\\2+").matches("11"))
|
||||
|
||||
regex = Regex("(1+)\\1*")
|
||||
result = regex.matchEntire("11")
|
||||
|
||||
assertTrue(result != null)
|
||||
assertEquals("11", result!!.groupValues[1])
|
||||
|
||||
regex = Regex("((1+)|(2+))(\\2+)")
|
||||
result = regex.matchEntire("11")
|
||||
|
||||
assertTrue(result != null)
|
||||
assertEquals("1", result!!.groupValues[2])
|
||||
assertEquals("1", result.groupValues[1])
|
||||
assertEquals("1", result.groupValues[4])
|
||||
assertEquals("", result.groupValues[3])
|
||||
}
|
||||
|
||||
@Test fun testUnicode() {
|
||||
assertTrue(Regex("\\x61a").matches("aa"))
|
||||
assertTrue(Regex("\\u0061a").matches("aa"))
|
||||
assertTrue(Regex("\\0141a").matches("aa"))
|
||||
assertTrue(Regex("\\0777").matches("?7"))
|
||||
}
|
||||
|
||||
@Test fun testUnicodeCategory() {
|
||||
assertTrue(Regex("\\p{Ll}").matches("k")) // Unicode lower case
|
||||
assertTrue(Regex("\\P{Ll}").matches("K")) // Unicode non-lower
|
||||
// case
|
||||
assertTrue(Regex("\\p{Lu}").matches("K")) // Unicode upper case
|
||||
assertTrue(Regex("\\P{Lu}").matches("k")) // Unicode non-upper
|
||||
// case combinations
|
||||
assertTrue(Regex("[\\p{L}&&[^\\p{Lu}]]").matches("k"))
|
||||
assertTrue(Regex("[\\p{L}&&[^\\p{Ll}]]").matches("K"))
|
||||
assertFalse(Regex("[\\p{L}&&[^\\p{Lu}]]").matches("K"))
|
||||
assertFalse(Regex("[\\p{L}&&[^\\p{Ll}]]").matches("k"))
|
||||
|
||||
// category/character combinations
|
||||
assertFalse(Regex("[\\p{L}&&[^a-z]]").matches("k"))
|
||||
assertTrue(Regex("[\\p{L}&&[^a-z]]").matches("K"))
|
||||
|
||||
assertTrue(Regex("[\\p{Lu}a-z]").matches("k"))
|
||||
assertTrue(Regex("[a-z\\p{Lu}]").matches("k"))
|
||||
|
||||
assertFalse(Regex("[\\p{Lu}a-d]").matches("k"))
|
||||
assertTrue(Regex("[a-d\\p{Lu}]").matches("K"))
|
||||
|
||||
assertFalse(Regex("[\\p{L}&&[^\\p{Lu}&&[^G]]]").matches("K"))
|
||||
|
||||
}
|
||||
|
||||
@Test fun testSplitEmpty() {
|
||||
|
||||
val regex = Regex("")
|
||||
val s = regex.split("", 0)
|
||||
|
||||
assertEquals(2, s.size)
|
||||
assertEquals("", s[0])
|
||||
assertEquals("", s[1])
|
||||
}
|
||||
|
||||
@Test fun testFindDollar() {
|
||||
val regex = Regex("a$")
|
||||
val result = regex.find("a\n")
|
||||
assertTrue(result != null)
|
||||
assertEquals("a", result!!.groupValues[0])
|
||||
}
|
||||
|
||||
/*
|
||||
* Regression test for HARMONY-674
|
||||
*/
|
||||
@Test fun testPatternMatcher() {
|
||||
assertTrue(Regex("(?:\\d+)(?:pt)").matches("14pt"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspired by HARMONY-3360
|
||||
*/
|
||||
@Test fun test3360() {
|
||||
val str = "!\"#%&'(),-./"
|
||||
val regex = Regex("\\s")
|
||||
assertFalse(regex.containsMatchIn(str))
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test for HARMONY-3360
|
||||
*/
|
||||
@Test fun testGeneralPunctuationCategory() {
|
||||
val s = arrayOf(",", "!", "\"", "#", "%", "&", "'", "(", ")", "-", ".", "/")
|
||||
val regexp = "\\p{P}"
|
||||
|
||||
for (i in s.indices) {
|
||||
val regex = Regex(regexp)
|
||||
assertTrue(regex.containsMatchIn(s[i]))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression test for https://github.com/JetBrains/kotlin-native/issues/2297
|
||||
*/
|
||||
@Test fun test2297() {
|
||||
assertTrue(Regex("^(:[0-5]?[0-9])+$").matches(":20:30"))
|
||||
assertTrue(Regex("(.{1,}){2}").matches("aa"))
|
||||
|
||||
assertTrue(Regex("(.+b)+").matches("0b0b"))
|
||||
assertTrue(Regex("(.+?b)+").matches("0b0b"))
|
||||
assertTrue(Regex("(.?b)+").matches("0b0b"))
|
||||
assertTrue(Regex("(.??b)+").matches("0b0b"))
|
||||
assertTrue(Regex("(.*b)+").matches("0b0b"))
|
||||
assertTrue(Regex("(.*?b)+").matches("0b0b"))
|
||||
assertTrue(Regex("(.{1,2}b)+").matches("0b00b"))
|
||||
assertTrue(Regex("(.{1,2}?b)+").matches("0b00b"))
|
||||
|
||||
assertTrue(Regex("([0]?[0]?)+").matches("0000"))
|
||||
assertTrue(Regex("([0]?[0]?b)+").matches("00b00b"))
|
||||
assertTrue(Regex("((b{2}){3})+").matches("bbbbbbbbbbbb"))
|
||||
|
||||
assertTrue(Regex("[^a]").matches("b"))
|
||||
}
|
||||
|
||||
@Test fun kt28158() {
|
||||
val comment = "😃😃😃😃😃😃"
|
||||
val regex = Regex("(.{3,})\\1+", RegexOption.IGNORE_CASE)
|
||||
assertTrue(comment.contains(regex))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 test.text.harmony_regex
|
||||
|
||||
import kotlin.text.*
|
||||
import kotlin.test.*
|
||||
|
||||
class MatchResultTest2 {
|
||||
|
||||
@Test fun testErrorConditions2() {
|
||||
// Test match cursors in absence of a match
|
||||
val regex = Regex("(foo[0-9])(bar[a-z])")
|
||||
var result = regex.find("foo1barzfoo2baryfoozbar5")
|
||||
|
||||
assertTrue(result != null)
|
||||
assertEquals(0, result!!.range.start)
|
||||
assertEquals(7, result.range.endInclusive)
|
||||
assertEquals(0, result.groups[0]!!.range.start)
|
||||
assertEquals(7, result.groups[0]!!.range.endInclusive)
|
||||
assertEquals(0, result.groups[1]!!.range.start)
|
||||
assertEquals(3, result.groups[1]!!.range.endInclusive)
|
||||
assertEquals(4, result.groups[2]!!.range.start)
|
||||
assertEquals(7, result.groups[2]!!.range.endInclusive)
|
||||
|
||||
try {
|
||||
result.groups[3]
|
||||
fail("IndexOutOfBoundsException expected")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
}
|
||||
|
||||
try {
|
||||
result.groupValues[3]
|
||||
fail("IndexOutOfBoundsException expected")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
}
|
||||
|
||||
try {
|
||||
result.groups[-1]
|
||||
fail("IndexOutOfBoundsException expected")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
}
|
||||
|
||||
try {
|
||||
result.groupValues[-1]
|
||||
fail("IndexOutOfBoundsException expected")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
}
|
||||
|
||||
result = result.next()
|
||||
assertTrue(result != null)
|
||||
assertEquals(8, result!!.range.start)
|
||||
assertEquals(15, result.range.endInclusive)
|
||||
assertEquals(8, result.groups[0]!!.range.start)
|
||||
assertEquals(15, result.groups[0]!!.range.endInclusive)
|
||||
assertEquals(8, result.groups[1]!!.range.start)
|
||||
assertEquals(11, result.groups[1]!!.range.endInclusive)
|
||||
assertEquals(12, result.groups[2]!!.range.start)
|
||||
assertEquals(15, result.groups[2]!!.range.endInclusive)
|
||||
|
||||
try {
|
||||
result.groups[3]
|
||||
fail("IndexOutOfBoundsException expected")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
}
|
||||
|
||||
try {
|
||||
result.groupValues[3]
|
||||
fail("IndexOutOfBoundsException expected")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
}
|
||||
|
||||
try {
|
||||
result.groups[-1]
|
||||
fail("IndexOutOfBoundsException expected")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
}
|
||||
|
||||
try {
|
||||
result.groupValues[-1]
|
||||
fail("IndexOutOfBoundsException expected")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
}
|
||||
|
||||
result = result.next()
|
||||
assertFalse(result != null)
|
||||
}
|
||||
|
||||
/*
|
||||
* Regression test for HARMONY-997
|
||||
*/
|
||||
@Test fun testReplacementBackSlash() {
|
||||
val str = "replace me"
|
||||
val replacedString = "me"
|
||||
val substitutionString = "\\"
|
||||
val regex = Regex(replacedString)
|
||||
try {
|
||||
regex.replace(str, substitutionString)
|
||||
fail("IllegalArgumentException should be thrown")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 test.text.harmony_regex
|
||||
|
||||
import kotlin.text.*
|
||||
import kotlin.test.*
|
||||
|
||||
class ModeTest {
|
||||
|
||||
/**
|
||||
* Tests Pattern compilation modes and modes triggered in pattern strings
|
||||
*/
|
||||
@Test fun testCase() {
|
||||
var regex: Regex
|
||||
var result: MatchResult?
|
||||
|
||||
regex = Regex("([a-z]+)[0-9]+")
|
||||
result = regex.find("cAT123#dog345")
|
||||
assertNotNull(result)
|
||||
assertEquals("dog", result!!.groupValues[1])
|
||||
assertNull(result.next())
|
||||
|
||||
regex = Regex("([a-z]+)[0-9]+", RegexOption.IGNORE_CASE)
|
||||
result = regex.find("cAt123#doG345")
|
||||
assertNotNull(result)
|
||||
assertEquals("cAt", result!!.groupValues[1])
|
||||
result = result.next()
|
||||
assertNotNull(result)
|
||||
assertEquals("doG", result!!.groupValues[1])
|
||||
assertNull(result.next())
|
||||
|
||||
regex = Regex("(?i)([a-z]+)[0-9]+")
|
||||
result = regex.find("cAt123#doG345")
|
||||
assertNotNull(result)
|
||||
assertEquals("cAt", result!!.groupValues[1])
|
||||
result = result.next()
|
||||
assertNotNull(result)
|
||||
assertEquals("doG", result!!.groupValues[1])
|
||||
assertNull(result.next())
|
||||
}
|
||||
|
||||
@Test fun testMultiline() {
|
||||
var regex: Regex
|
||||
var result: MatchResult?
|
||||
|
||||
regex = Regex("^foo")
|
||||
result = regex.find("foobar")
|
||||
assertNotNull(result)
|
||||
assertTrue(result!!.range.start == 0 && result.range.endInclusive == 2)
|
||||
assertTrue(result.groups[0]!!.range.start == 0 && result.groups[0]!!.range.endInclusive == 2)
|
||||
assertNull(result.next())
|
||||
|
||||
result = regex.find("barfoo")
|
||||
assertNull(result)
|
||||
|
||||
regex = Regex("foo$")
|
||||
result = regex.find("foobar")
|
||||
assertNull(result)
|
||||
|
||||
result = regex.find("barfoo")
|
||||
assertNotNull(result)
|
||||
assertTrue(result!!.range.start == 3 && result.range.endInclusive == 5)
|
||||
assertTrue(result.groups[0]!!.range.start == 3 && result.groups[0]!!.range.endInclusive == 5)
|
||||
assertNull(result.next())
|
||||
|
||||
regex = Regex("^foo([0-9]*)", RegexOption.MULTILINE)
|
||||
result = regex.find("foo1bar\nfoo2foo3\nbarfoo4")
|
||||
assertNotNull(result)
|
||||
assertEquals("1", result!!.groupValues[1])
|
||||
result = result.next()
|
||||
assertNotNull(result)
|
||||
assertEquals("2", result!!.groupValues[1])
|
||||
assertNull(result.next())
|
||||
|
||||
regex = Regex("foo([0-9]*)$", RegexOption.MULTILINE)
|
||||
result = regex.find("foo1bar\nfoo2foo3\nbarfoo4")
|
||||
assertNotNull(result)
|
||||
assertEquals("3", result!!.groupValues[1])
|
||||
result = result.next()
|
||||
assertNotNull(result)
|
||||
assertEquals("4", result!!.groupValues[1])
|
||||
assertNull(result.next())
|
||||
|
||||
regex = Regex("(?m)^foo([0-9]*)")
|
||||
result = regex.find("foo1bar\nfoo2foo3\nbarfoo4")
|
||||
assertNotNull(result)
|
||||
assertEquals("1", result!!.groupValues[1])
|
||||
result = result.next()
|
||||
assertNotNull(result)
|
||||
assertEquals("2", result!!.groupValues[1])
|
||||
assertNull(result.next())
|
||||
|
||||
regex = Regex("(?m)foo([0-9]*)$")
|
||||
result = regex.find("foo1bar\nfoo2foo3\nbarfoo4")
|
||||
assertNotNull(result)
|
||||
assertEquals("3", result!!.groupValues[1])
|
||||
result = result.next()
|
||||
assertNotNull(result)
|
||||
assertEquals("4", result!!.groupValues[1])
|
||||
assertNull(result.next())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 test.text.harmony_regex
|
||||
|
||||
import kotlin.text.*
|
||||
import kotlin.test.*
|
||||
|
||||
class PatternErrorTest {
|
||||
|
||||
@Test fun testCompileErrors() {
|
||||
// empty regex string - no exception should be thrown
|
||||
Regex("")
|
||||
|
||||
// note: invalid regex syntax checked in PatternSyntaxExceptionTest
|
||||
|
||||
// flags = 0 should raise no exception
|
||||
Regex("foo", emptySet())
|
||||
|
||||
// check that all valid flags accepted without exception
|
||||
val options = setOf(
|
||||
RegexOption.UNIX_LINES,
|
||||
RegexOption.IGNORE_CASE,
|
||||
RegexOption.MULTILINE,
|
||||
RegexOption.CANON_EQ,
|
||||
RegexOption.COMMENTS,
|
||||
RegexOption.DOT_MATCHES_ALL)
|
||||
Regex("foo", options)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 test.text.harmony_regex
|
||||
|
||||
import kotlin.text.*
|
||||
import kotlin.test.*
|
||||
|
||||
class PatternSyntaxExceptionTest {
|
||||
|
||||
@Test fun testCase() {
|
||||
val regex = "("
|
||||
try {
|
||||
Regex(regex)
|
||||
fail("IllegalArgumentException expected")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// TODO: Check the exception's properties.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test fun testCase2() {
|
||||
val regex = "[4-"
|
||||
try {
|
||||
Regex(regex)
|
||||
fail("IllegalArgumentException expected")
|
||||
} catch (e: IllegalArgumentException) {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
/* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 test.text.harmony_regex
|
||||
|
||||
import kotlin.text.*
|
||||
import kotlin.test.*
|
||||
|
||||
class ReplaceTest {
|
||||
|
||||
@Test fun testSimpleReplace() {
|
||||
val target: String
|
||||
val pattern: String
|
||||
val repl: String
|
||||
|
||||
target = "foobarfobarfoofo1"
|
||||
pattern = "fo[^o]"
|
||||
repl = "xxx"
|
||||
|
||||
val regex = Regex(pattern)
|
||||
|
||||
assertEquals("foobarxxxarfoofo1", regex.replaceFirst(target, repl))
|
||||
assertEquals("foobarxxxarfooxxx", regex.replace(target, repl))
|
||||
}
|
||||
|
||||
@Test fun testCaptureReplace() {
|
||||
var target: String
|
||||
var pattern: String
|
||||
var repl: String
|
||||
var s: String
|
||||
var regex: Regex
|
||||
|
||||
target = "[31]foo;bar[42];[99]xyz"
|
||||
pattern = "\\[([0-9]+)\\]([a-z]+)"
|
||||
repl = "$2[$1]"
|
||||
|
||||
regex = Regex(pattern)
|
||||
s = regex.replaceFirst(target, repl)
|
||||
assertEquals("foo[31];bar[42];[99]xyz", s)
|
||||
s = regex.replace(target, repl)
|
||||
assertEquals("foo[31];bar[42];xyz[99]", s)
|
||||
|
||||
target = "[31]foo(42)bar{63}zoo;[12]abc(34)def{56}ghi;{99}xyz[88]xyz(77)xyz;"
|
||||
pattern = "\\[([0-9]+)\\]([a-z]+)\\(([0-9]+)\\)([a-z]+)\\{([0-9]+)\\}([a-z]+)"
|
||||
repl = "[$5]$6($3)$4{$1}$2"
|
||||
regex = Regex(pattern)
|
||||
s = regex.replaceFirst(target, repl)
|
||||
assertEquals("[63]zoo(42)bar{31}foo;[12]abc(34)def{56}ghi;{99}xyz[88]xyz(77)xyz;", s)
|
||||
s = regex.replace(target, repl)
|
||||
assertEquals("[63]zoo(42)bar{31}foo;[56]ghi(34)def{12}abc;{99}xyz[88]xyz(77)xyz;", s)
|
||||
}
|
||||
|
||||
@Test fun testEscapeReplace() {
|
||||
val target: String
|
||||
val pattern: String
|
||||
var repl: String
|
||||
var s: String
|
||||
|
||||
target = "foo'bar''foo"
|
||||
pattern = "'"
|
||||
repl = "\\'"
|
||||
s = target.replace(pattern.toRegex(), repl)
|
||||
assertEquals("foo'bar''foo", s)
|
||||
repl = "\\\\'"
|
||||
s = target.replace(pattern.toRegex(), repl)
|
||||
assertEquals("foo\\'bar\\'\\'foo", s)
|
||||
repl = "\\$3"
|
||||
s = target.replace(pattern.toRegex(), repl)
|
||||
assertEquals("foo$3bar$3$3foo", s)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 test.text.harmony_regex
|
||||
|
||||
import kotlin.text.*
|
||||
import kotlin.test.*
|
||||
|
||||
class SplitTest {
|
||||
|
||||
@Test fun testSimple() {
|
||||
val p = Regex("/")
|
||||
val results = p.split("have/you/done/it/right")
|
||||
val expected = arrayOf("have", "you", "done", "it", "right")
|
||||
assertEquals(expected.size, results.size)
|
||||
for (i in expected.indices) {
|
||||
assertEquals(results[i], expected[i])
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun testSplit1() {
|
||||
var p = Regex(" ")
|
||||
|
||||
val input = "poodle zoo"
|
||||
var tokens: List<String>
|
||||
|
||||
tokens = p.split(input, 1)
|
||||
assertEquals(1, tokens.size)
|
||||
assertTrue(tokens[0] == input)
|
||||
tokens = p.split(input, 2)
|
||||
assertEquals(2, tokens.size)
|
||||
assertEquals("poodle", tokens[0])
|
||||
assertEquals("zoo", tokens[1])
|
||||
tokens = p.split(input, 5)
|
||||
assertEquals(2, tokens.size)
|
||||
assertEquals("poodle", tokens[0])
|
||||
assertEquals("zoo", tokens[1])
|
||||
tokens = p.split(input, 0)
|
||||
assertEquals(2, tokens.size)
|
||||
assertEquals("poodle", tokens[0])
|
||||
assertEquals("zoo", tokens[1])
|
||||
tokens = p.split(input)
|
||||
assertEquals(2, tokens.size)
|
||||
assertEquals("poodle", tokens[0])
|
||||
assertEquals("zoo", tokens[1])
|
||||
|
||||
p = Regex("d")
|
||||
|
||||
tokens = p.split(input, 1)
|
||||
assertEquals(1, tokens.size)
|
||||
assertTrue(tokens[0] == input)
|
||||
tokens = p.split(input, 2)
|
||||
assertEquals(2, tokens.size)
|
||||
assertEquals("poo", tokens[0])
|
||||
assertEquals("le zoo", tokens[1])
|
||||
tokens = p.split(input, 5)
|
||||
assertEquals(2, tokens.size)
|
||||
assertEquals("poo", tokens[0])
|
||||
assertEquals("le zoo", tokens[1])
|
||||
tokens = p.split(input, 0)
|
||||
assertEquals(2, tokens.size)
|
||||
assertEquals("poo", tokens[0])
|
||||
assertEquals("le zoo", tokens[1])
|
||||
tokens = p.split(input)
|
||||
assertEquals(2, tokens.size)
|
||||
assertEquals("poo", tokens[0])
|
||||
assertEquals("le zoo", tokens[1])
|
||||
|
||||
p = Regex("o")
|
||||
|
||||
tokens = p.split(input, 1)
|
||||
assertEquals(1, tokens.size)
|
||||
assertTrue(tokens[0] == input)
|
||||
tokens = p.split(input, 2)
|
||||
assertEquals(2, tokens.size)
|
||||
assertEquals("p", tokens[0])
|
||||
assertEquals("odle zoo", tokens[1])
|
||||
tokens = p.split(input, 5)
|
||||
assertEquals(5, tokens.size)
|
||||
assertEquals("p", tokens[0])
|
||||
assertTrue(tokens[1] == "")
|
||||
assertEquals("dle z", tokens[2])
|
||||
assertTrue(tokens[3] == "")
|
||||
assertTrue(tokens[4] == "")
|
||||
tokens = p.split(input, 0)
|
||||
assertEquals(5, tokens.size)
|
||||
assertEquals("p", tokens[0])
|
||||
assertTrue(tokens[1] == "")
|
||||
assertEquals("dle z", tokens[2])
|
||||
assertTrue(tokens[3] == "")
|
||||
assertTrue(tokens[4] == "")
|
||||
tokens = p.split(input)
|
||||
assertEquals(5, tokens.size)
|
||||
assertEquals("p", tokens[0])
|
||||
assertTrue(tokens[1] == "")
|
||||
assertEquals("dle z", tokens[2])
|
||||
assertTrue(tokens[3] == "")
|
||||
assertTrue(tokens[4] == "")
|
||||
}
|
||||
|
||||
@Test fun testSplit2() {
|
||||
val p = Regex("")
|
||||
var s: List<String>
|
||||
s = p.split("a", 0)
|
||||
assertEquals(3, s.size)
|
||||
assertEquals("", s[0])
|
||||
assertEquals("a", s[1])
|
||||
assertEquals("", s[2])
|
||||
|
||||
s = p.split("", 0)
|
||||
assertEquals(2, s.size)
|
||||
assertEquals("", s[0])
|
||||
assertEquals("", s[1])
|
||||
|
||||
s = p.split("abcd", 0)
|
||||
assertEquals(6, s.size)
|
||||
assertEquals("", s[0])
|
||||
assertEquals("a", s[1])
|
||||
assertEquals("b", s[2])
|
||||
assertEquals("c", s[3])
|
||||
assertEquals("d", s[4])
|
||||
assertEquals("", s[5])
|
||||
}
|
||||
|
||||
@Test fun testSplitSupplementaryWithEmptyString() {
|
||||
|
||||
/*
|
||||
* See http://www.unicode.org/reports/tr18/#Supplementary_Characters We
|
||||
* have to treat text as code points not code units.
|
||||
*/
|
||||
val p = Regex("")
|
||||
val s: List<String>
|
||||
s = p.split("a\ud869\uded6b", 0)
|
||||
assertEquals(5, s.size)
|
||||
assertEquals("", s[0])
|
||||
assertEquals("a", s[1])
|
||||
assertEquals("\ud869\uded6", s[2])
|
||||
assertEquals("b", s[3])
|
||||
assertEquals("", s[4])
|
||||
}
|
||||
|
||||
}
|
||||
@@ -82,7 +82,7 @@ kotlin {
|
||||
sourceSets {
|
||||
val wasmMain by getting {
|
||||
kotlin.srcDirs("builtins", "internal", "runtime", "src", "stubs")
|
||||
kotlin.srcDirs("../native-wasm/")
|
||||
kotlin.srcDirs("$rootDir/libraries/stdlib/native-wasm/src")
|
||||
kotlin.srcDirs(files(builtInsSources.map { it.destinationDir }))
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ kotlin {
|
||||
api(project(":kotlin-test:kotlin-test-wasm"))
|
||||
}
|
||||
kotlin.srcDir("$rootDir/libraries/stdlib/wasm/test/")
|
||||
kotlin.srcDir("$rootDir/libraries/stdlib/native-wasm/test/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,17 +144,27 @@ public class Char private constructor(public val value: Char) : Comparable<Char>
|
||||
/**
|
||||
* The minimum value of a supplementary code point, `\u0x10000`.
|
||||
*/
|
||||
public const val MIN_SUPPLEMENTARY_CODE_POINT: Int = 0x10000
|
||||
internal const val MIN_SUPPLEMENTARY_CODE_POINT: Int = 0x10000
|
||||
|
||||
/**
|
||||
* The minimum value of a Unicode code point.
|
||||
*/
|
||||
internal const val MIN_CODE_POINT = 0x000000
|
||||
|
||||
/**
|
||||
* The maximum value of a Unicode code point.
|
||||
*/
|
||||
internal const val MAX_CODE_POINT = 0X10FFFF
|
||||
|
||||
/**
|
||||
* The minimum radix available for conversion to and from strings.
|
||||
*/
|
||||
public const val MIN_RADIX: Int = 2
|
||||
internal const val MIN_RADIX: Int = 2
|
||||
|
||||
/**
|
||||
* The maximum radix available for conversion to and from strings.
|
||||
*/
|
||||
public const val MAX_RADIX: Int = 36
|
||||
internal const val MAX_RADIX: Int = 36
|
||||
|
||||
/**
|
||||
* The number of bytes used to represent a Char in a binary form.
|
||||
|
||||
@@ -1,972 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.text
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
actual class Regex {
|
||||
actual constructor(pattern: String) { TODO("Wasm stdlib: Text") }
|
||||
actual constructor(pattern: String, option: RegexOption) { TODO("Wasm stdlib: Text") }
|
||||
actual constructor(pattern: String, options: Set<RegexOption>) { TODO("Wasm stdlib: Text") }
|
||||
|
||||
actual val pattern: String = TODO("Wasm stdlib: Text")
|
||||
actual val options: Set<RegexOption> = TODO("Wasm stdlib: Text")
|
||||
|
||||
actual fun matchEntire(input: CharSequence): MatchResult? = TODO("Wasm stdlib: Text")
|
||||
actual infix fun matches(input: CharSequence): Boolean = TODO("Wasm stdlib: Text")
|
||||
actual fun containsMatchIn(input: CharSequence): Boolean = TODO("Wasm stdlib: Text")
|
||||
actual fun replace(input: CharSequence, replacement: String): String = TODO("Wasm stdlib: Text")
|
||||
actual fun replace(input: CharSequence, transform: (MatchResult) -> CharSequence): String = TODO("Wasm stdlib: Text")
|
||||
actual fun replaceFirst(input: CharSequence, replacement: String): String = TODO("Wasm stdlib: Text")
|
||||
|
||||
actual fun matchAt(input: CharSequence, index: Int): MatchResult? = TODO("Wasm stdlib: Text")
|
||||
actual fun matchesAt(input: CharSequence, index: Int): Boolean = TODO("Wasm stdlib: Text")
|
||||
|
||||
/**
|
||||
* Returns the first match of a regular expression in the [input], beginning at the specified [startIndex].
|
||||
*
|
||||
* @param startIndex An index to start search with, by default 0. Must be not less than zero and not greater than `input.length()`
|
||||
* @return An instance of [MatchResult] if match was found or `null` otherwise.
|
||||
* @sample samples.text.Regexps.find
|
||||
*/
|
||||
actual fun find(input: CharSequence, startIndex: Int): MatchResult? = TODO("Wasm stdlib: Text")
|
||||
|
||||
/**
|
||||
* Returns a sequence of all occurrences of a regular expression within the [input] string, beginning at the specified [startIndex].
|
||||
*
|
||||
* @sample samples.text.Regexps.findAll
|
||||
*/
|
||||
actual fun findAll(input: CharSequence, startIndex: Int): Sequence<MatchResult> = TODO("Wasm stdlib: Text")
|
||||
|
||||
/**
|
||||
* Splits the [input] CharSequence to a list of strings around matches of this regular expression.
|
||||
*
|
||||
* @param limit Non-negative value specifying the maximum number of substrings the string can be split to.
|
||||
* Zero by default means no limit is set.
|
||||
*/
|
||||
actual fun split(input: CharSequence, limit: Int): List<String> = TODO("Wasm stdlib: Text")
|
||||
|
||||
/**
|
||||
* Splits the [input] CharSequence to a sequence of strings around matches of this regular expression.
|
||||
*
|
||||
* @param limit Non-negative value specifying the maximum number of substrings the string can be split to.
|
||||
* Zero by default means no limit is set.
|
||||
* @sample samples.text.Regexps.splitToSequence
|
||||
*/
|
||||
public actual fun splitToSequence(input: CharSequence, limit: Int): Sequence<String> = TODO("Wasm stdlib: Text")
|
||||
|
||||
actual companion object {
|
||||
actual fun fromLiteral(literal: String): Regex = TODO("Wasm stdlib: Text")
|
||||
actual fun escape(literal: String): String = TODO("Wasm stdlib: Text")
|
||||
actual fun escapeReplacement(literal: String): String = TODO("Wasm stdlib: Text")
|
||||
}
|
||||
}
|
||||
|
||||
actual class MatchGroup {
|
||||
actual val value: String = TODO("Wasm stdlib: Text")
|
||||
}
|
||||
|
||||
actual enum class RegexOption {
|
||||
IGNORE_CASE,
|
||||
MULTILINE
|
||||
}
|
||||
|
||||
|
||||
// From char.kt
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a Unicode high-surrogate code unit (also known as leading-surrogate code unit).
|
||||
*/
|
||||
public actual fun Char.isHighSurrogate(): Boolean = this in Char.MIN_HIGH_SURROGATE..Char.MAX_HIGH_SURROGATE
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a Unicode low-surrogate code unit (also known as trailing-surrogate code unit).
|
||||
*/
|
||||
public actual fun Char.isLowSurrogate(): Boolean = this in Char.MIN_LOW_SURROGATE..Char.MAX_LOW_SURROGATE
|
||||
|
||||
/**
|
||||
* Converts this character to lower case using Unicode mapping rules of the invariant locale.
|
||||
*/
|
||||
@Deprecated("Use lowercaseChar() instead.", ReplaceWith("lowercaseChar()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun Char.toLowerCase(): Char = lowercaseCharImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to lower case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function performs one-to-one character mapping.
|
||||
* To support one-to-many character mapping use the [lowercase] function.
|
||||
* If this character has no mapping equivalent, the character itself is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.lowercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun Char.lowercaseChar(): Char = lowercaseCharImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to lower case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function supports one-to-many character mapping, thus the length of the returned string can be greater than one.
|
||||
* For example, `'\u0130'.lowercase()` returns `"\u0069\u0307"`,
|
||||
* where `'\u0130'` is the LATIN CAPITAL LETTER I WITH DOT ABOVE character (`İ`).
|
||||
* If this character has no lower case mapping, the result of `toString()` of this char is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.lowercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun Char.lowercase(): String = lowercaseImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to upper case using Unicode mapping rules of the invariant locale.
|
||||
*/
|
||||
@Deprecated("Use uppercaseChar() instead.", ReplaceWith("uppercaseChar()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun Char.toUpperCase(): Char = uppercaseCharImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to upper case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function performs one-to-one character mapping.
|
||||
* To support one-to-many character mapping use the [uppercase] function.
|
||||
* If this character has no mapping equivalent, the character itself is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.uppercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun Char.uppercaseChar(): Char = uppercaseCharImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to upper case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function supports one-to-many character mapping, thus the length of the returned string can be greater than one.
|
||||
* For example, `'\uFB00'.uppercase()` returns `"\u0046\u0046"`,
|
||||
* where `'\uFB00'` is the LATIN SMALL LIGATURE FF character (`ff`).
|
||||
* If this character has no upper case mapping, the result of `toString()` of this char is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.uppercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun Char.uppercase(): String = uppercaseImpl()
|
||||
|
||||
/**
|
||||
* Converts this character to title case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function performs one-to-one character mapping.
|
||||
* To support one-to-many character mapping use the [titlecase] function.
|
||||
* If this character has no mapping equivalent, the result of calling [uppercaseChar] is returned.
|
||||
*
|
||||
* @sample samples.text.Chars.titlecase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.titlecaseChar(): Char = titlecaseCharImpl()
|
||||
|
||||
|
||||
/**
|
||||
* Returns the Unicode general category of this character.
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual val Char.category: CharCategory
|
||||
get() = CharCategory.valueOf(getCategoryValue())
|
||||
|
||||
/**
|
||||
* Returns `true` if this character (Unicode code point) is defined in Unicode.
|
||||
*
|
||||
* A character is considered to be defined in Unicode if its [category] is not [CharCategory.UNASSIGNED].
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isDefined(): Boolean {
|
||||
if (this < '\u0080') {
|
||||
return true
|
||||
}
|
||||
return getCategoryValue() != CharCategory.UNASSIGNED.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a letter.
|
||||
*
|
||||
* A character is considered to be a letter if its [category] is [CharCategory.UPPERCASE_LETTER],
|
||||
* [CharCategory.LOWERCASE_LETTER], [CharCategory.TITLECASE_LETTER], [CharCategory.MODIFIER_LETTER], or [CharCategory.OTHER_LETTER].
|
||||
*
|
||||
* @sample samples.text.Chars.isLetter
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isLetter(): Boolean {
|
||||
if (this in 'a'..'z' || this in 'A'..'Z') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return isLetterImpl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a letter or digit.
|
||||
*
|
||||
* @see isLetter
|
||||
* @see isDigit
|
||||
*
|
||||
* @sample samples.text.Chars.isLetterOrDigit
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isLetterOrDigit(): Boolean {
|
||||
if (this in 'a'..'z' || this in 'A'..'Z' || this in '0'..'9') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
|
||||
return isDigit() || isLetter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a digit.
|
||||
*
|
||||
* A character is considered to be a digit if its [category] is [CharCategory.DECIMAL_DIGIT_NUMBER].
|
||||
*
|
||||
* @sample samples.text.Chars.isDigit
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isDigit(): Boolean {
|
||||
if (this in '0'..'9') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return isDigitImpl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is an upper case letter.
|
||||
*
|
||||
* A character is considered to be an upper case letter if its [category] is [CharCategory.UPPERCASE_LETTER].
|
||||
*
|
||||
* @sample samples.text.Chars.isUpperCase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isUpperCase(): Boolean {
|
||||
if (this in 'A'..'Z') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return isUpperCaseImpl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a lower case letter.
|
||||
*
|
||||
* A character is considered to be a lower case letter if its [category] is [CharCategory.LOWERCASE_LETTER].
|
||||
*
|
||||
* @sample samples.text.Chars.isLowerCase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isLowerCase(): Boolean {
|
||||
if (this in 'a'..'z') {
|
||||
return true
|
||||
}
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return isLowerCaseImpl()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a title case letter.
|
||||
*
|
||||
* A character is considered to be a title case letter if its [category] is [CharCategory.TITLECASE_LETTER].
|
||||
*
|
||||
* @sample samples.text.Chars.isTitleCase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isTitleCase(): Boolean {
|
||||
if (this < '\u0080') {
|
||||
return false
|
||||
}
|
||||
return getCategoryValue() == CharCategory.TITLECASE_LETTER.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is an ISO control character.
|
||||
*
|
||||
* A character is considered to be an ISO control character if its [category] is [CharCategory.CONTROL].
|
||||
*
|
||||
* @sample samples.text.Chars.isISOControl
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isISOControl(): Boolean {
|
||||
return this <= '\u001F' || this in '\u007F'..'\u009F'
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a character is whitespace according to the Unicode standard.
|
||||
* Returns `true` if the character is whitespace.
|
||||
*
|
||||
* @sample samples.text.Chars.isWhitespace
|
||||
*/
|
||||
public actual fun Char.isWhitespace(): Boolean = isWhitespaceImpl()
|
||||
|
||||
// From string.kt
|
||||
|
||||
|
||||
/**
|
||||
* Converts the characters in the specified array to a string.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
@Deprecated("Use CharArray.concatToString() instead", ReplaceWith("chars.concatToString()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.4", errorSince = "1.5")
|
||||
public actual fun String(chars: CharArray): String {
|
||||
var result = ""
|
||||
for (char in chars) {
|
||||
result += char
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the characters from a portion of the specified array to a string.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if either [offset] or [length] are less than zero
|
||||
* or `offset + length` is out of [chars] array bounds.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
@Deprecated("Use CharArray.concatToString(startIndex, endIndex) instead", ReplaceWith("chars.concatToString(offset, offset + length)"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.4", errorSince = "1.5")
|
||||
public actual fun String(chars: CharArray, offset: Int, length: Int): String {
|
||||
if (offset < 0 || length < 0 || chars.size - offset < length)
|
||||
throw IndexOutOfBoundsException("size: ${chars.size}; offset: $offset; length: $length")
|
||||
var result = ""
|
||||
for (index in offset until offset + length) {
|
||||
result += chars[index]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates characters in this [CharArray] into a String.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun CharArray.concatToString(): String {
|
||||
var result = ""
|
||||
for (char in this) {
|
||||
result += char
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates characters in this [CharArray] or its subrange into a String.
|
||||
*
|
||||
* @param startIndex the beginning (inclusive) of the subrange of characters, 0 by default.
|
||||
* @param endIndex the end (exclusive) of the subrange of characters, size of this array by default.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the size of this array.
|
||||
* @throws IllegalArgumentException if [startIndex] is greater than [endIndex].
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun CharArray.concatToString(startIndex: Int = 0, endIndex: Int = this.size): String {
|
||||
AbstractList.checkBoundsIndexes(startIndex, endIndex, this.size)
|
||||
var result = ""
|
||||
for (index in startIndex until endIndex) {
|
||||
result += this[index]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [CharArray] containing characters of this string.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun String.toCharArray(): CharArray {
|
||||
return CharArray(length) { get(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [CharArray] containing characters of this string or its substring.
|
||||
*
|
||||
* @param startIndex the beginning (inclusive) of the substring, 0 by default.
|
||||
* @param endIndex the end (exclusive) of the substring, length of this string by default.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the length of this string.
|
||||
* @throws IllegalArgumentException if [startIndex] is greater than [endIndex].
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.toCharArray(startIndex: Int = 0, endIndex: Int = this.length): CharArray {
|
||||
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
|
||||
return CharArray(endIndex - startIndex) { get(startIndex + it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a string from the bytes in UTF-8 encoding in this array.
|
||||
*
|
||||
* Malformed byte sequences are replaced by the replacement char `\uFFFD`.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun ByteArray.decodeToString(): String {
|
||||
return decodeUtf8(this, 0, size, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a string from the bytes in UTF-8 encoding in this array or its subrange.
|
||||
*
|
||||
* @param startIndex the beginning (inclusive) of the subrange to decode, 0 by default.
|
||||
* @param endIndex the end (exclusive) of the subrange to decode, size of this array by default.
|
||||
* @param throwOnInvalidSequence specifies whether to throw an exception on malformed byte sequence or replace it by the replacement char `\uFFFD`.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the size of this array.
|
||||
* @throws IllegalArgumentException if [startIndex] is greater than [endIndex].
|
||||
* @throws CharacterCodingException if the byte array contains malformed UTF-8 byte sequence and [throwOnInvalidSequence] is true.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun ByteArray.decodeToString(
|
||||
startIndex: Int = 0,
|
||||
endIndex: Int = this.size,
|
||||
throwOnInvalidSequence: Boolean = false
|
||||
): String {
|
||||
AbstractList.checkBoundsIndexes(startIndex, endIndex, this.size)
|
||||
return decodeUtf8(this, startIndex, endIndex, throwOnInvalidSequence)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes this string to an array of bytes in UTF-8 encoding.
|
||||
*
|
||||
* Any malformed char sequence is replaced by the replacement byte sequence.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun String.encodeToByteArray(): ByteArray {
|
||||
return encodeUtf8(this, 0, length, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes this string or its substring to an array of bytes in UTF-8 encoding.
|
||||
*
|
||||
* @param startIndex the beginning (inclusive) of the substring to encode, 0 by default.
|
||||
* @param endIndex the end (exclusive) of the substring to encode, length of this string by default.
|
||||
* @param throwOnInvalidSequence specifies whether to throw an exception on malformed char sequence or replace.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the length of this string.
|
||||
* @throws IllegalArgumentException if [startIndex] is greater than [endIndex].
|
||||
* @throws CharacterCodingException if this string contains malformed char sequence and [throwOnInvalidSequence] is true.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.encodeToByteArray(
|
||||
startIndex: Int = 0,
|
||||
endIndex: Int = this.length,
|
||||
throwOnInvalidSequence: Boolean = false
|
||||
): ByteArray {
|
||||
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
|
||||
return encodeUtf8(this, startIndex, endIndex, throwOnInvalidSequence)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a substring of this string that starts at the specified [startIndex] and continues to the end of the string.
|
||||
*/
|
||||
public actual fun String.substring(startIndex: Int): String =
|
||||
subSequence(startIndex, this.length) as String
|
||||
|
||||
/**
|
||||
* Returns the substring of this string starting at the [startIndex] and ending right before the [endIndex].
|
||||
*
|
||||
* @param startIndex the start index (inclusive).
|
||||
* @param endIndex the end index (exclusive).
|
||||
*/
|
||||
public actual fun String.substring(startIndex: Int, endIndex: Int): String =
|
||||
subSequence(startIndex, endIndex) as String
|
||||
|
||||
/**
|
||||
* Returns a copy of this string converted to upper case using the rules of the default locale.
|
||||
*/
|
||||
@Deprecated("Use uppercase() instead.", ReplaceWith("uppercase()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun String.toUpperCase(): String = uppercase()
|
||||
|
||||
/**
|
||||
* Returns a copy of this string converted to upper case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function supports one-to-many and many-to-one character mapping,
|
||||
* thus the length of the returned string can be different from the length of the original string.
|
||||
*
|
||||
* @sample samples.text.Strings.uppercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun String.uppercase(): String = uppercaseImpl()
|
||||
|
||||
/**
|
||||
* Returns a copy of this string converted to lower case using the rules of the default locale.
|
||||
*/
|
||||
@Deprecated("Use lowercase() instead.", ReplaceWith("lowercase()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun String.toLowerCase(): String = lowercase()
|
||||
|
||||
/**
|
||||
* Returns a copy of this string converted to lower case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function supports one-to-many and many-to-one character mapping,
|
||||
* thus the length of the returned string can be different from the length of the original string.
|
||||
*
|
||||
* @sample samples.text.Strings.lowercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun String.lowercase(): String = lowercaseImpl()
|
||||
|
||||
/**
|
||||
* Returns a copy of this string having its first letter titlecased using the rules of the default locale,
|
||||
* or the original string if it's empty or already starts with a title case letter.
|
||||
*
|
||||
* The title case of a character is usually the same as its upper case with several exceptions.
|
||||
* The particular list of characters with the special title case form depends on the underlying platform.
|
||||
*
|
||||
* @sample samples.text.Strings.capitalize
|
||||
*/
|
||||
@Deprecated("Use replaceFirstChar instead.", ReplaceWith("replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun String.capitalize(): String = replaceFirstChar(Char::uppercaseChar)
|
||||
|
||||
/**
|
||||
* Returns a copy of this string having its first letter lowercased using the rules of the default locale,
|
||||
* or the original string if it's empty or already starts with a lower case letter.
|
||||
*
|
||||
* @sample samples.text.Strings.decapitalize
|
||||
*/
|
||||
@Deprecated("Use replaceFirstChar instead.", ReplaceWith("replaceFirstChar { it.lowercase() }"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun String.decapitalize(): String = replaceFirstChar(Char::lowercaseChar)
|
||||
|
||||
/**
|
||||
* Returns a string containing this char sequence repeated [n] times.
|
||||
* @throws [IllegalArgumentException] when n < 0.
|
||||
* @sample samples.text.Strings.repeat
|
||||
*/
|
||||
public actual fun CharSequence.repeat(n: Int): String {
|
||||
require(n >= 0) { "Count 'n' must be non-negative, but was $n." }
|
||||
return when (n) {
|
||||
0 -> ""
|
||||
1 -> this.toString()
|
||||
else -> {
|
||||
var result = ""
|
||||
if (!isEmpty()) {
|
||||
var s = this.toString()
|
||||
var count = n
|
||||
while (true) {
|
||||
if ((count and 1) == 1) {
|
||||
result += s
|
||||
}
|
||||
count = count ushr 1
|
||||
if (count == 0) {
|
||||
break
|
||||
}
|
||||
s += s
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new string with all occurrences of [oldChar] replaced with [newChar].
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.replace(oldChar: Char, newChar: Char, ignoreCase: Boolean = false): String {
|
||||
return buildString(length) {
|
||||
this@replace.forEach { c ->
|
||||
append(if (c.equals(oldChar, ignoreCase)) newChar else c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new string obtained by replacing all occurrences of the [oldValue] substring in this string
|
||||
* with the specified [newValue] string.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.replace(oldValue: String, newValue: String, ignoreCase: Boolean = false): String {
|
||||
run {
|
||||
var occurrenceIndex: Int = indexOf(oldValue, 0, ignoreCase)
|
||||
// FAST PATH: no match
|
||||
if (occurrenceIndex < 0) return this
|
||||
|
||||
val oldValueLength = oldValue.length
|
||||
val searchStep = oldValueLength.coerceAtLeast(1)
|
||||
val newLengthHint = length - oldValueLength + newValue.length
|
||||
if (newLengthHint < 0) throw OutOfMemoryError()
|
||||
val stringBuilder = StringBuilder(newLengthHint)
|
||||
|
||||
var i = 0
|
||||
do {
|
||||
stringBuilder.append(this, i, occurrenceIndex).append(newValue)
|
||||
i = occurrenceIndex + oldValueLength
|
||||
if (occurrenceIndex >= length) break
|
||||
occurrenceIndex = indexOf(oldValue, occurrenceIndex + searchStep, ignoreCase)
|
||||
} while (occurrenceIndex > 0)
|
||||
return stringBuilder.append(this, i, length).toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new string with the first occurrence of [oldChar] replaced with [newChar].
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual 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.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual 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 `true` if this string is equal to [other], optionally ignoring character case.
|
||||
*
|
||||
* Two strings are considered to be equal if they have the same length and the same character at the same index.
|
||||
* If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared.
|
||||
*
|
||||
* @param ignoreCase `true` to ignore character case when comparing strings. By default `false`.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean {
|
||||
if (this == null) return other == null
|
||||
if (other == null) return false
|
||||
if (!ignoreCase) return this == other
|
||||
|
||||
if (this.length != other.length) return false
|
||||
|
||||
for (index in 0 until this.length) {
|
||||
val thisChar = this[index]
|
||||
val otherChar = other[index]
|
||||
if (!thisChar.equals(otherChar, ignoreCase)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two strings lexicographically, optionally ignoring case differences.
|
||||
*
|
||||
* If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.compareTo(other: String, ignoreCase: Boolean = false): Int {
|
||||
if (ignoreCase) {
|
||||
val n1 = this.length
|
||||
val n2 = other.length
|
||||
val min = minOf(n1, n2)
|
||||
if (min == 0) return n1 - n2
|
||||
for (index in 0 until min) {
|
||||
var thisChar = this[index]
|
||||
var otherChar = other[index]
|
||||
|
||||
if (thisChar != otherChar) {
|
||||
thisChar = thisChar.uppercaseChar()
|
||||
otherChar = otherChar.uppercaseChar()
|
||||
|
||||
if (thisChar != otherChar) {
|
||||
thisChar = thisChar.lowercaseChar()
|
||||
otherChar = otherChar.lowercaseChar()
|
||||
|
||||
if (thisChar != otherChar) {
|
||||
return thisChar.compareTo(otherChar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return n1 - n2
|
||||
} else {
|
||||
return compareTo(other)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the contents of this char sequence are equal to the contents of the specified [other],
|
||||
* i.e. both char sequences contain the same number of the same characters in the same order.
|
||||
*
|
||||
* @sample samples.text.Strings.contentEquals
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual infix fun CharSequence?.contentEquals(other: CharSequence?): Boolean = contentEqualsImpl(other)
|
||||
|
||||
/**
|
||||
* Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], optionally ignoring case difference.
|
||||
*
|
||||
* @param ignoreCase `true` to ignore character case when comparing contents.
|
||||
*
|
||||
* @sample samples.text.Strings.contentEquals
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun CharSequence?.contentEquals(other: CharSequence?, ignoreCase: Boolean): Boolean {
|
||||
return if (ignoreCase)
|
||||
this.contentEqualsIgnoreCaseImpl(other)
|
||||
else
|
||||
this.contentEqualsImpl(other)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this string starts with the specified prefix.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.startsWith(prefix: String, ignoreCase: Boolean = false): Boolean =
|
||||
regionMatches(0, prefix, 0, prefix.length, ignoreCase)
|
||||
|
||||
/**
|
||||
* Returns `true` if a substring of this string starting at the specified offset [startIndex] starts with the specified prefix.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.startsWith(prefix: String, startIndex: Int, ignoreCase: Boolean = false): Boolean =
|
||||
regionMatches(startIndex, prefix, 0, prefix.length, ignoreCase)
|
||||
|
||||
/**
|
||||
* Returns `true` if this string ends with the specified suffix.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.endsWith(suffix: String, ignoreCase: Boolean = false): Boolean =
|
||||
regionMatches(length - suffix.length, suffix, 0, suffix.length, ignoreCase)
|
||||
|
||||
// From stringsCode.kt
|
||||
|
||||
/**
|
||||
* Returns `true` if this string is empty or consists solely of whitespace characters.
|
||||
*
|
||||
* @sample samples.text.Strings.stringIsBlank
|
||||
*/
|
||||
public actual 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.
|
||||
*/
|
||||
actual fun CharSequence.regionMatches(
|
||||
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 until length) {
|
||||
if (!this[thisOffset + index].equals(other[otherOffset + index], ignoreCase))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val STRING_CASE_INSENSITIVE_ORDER = Comparator<String> { a, b -> a.compareTo(b, ignoreCase = true) }
|
||||
|
||||
/**
|
||||
* A Comparator that orders strings ignoring character case.
|
||||
*
|
||||
* Note that this Comparator does not take locale into account,
|
||||
* and will result in an unsatisfactory ordering for certain locales.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
public actual val String.Companion.CASE_INSENSITIVE_ORDER: Comparator<String>
|
||||
get() = STRING_CASE_INSENSITIVE_ORDER
|
||||
|
||||
/**
|
||||
* Returns `true` if the content of this string is equal to the word "true", ignoring case, and `false` otherwise.
|
||||
*/
|
||||
@Deprecated("Use Kotlin compiler 1.4 to avoid deprecation warning.")
|
||||
@DeprecatedSinceKotlin(hiddenSince = "1.4")
|
||||
@kotlin.internal.InlineOnly
|
||||
actual fun String.toBoolean(): Boolean = this.toBoolean()
|
||||
|
||||
/**
|
||||
* Returns `true` if the contents of this string is equal to the word "true", ignoring case, and `false` otherwise.
|
||||
*
|
||||
* There are also strict versions of the function available on non-nullable String, [toBooleanStrict] and [toBooleanStrictOrNull].
|
||||
*/
|
||||
actual fun String?.toBoolean(): Boolean = this != null && this.lowercase() == "true"
|
||||
|
||||
/**
|
||||
* Parses the string as a signed [Byte] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
actual fun String.toByte(): Byte = toByteOrNull() ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a signed [Byte] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for string to number conversion.
|
||||
*/
|
||||
public actual fun String.toByte(radix: Int): Byte = toByteOrNull(radix) ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Short] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toShort(): Short = toShortOrNull() ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Short] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for string to number conversion.
|
||||
*/
|
||||
public actual fun String.toShort(radix: Int): Short = toShortOrNull(radix) ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as an [Int] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toInt(): Int = toIntOrNull() ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as an [Int] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for string to number conversion.
|
||||
*/
|
||||
public actual fun String.toInt(radix: Int): Int = toIntOrNull(radix) ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Long] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toLong(): Long = toLongOrNull() ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Long] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for string to number conversion.
|
||||
*/
|
||||
public actual fun String.toLong(radix: Int): Long = toLongOrNull(radix) ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Double] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toDouble(): Double = kotlin.text.parseDouble(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Float] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toFloat(): Float = toDouble() as Float
|
||||
|
||||
/**
|
||||
* Parses the string as a [Float] number and returns the result
|
||||
* or `null` if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toFloatOrNull(): Float? = toDoubleOrNull() as Float?
|
||||
|
||||
/**
|
||||
* Parses the string as a [Double] number and returns the result
|
||||
* or `null` if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toDoubleOrNull(): Double? {
|
||||
try {
|
||||
return toDouble()
|
||||
} catch (e: NumberFormatException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this [Byte] value in the specified [radix].
|
||||
*
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for number to string conversion.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
public actual fun Byte.toString(radix: Int): String = this.toLong().toString(radix)
|
||||
|
||||
/**
|
||||
* Returns a string representation of this [Short] value in the specified [radix].
|
||||
*
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for number to string conversion.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
public actual fun Short.toString(radix: Int): String = this.toLong().toString(radix)
|
||||
|
||||
/**
|
||||
* Returns a string representation of this [Int] value in the specified [radix].
|
||||
*
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for number to string conversion.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
actual fun Int.toString(radix: Int): String = toLong().toString(radix)
|
||||
|
||||
/**
|
||||
* Returns a string representation of this [Long] value in the specified [radix].
|
||||
*
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for number to string conversion.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
actual fun Long.toString(radix: Int): String {
|
||||
checkRadix(radix)
|
||||
|
||||
fun Long.getChar() = toInt().let { if (it < 10) '0' + it else 'a' + (it - 10) }
|
||||
|
||||
if (radix == 10) return toString()
|
||||
if (this in 0 until radix) return getChar().toString()
|
||||
|
||||
val isNegative = this < 0
|
||||
val buffer = CharArray(Long.SIZE_BITS + 1)
|
||||
|
||||
var currentBufferIndex= buffer.lastIndex
|
||||
var current: Long = this
|
||||
while(current != 0L) {
|
||||
buffer[currentBufferIndex] = abs(current % radix).getChar()
|
||||
current /= radix
|
||||
currentBufferIndex--
|
||||
}
|
||||
|
||||
if (isNegative) {
|
||||
buffer[currentBufferIndex] = '-'
|
||||
currentBufferIndex--
|
||||
}
|
||||
|
||||
return buffer.concatToString(currentBufferIndex + 1)
|
||||
}
|
||||
|
||||
@PublishedApi
|
||||
internal actual fun checkRadix(radix: Int): Int {
|
||||
if (radix !in Char.MIN_RADIX..Char.MAX_RADIX) {
|
||||
throw IllegalArgumentException("radix $radix was not in valid range ${Char.MIN_RADIX..Char.MAX_RADIX}")
|
||||
}
|
||||
return radix
|
||||
}
|
||||
|
||||
internal actual fun digitOf(char: Char, radix: Int): Int = when {
|
||||
char >= '0' && char <= '9' -> char - '0'
|
||||
char >= 'A' && char <= 'Z' -> char - 'A' + 10
|
||||
char >= 'a' && char <= 'z' -> char - 'a' + 10
|
||||
char < '\u0080' -> -1
|
||||
char >= '\uFF21' && char <= '\uFF3A' -> char - '\uFF21' + 10 // full-width latin capital letter
|
||||
char >= '\uFF41' && char <= '\uFF5A' -> char - '\uFF41' + 10 // full-width latin small letter
|
||||
else -> char.digitToIntImpl()
|
||||
}.let { if (it >= radix) -1 else it }
|
||||
@@ -5,5 +5,34 @@
|
||||
|
||||
package kotlin.text
|
||||
|
||||
internal fun Char.Companion.toCodePoint(high: Char, low: Char): Int =
|
||||
(((high - MIN_HIGH_SURROGATE) shl 10) or (low - MIN_LOW_SURROGATE)) + 0x10000
|
||||
/**
|
||||
* Returns `true` if this character is an ISO control character.
|
||||
*
|
||||
* A character is considered to be an ISO control character if its [category] is [CharCategory.CONTROL].
|
||||
*
|
||||
* @sample samples.text.Chars.isISOControl
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun Char.isISOControl(): Boolean {
|
||||
return this <= '\u001F' || this in '\u007F'..'\u009F'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a Unicode high-surrogate code unit (also known as leading-surrogate code unit).
|
||||
*/
|
||||
public actual fun Char.isHighSurrogate(): Boolean = this in Char.MIN_HIGH_SURROGATE..Char.MAX_HIGH_SURROGATE
|
||||
|
||||
/**
|
||||
* Returns `true` if this character is a Unicode low-surrogate code unit (also known as trailing-surrogate code unit).
|
||||
*/
|
||||
public actual fun Char.isLowSurrogate(): Boolean = this in Char.MIN_LOW_SURROGATE..Char.MAX_LOW_SURROGATE
|
||||
|
||||
internal actual fun digitOf(char: Char, radix: Int): Int = when {
|
||||
char >= '0' && char <= '9' -> char - '0'
|
||||
char >= 'A' && char <= 'Z' -> char - 'A' + 10
|
||||
char >= 'a' && char <= 'z' -> char - 'a' + 10
|
||||
char < '\u0080' -> -1
|
||||
char >= '\uFF21' && char <= '\uFF3A' -> char - '\uFF21' + 10 // full-width latin capital letter
|
||||
char >= '\uFF41' && char <= '\uFF5A' -> char - '\uFF41' + 10 // full-width latin small letter
|
||||
else -> char.digitToIntImpl()
|
||||
}.let { if (it >= radix) -1 else it }
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.text
|
||||
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Returns `true` if the content of this string is equal to the word "true", ignoring case, and `false` otherwise.
|
||||
*/
|
||||
@Deprecated("Use Kotlin compiler 1.4 to avoid deprecation warning.")
|
||||
@DeprecatedSinceKotlin(hiddenSince = "1.4")
|
||||
@kotlin.internal.InlineOnly
|
||||
actual fun String.toBoolean(): Boolean = this.toBoolean()
|
||||
|
||||
/**
|
||||
* Returns `true` if the contents of this string is equal to the word "true", ignoring case, and `false` otherwise.
|
||||
*
|
||||
* There are also strict versions of the function available on non-nullable String, [toBooleanStrict] and [toBooleanStrictOrNull].
|
||||
*/
|
||||
actual fun String?.toBoolean(): Boolean = this != null && this.lowercase() == "true"
|
||||
|
||||
/**
|
||||
* Parses the string as a signed [Byte] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
actual fun String.toByte(): Byte = toByteOrNull() ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a signed [Byte] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for string to number conversion.
|
||||
*/
|
||||
public actual fun String.toByte(radix: Int): Byte = toByteOrNull(radix) ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Short] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toShort(): Short = toShortOrNull() ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Short] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for string to number conversion.
|
||||
*/
|
||||
public actual fun String.toShort(radix: Int): Short = toShortOrNull(radix) ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as an [Int] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toInt(): Int = toIntOrNull() ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as an [Int] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for string to number conversion.
|
||||
*/
|
||||
public actual fun String.toInt(radix: Int): Int = toIntOrNull(radix) ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Long] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toLong(): Long = toLongOrNull() ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Long] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for string to number conversion.
|
||||
*/
|
||||
public actual fun String.toLong(radix: Int): Long = toLongOrNull(radix) ?: numberFormatError(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Double] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toDouble(): Double = kotlin.text.parseDouble(this)
|
||||
|
||||
/**
|
||||
* Parses the string as a [Float] number and returns the result.
|
||||
* @throws NumberFormatException if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toFloat(): Float = toDouble() as Float
|
||||
|
||||
/**
|
||||
* Parses the string as a [Float] number and returns the result
|
||||
* or `null` if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toFloatOrNull(): Float? = toDoubleOrNull() as Float?
|
||||
|
||||
/**
|
||||
* Parses the string as a [Double] number and returns the result
|
||||
* or `null` if the string is not a valid representation of a number.
|
||||
*/
|
||||
public actual fun String.toDoubleOrNull(): Double? {
|
||||
try {
|
||||
return toDouble()
|
||||
} catch (e: NumberFormatException) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of this [Byte] value in the specified [radix].
|
||||
*
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for number to string conversion.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
public actual fun Byte.toString(radix: Int): String = this.toLong().toString(radix)
|
||||
|
||||
/**
|
||||
* Returns a string representation of this [Short] value in the specified [radix].
|
||||
*
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for number to string conversion.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
public actual fun Short.toString(radix: Int): String = this.toLong().toString(radix)
|
||||
|
||||
/**
|
||||
* Returns a string representation of this [Int] value in the specified [radix].
|
||||
*
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for number to string conversion.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
actual fun Int.toString(radix: Int): String = toLong().toString(radix)
|
||||
|
||||
/**
|
||||
* Returns a string representation of this [Long] value in the specified [radix].
|
||||
*
|
||||
* @throws IllegalArgumentException when [radix] is not a valid radix for number to string conversion.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
actual fun Long.toString(radix: Int): String {
|
||||
checkRadix(radix)
|
||||
|
||||
fun Long.getChar() = toInt().let { if (it < 10) '0' + it else 'a' + (it - 10) }
|
||||
|
||||
if (radix == 10) return toString()
|
||||
if (this in 0 until radix) return getChar().toString()
|
||||
|
||||
val isNegative = this < 0
|
||||
val buffer = CharArray(Long.SIZE_BITS + 1)
|
||||
|
||||
var currentBufferIndex = buffer.lastIndex
|
||||
var current: Long = this
|
||||
while(current != 0L) {
|
||||
buffer[currentBufferIndex] = abs(current % radix).getChar()
|
||||
current /= radix
|
||||
currentBufferIndex--
|
||||
}
|
||||
|
||||
if (isNegative) {
|
||||
buffer[currentBufferIndex] = '-'
|
||||
currentBufferIndex--
|
||||
}
|
||||
|
||||
return buffer.concatToString(currentBufferIndex + 1)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ internal actual fun String.nativeLastIndexOf(ch: Char, fromIndex: Int): Int {
|
||||
* Returns the index within this string of the first occurrence of the specified substring, starting from the specified offset.
|
||||
*/
|
||||
internal actual fun String.nativeIndexOf(str: String, fromIndex: Int): Int {
|
||||
for (index in fromIndex.coerceAtLeast(0)..this.length) {
|
||||
for (index in fromIndex.coerceAtLeast(0)..(this.length - str.length)) {
|
||||
if (str.regionMatchesImpl(0, this, index, str.length, false)) {
|
||||
return index
|
||||
}
|
||||
@@ -41,10 +41,461 @@ internal actual fun String.nativeIndexOf(str: String, fromIndex: Int): Int {
|
||||
* Returns the index within this string of the last occurrence of the specified character, starting from the specified offset.
|
||||
*/
|
||||
internal actual fun String.nativeLastIndexOf(str: String, fromIndex: Int): Int {
|
||||
for (index in fromIndex.coerceAtMost(this.lastIndex) downTo 0) {
|
||||
for (index in fromIndex.coerceAtMost(this.length - str.length) downTo 0) {
|
||||
if (str.regionMatchesImpl(0, this, index, str.length, false)) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the characters in the specified array to a string.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
@Deprecated("Use CharArray.concatToString() instead", ReplaceWith("chars.concatToString()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.4", errorSince = "1.5")
|
||||
public actual fun String(chars: CharArray): String {
|
||||
var result = ""
|
||||
for (char in chars) {
|
||||
result += char
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the characters from a portion of the specified array to a string.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if either [offset] or [length] are less than zero
|
||||
* or `offset + length` is out of [chars] array bounds.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
@Deprecated("Use CharArray.concatToString(startIndex, endIndex) instead", ReplaceWith("chars.concatToString(offset, offset + length)"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.4", errorSince = "1.5")
|
||||
public actual fun String(chars: CharArray, offset: Int, length: Int): String {
|
||||
if (offset < 0 || length < 0 || chars.size - offset < length)
|
||||
throw IndexOutOfBoundsException("size: ${chars.size}; offset: $offset; length: $length")
|
||||
var result = ""
|
||||
for (index in offset until offset + length) {
|
||||
result += chars[index]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates characters in this [CharArray] into a String.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun CharArray.concatToString(): String =
|
||||
String.unsafeFromCharArray(this.copyOf())
|
||||
|
||||
/**
|
||||
* Concatenates characters in this [CharArray] or its subrange into a String.
|
||||
*
|
||||
* @param startIndex the beginning (inclusive) of the subrange of characters, 0 by default.
|
||||
* @param endIndex the end (exclusive) of the subrange of characters, size of this array by default.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the size of this array.
|
||||
* @throws IllegalArgumentException if [startIndex] is greater than [endIndex].
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun CharArray.concatToString(startIndex: Int = 0, endIndex: Int = this.size): String {
|
||||
AbstractList.checkBoundsIndexes(startIndex, endIndex, this.size)
|
||||
return String.unsafeFromCharArray(this.copyOfRange(startIndex, endIndex))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [CharArray] containing characters of this string.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun String.toCharArray(): CharArray {
|
||||
return CharArray(length) { get(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a [CharArray] containing characters of this string or its substring.
|
||||
*
|
||||
* @param startIndex the beginning (inclusive) of the substring, 0 by default.
|
||||
* @param endIndex the end (exclusive) of the substring, length of this string by default.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the length of this string.
|
||||
* @throws IllegalArgumentException if [startIndex] is greater than [endIndex].
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.toCharArray(startIndex: Int = 0, endIndex: Int = this.length): CharArray {
|
||||
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
|
||||
return CharArray(endIndex - startIndex) { get(startIndex + it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a string from the bytes in UTF-8 encoding in this array.
|
||||
*
|
||||
* Malformed byte sequences are replaced by the replacement char `\uFFFD`.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun ByteArray.decodeToString(): String {
|
||||
return decodeUtf8(this, 0, size, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a string from the bytes in UTF-8 encoding in this array or its subrange.
|
||||
*
|
||||
* @param startIndex the beginning (inclusive) of the subrange to decode, 0 by default.
|
||||
* @param endIndex the end (exclusive) of the subrange to decode, size of this array by default.
|
||||
* @param throwOnInvalidSequence specifies whether to throw an exception on malformed byte sequence or replace it by the replacement char `\uFFFD`.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the size of this array.
|
||||
* @throws IllegalArgumentException if [startIndex] is greater than [endIndex].
|
||||
* @throws CharacterCodingException if the byte array contains malformed UTF-8 byte sequence and [throwOnInvalidSequence] is true.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun ByteArray.decodeToString(
|
||||
startIndex: Int = 0,
|
||||
endIndex: Int = this.size,
|
||||
throwOnInvalidSequence: Boolean = false
|
||||
): String {
|
||||
AbstractList.checkBoundsIndexes(startIndex, endIndex, this.size)
|
||||
return decodeUtf8(this, startIndex, endIndex, throwOnInvalidSequence)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes this string to an array of bytes in UTF-8 encoding.
|
||||
*
|
||||
* Any malformed char sequence is replaced by the replacement byte sequence.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
public actual fun String.encodeToByteArray(): ByteArray {
|
||||
return encodeUtf8(this, 0, length, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes this string or its substring to an array of bytes in UTF-8 encoding.
|
||||
*
|
||||
* @param startIndex the beginning (inclusive) of the substring to encode, 0 by default.
|
||||
* @param endIndex the end (exclusive) of the substring to encode, length of this string by default.
|
||||
* @param throwOnInvalidSequence specifies whether to throw an exception on malformed char sequence or replace.
|
||||
*
|
||||
* @throws IndexOutOfBoundsException if [startIndex] is less than zero or [endIndex] is greater than the length of this string.
|
||||
* @throws IllegalArgumentException if [startIndex] is greater than [endIndex].
|
||||
* @throws CharacterCodingException if this string contains malformed char sequence and [throwOnInvalidSequence] is true.
|
||||
*/
|
||||
@SinceKotlin("1.4")
|
||||
@WasExperimental(ExperimentalStdlibApi::class)
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.encodeToByteArray(
|
||||
startIndex: Int = 0,
|
||||
endIndex: Int = this.length,
|
||||
throwOnInvalidSequence: Boolean = false
|
||||
): ByteArray {
|
||||
AbstractList.checkBoundsIndexes(startIndex, endIndex, length)
|
||||
return encodeUtf8(this, startIndex, endIndex, throwOnInvalidSequence)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a substring of this string that starts at the specified [startIndex] and continues to the end of the string.
|
||||
*/
|
||||
public actual fun String.substring(startIndex: Int): String =
|
||||
subSequence(startIndex, this.length) as String
|
||||
|
||||
/**
|
||||
* Returns the substring of this string starting at the [startIndex] and ending right before the [endIndex].
|
||||
*
|
||||
* @param startIndex the start index (inclusive).
|
||||
* @param endIndex the end index (exclusive).
|
||||
*/
|
||||
public actual fun String.substring(startIndex: Int, endIndex: Int): String =
|
||||
subSequence(startIndex, endIndex) as String
|
||||
|
||||
/**
|
||||
* Returns a copy of this string converted to upper case using the rules of the default locale.
|
||||
*/
|
||||
@Deprecated("Use uppercase() instead.", ReplaceWith("uppercase()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun String.toUpperCase(): String = uppercase()
|
||||
|
||||
/**
|
||||
* Returns a copy of this string converted to upper case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function supports one-to-many and many-to-one character mapping,
|
||||
* thus the length of the returned string can be different from the length of the original string.
|
||||
*
|
||||
* @sample samples.text.Strings.uppercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun String.uppercase(): String = uppercaseImpl()
|
||||
|
||||
/**
|
||||
* Returns a copy of this string converted to lower case using the rules of the default locale.
|
||||
*/
|
||||
@Deprecated("Use lowercase() instead.", ReplaceWith("lowercase()"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun String.toLowerCase(): String = lowercase()
|
||||
|
||||
/**
|
||||
* Returns a copy of this string converted to lower case using Unicode mapping rules of the invariant locale.
|
||||
*
|
||||
* This function supports one-to-many and many-to-one character mapping,
|
||||
* thus the length of the returned string can be different from the length of the original string.
|
||||
*
|
||||
* @sample samples.text.Strings.lowercase
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun String.lowercase(): String = lowercaseImpl()
|
||||
|
||||
/**
|
||||
* Returns a copy of this string having its first letter titlecased using the rules of the default locale,
|
||||
* or the original string if it's empty or already starts with a title case letter.
|
||||
*
|
||||
* The title case of a character is usually the same as its upper case with several exceptions.
|
||||
* The particular list of characters with the special title case form depends on the underlying platform.
|
||||
*
|
||||
* @sample samples.text.Strings.capitalize
|
||||
*/
|
||||
@Deprecated("Use replaceFirstChar instead.", ReplaceWith("replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun String.capitalize(): String = replaceFirstChar(Char::uppercaseChar)
|
||||
|
||||
/**
|
||||
* Returns a copy of this string having its first letter lowercased using the rules of the default locale,
|
||||
* or the original string if it's empty or already starts with a lower case letter.
|
||||
*
|
||||
* @sample samples.text.Strings.decapitalize
|
||||
*/
|
||||
@Deprecated("Use replaceFirstChar instead.", ReplaceWith("replaceFirstChar { it.lowercase() }"))
|
||||
@DeprecatedSinceKotlin(warningSince = "1.5")
|
||||
public actual fun String.decapitalize(): String = replaceFirstChar(Char::lowercaseChar)
|
||||
|
||||
/**
|
||||
* Returns a string containing this char sequence repeated [n] times.
|
||||
* @throws [IllegalArgumentException] when n < 0.
|
||||
* @sample samples.text.Strings.repeat
|
||||
*/
|
||||
public actual fun CharSequence.repeat(n: Int): String {
|
||||
require(n >= 0) { "Count 'n' must be non-negative, but was $n." }
|
||||
if (isEmpty()) return ""
|
||||
return when (n) {
|
||||
0 -> ""
|
||||
1 -> this.toString()
|
||||
else -> {
|
||||
buildString(n * length) {
|
||||
repeat(n) {
|
||||
append(this@repeat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new string with all occurrences of [oldChar] replaced with [newChar].
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.replace(oldChar: Char, newChar: Char, ignoreCase: Boolean = false): String {
|
||||
return buildString(length) {
|
||||
this@replace.forEach { c ->
|
||||
append(if (c.equals(oldChar, ignoreCase)) newChar else c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new string obtained by replacing all occurrences of the [oldValue] substring in this string
|
||||
* with the specified [newValue] string.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.replace(oldValue: String, newValue: String, ignoreCase: Boolean = false): String {
|
||||
run {
|
||||
var occurrenceIndex: Int = indexOf(oldValue, 0, ignoreCase)
|
||||
// FAST PATH: no match
|
||||
if (occurrenceIndex < 0) return this
|
||||
|
||||
val oldValueLength = oldValue.length
|
||||
val searchStep = oldValueLength.coerceAtLeast(1)
|
||||
val newLengthHint = length - oldValueLength + newValue.length
|
||||
if (newLengthHint < 0) throw OutOfMemoryError()
|
||||
val stringBuilder = StringBuilder(newLengthHint)
|
||||
|
||||
var i = 0
|
||||
do {
|
||||
stringBuilder.append(this, i, occurrenceIndex).append(newValue)
|
||||
i = occurrenceIndex + oldValueLength
|
||||
if (occurrenceIndex >= length) break
|
||||
occurrenceIndex = indexOf(oldValue, occurrenceIndex + searchStep, ignoreCase)
|
||||
} while (occurrenceIndex > 0)
|
||||
return stringBuilder.append(this, i, length).toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new string with the first occurrence of [oldChar] replaced with [newChar].
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual 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.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual 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 `true` if this string is equal to [other], optionally ignoring character case.
|
||||
*
|
||||
* Two strings are considered to be equal if they have the same length and the same character at the same index.
|
||||
* If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared.
|
||||
*
|
||||
* @param ignoreCase `true` to ignore character case when comparing strings. By default `false`.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean {
|
||||
if (this == null) return other == null
|
||||
if (other == null) return false
|
||||
if (!ignoreCase) return this == other
|
||||
|
||||
if (this.length != other.length) return false
|
||||
|
||||
for (index in 0 until this.length) {
|
||||
val thisChar = this[index]
|
||||
val otherChar = other[index]
|
||||
if (!thisChar.equals(otherChar, ignoreCase)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two strings lexicographically, optionally ignoring case differences.
|
||||
*
|
||||
* If [ignoreCase] is true, the result of `Char.uppercaseChar().lowercaseChar()` on each character is compared.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.compareTo(other: String, ignoreCase: Boolean = false): Int {
|
||||
if (ignoreCase) {
|
||||
val n1 = this.length
|
||||
val n2 = other.length
|
||||
val min = minOf(n1, n2)
|
||||
if (min == 0) return n1 - n2
|
||||
for (index in 0 until min) {
|
||||
var thisChar = this[index]
|
||||
var otherChar = other[index]
|
||||
|
||||
if (thisChar != otherChar) {
|
||||
thisChar = thisChar.uppercaseChar()
|
||||
otherChar = otherChar.uppercaseChar()
|
||||
|
||||
if (thisChar != otherChar) {
|
||||
thisChar = thisChar.lowercaseChar()
|
||||
otherChar = otherChar.lowercaseChar()
|
||||
|
||||
if (thisChar != otherChar) {
|
||||
return thisChar.compareTo(otherChar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return n1 - n2
|
||||
} else {
|
||||
return compareTo(other)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the contents of this char sequence are equal to the contents of the specified [other],
|
||||
* i.e. both char sequences contain the same number of the same characters in the same order.
|
||||
*
|
||||
* @sample samples.text.Strings.contentEquals
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual infix fun CharSequence?.contentEquals(other: CharSequence?): Boolean = contentEqualsImpl(other)
|
||||
|
||||
/**
|
||||
* Returns `true` if the contents of this char sequence are equal to the contents of the specified [other], optionally ignoring case difference.
|
||||
*
|
||||
* @param ignoreCase `true` to ignore character case when comparing contents.
|
||||
*
|
||||
* @sample samples.text.Strings.contentEquals
|
||||
*/
|
||||
@SinceKotlin("1.5")
|
||||
public actual fun CharSequence?.contentEquals(other: CharSequence?, ignoreCase: Boolean): Boolean {
|
||||
return if (ignoreCase)
|
||||
this.contentEqualsIgnoreCaseImpl(other)
|
||||
else
|
||||
this.contentEqualsImpl(other)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this string starts with the specified prefix.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.startsWith(prefix: String, ignoreCase: Boolean = false): Boolean =
|
||||
regionMatches(0, prefix, 0, prefix.length, ignoreCase)
|
||||
|
||||
/**
|
||||
* Returns `true` if a substring of this string starting at the specified offset [startIndex] starts with the specified prefix.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.startsWith(prefix: String, startIndex: Int, ignoreCase: Boolean = false): Boolean =
|
||||
regionMatches(startIndex, prefix, 0, prefix.length, ignoreCase)
|
||||
|
||||
/**
|
||||
* Returns `true` if this string ends with the specified suffix.
|
||||
*/
|
||||
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
|
||||
public actual fun String.endsWith(suffix: String, ignoreCase: Boolean = false): Boolean =
|
||||
regionMatches(length - suffix.length, suffix, 0, suffix.length, ignoreCase)
|
||||
|
||||
// From stringsCode.kt
|
||||
|
||||
/**
|
||||
* Returns `true` if this string is empty or consists solely of whitespace characters.
|
||||
*
|
||||
* @sample samples.text.Strings.stringIsBlank
|
||||
*/
|
||||
public actual 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.
|
||||
*/
|
||||
actual fun CharSequence.regionMatches(
|
||||
thisOffset: Int,
|
||||
other: CharSequence,
|
||||
otherOffset: Int,
|
||||
length: Int,
|
||||
ignoreCase: Boolean
|
||||
): Boolean = regionMatchesImpl(thisOffset, other, otherOffset, length, ignoreCase)
|
||||
|
||||
private val STRING_CASE_INSENSITIVE_ORDER = Comparator<String> { a, b -> a.compareTo(b, ignoreCase = true) }
|
||||
|
||||
/**
|
||||
* A Comparator that orders strings ignoring character case.
|
||||
*
|
||||
* Note that this Comparator does not take locale into account,
|
||||
* and will result in an unsatisfactory ordering for certain locales.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
public actual val String.Companion.CASE_INSENSITIVE_ORDER: Comparator<String>
|
||||
get() = STRING_CASE_INSENSITIVE_ORDER
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
|
||||
package kotlin.native
|
||||
|
||||
/**
|
||||
* A vector of bits growing if necessary and allowing one to set/clear/read bits from it by a bit index.
|
||||
* (this is the stripped copy of K/N implementation for Regex)
|
||||
*
|
||||
* @constructor creates an empty bit set with the specified [size]
|
||||
* @param size the size of one element in the array used to store bits.
|
||||
*/
|
||||
internal class BitSet constructor(size: Int = ELEMENT_SIZE) {
|
||||
|
||||
companion object {
|
||||
// Default size of one element in the array used to store bits.
|
||||
private const val ELEMENT_SIZE = 64
|
||||
private const val MAX_BIT_OFFSET = ELEMENT_SIZE - 1
|
||||
private const val ALL_TRUE = -1L // 0xFFFF_FFFF_FFFF_FFFF
|
||||
private const val ALL_FALSE = 0L // 0x0000_0000_0000_0000
|
||||
}
|
||||
|
||||
private var bits: LongArray = LongArray(bitToElementSize(size))
|
||||
|
||||
private val lastIndex: Int
|
||||
get() = size - 1
|
||||
|
||||
/** True if this BitSet contains no bits set to true. */
|
||||
val isEmpty: Boolean
|
||||
get() = bits.all { it == ALL_FALSE }
|
||||
|
||||
/** Actual number of bits available in the set. All bits with indices >= size assumed to be 0 */
|
||||
var size: Int = size
|
||||
private set
|
||||
|
||||
// Transforms a bit index into an element index in the `bits` array.
|
||||
private val Int.elementIndex: Int
|
||||
get() = this / ELEMENT_SIZE
|
||||
|
||||
// Transforms a bit index in the set into a bit in the element of the `bits` array.
|
||||
private val Int.bitOffset: Int
|
||||
get() = this % ELEMENT_SIZE
|
||||
|
||||
// Transforms a bit index in the set into pair of a `bits` element index and a bit index in the element.
|
||||
private val Int.asBitCoordinates: Pair<Int, Int>
|
||||
get() = Pair(elementIndex, bitOffset)
|
||||
|
||||
// Transforms a bit offset to the mask with only bit set corresponding to the offset.
|
||||
private val Int.asMask: Long
|
||||
get() = 0x1L shl this
|
||||
|
||||
// Transforms a bit offset to the mask with only bits before the index (inclusive) set.
|
||||
private val Int.asMaskBefore: Long
|
||||
get() = getMaskBetween(0, this)
|
||||
|
||||
// Transforms a bit offset to the mask with only bits after the index (inclusive) set.
|
||||
private val Int.asMaskAfter: Long
|
||||
get() = getMaskBetween(this, MAX_BIT_OFFSET)
|
||||
|
||||
// Builds a masks with 1 between fromOffset and toOffset (both inclusive).
|
||||
private fun getMaskBetween(fromOffset: Int, toOffset: Int): Long {
|
||||
var res = 0L
|
||||
val maskToAdd = fromOffset.asMask
|
||||
for (i in fromOffset..toOffset) {
|
||||
res = (res shl 1) or maskToAdd
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Transforms a size in bits to a size in elements of the `bits` array.
|
||||
private fun bitToElementSize(bitSize: Int): Int = (bitSize + ELEMENT_SIZE - 1) / ELEMENT_SIZE
|
||||
|
||||
// Transforms a pair of an element index and a bit offset to a bit index.
|
||||
private fun bitIndex(elementIndex: Int, bitOffset: Int) =
|
||||
elementIndex * ELEMENT_SIZE + bitOffset
|
||||
|
||||
// Sets all bits after the last available bit (size - 1) to 0.
|
||||
private fun clearUnusedTail() {
|
||||
val (lastElementIndex, lastBitOffset) = lastIndex.asBitCoordinates
|
||||
bits[bits.lastIndex] = bits[bits.lastIndex] and lastBitOffset.asMaskBefore
|
||||
for (i in lastElementIndex + 1 until bits.size) {
|
||||
bits[i] = ALL_FALSE
|
||||
}
|
||||
}
|
||||
|
||||
// Internal function. Sets bits specified by the element index and the given mask to value.
|
||||
private fun setBitsWithMask(elementIndex: Int, mask: Long, value: Boolean) {
|
||||
val element = bits[elementIndex]
|
||||
if (value) {
|
||||
bits[elementIndex] = element or mask
|
||||
} else {
|
||||
bits[elementIndex] = element and mask.inv()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if index is valid and extends the `bits` array if the index exceeds its size.
|
||||
* @throws [IndexOutOfBoundsException] if [index] < 0.
|
||||
*/
|
||||
private fun ensureCapacity(index: Int) {
|
||||
if (index < 0) {
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
if (index >= size) {
|
||||
size = index + 1
|
||||
if (index.elementIndex >= bits.size) {
|
||||
// Create a new array containing the index-th bit.
|
||||
bits = bits.copyOf(bitToElementSize(index + 1))
|
||||
}
|
||||
// Set all bits after the index to 0. TODO: We can remove it.
|
||||
clearUnusedTail()
|
||||
}
|
||||
}
|
||||
|
||||
/** Set the bit specified to the specified value. */
|
||||
fun set(index: Int, value: Boolean = true) {
|
||||
ensureCapacity(index)
|
||||
val (elementIndex, offset) = index.asBitCoordinates
|
||||
setBitsWithMask(elementIndex, offset.asMask, value)
|
||||
}
|
||||
|
||||
/** Sets the bits with indices between [from] (inclusive) and [to] (exclusive) to the specified value. */
|
||||
fun set(from : Int, to: Int, value: Boolean = true) = set(from until to, value)
|
||||
|
||||
/** Sets the bits from the range specified to the specified value. */
|
||||
fun set(range: IntRange, value: Boolean = true) {
|
||||
if (range.start < 0 || range.endInclusive < 0) {
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
if (range.start > range.endInclusive) { // Empty range.
|
||||
return
|
||||
}
|
||||
ensureCapacity(range.endInclusive)
|
||||
val (fromIndex, fromOffset) = range.start.asBitCoordinates
|
||||
val (toIndex, toOffset) = range.endInclusive.asBitCoordinates
|
||||
if (toIndex == fromIndex) {
|
||||
val mask = getMaskBetween(fromOffset, toOffset)
|
||||
setBitsWithMask(fromIndex, mask, value)
|
||||
} else {
|
||||
// Set bits in the first element.
|
||||
setBitsWithMask(fromIndex, fromOffset.asMaskAfter, value)
|
||||
// Set all bits of all elements (excluding border ones) to 0 or 1 depending.
|
||||
for (index in fromIndex + 1 until toIndex) {
|
||||
bits[index] = if (value) ALL_TRUE else ALL_FALSE
|
||||
}
|
||||
// Set bits in the last element
|
||||
setBitsWithMask(toIndex, toOffset.asMaskBefore, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an index of a next set (if [lookFor] == true) or clear
|
||||
* (if [lookFor] == false) bit after [startIndex] (inclusive).
|
||||
* Returns -1 (for [lookFor] == true) or [size] (for lookFor == false)
|
||||
* if there is no such bits between [startIndex] and [size] - 1.
|
||||
* @throws IndexOutOfBoundException if [startIndex] < 0.
|
||||
*/
|
||||
private fun nextBit(startIndex: Int, lookFor: Boolean): Int {
|
||||
if (startIndex < 0) {
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
if (startIndex >= size) {
|
||||
return if (lookFor) -1 else startIndex
|
||||
}
|
||||
val (startElementIndex, startOffset) = startIndex.asBitCoordinates
|
||||
// Look for the next set bit in the first element.
|
||||
var element = bits[startElementIndex]
|
||||
for (offset in startOffset..MAX_BIT_OFFSET) {
|
||||
val bit = element and (0x1L shl offset) != 0L
|
||||
if (bit == lookFor) { // Look for not 0 if we need a set bit and look for 0 otherwise.
|
||||
return bitIndex(startElementIndex, offset)
|
||||
}
|
||||
}
|
||||
// Look for in the remaining elements.
|
||||
for (index in startElementIndex + 1..bits.lastIndex) {
|
||||
element = bits[index]
|
||||
for (offset in 0..MAX_BIT_OFFSET) {
|
||||
val bit = element and (0x1L shl offset) != 0L
|
||||
if (bit == lookFor) { // Look for not 0 if we need a set bit and look for 0 otherwise.
|
||||
return bitIndex(index, offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (lookFor) -1 else size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an index of a next bit which value is `true` after [startIndex] (inclusive).
|
||||
* Returns -1 if there is no such bits after [startIndex].
|
||||
* @throws IndexOutOfBoundException if [startIndex] < 0.
|
||||
*/
|
||||
fun nextSetBit(startIndex: Int = 0): Int = nextBit(startIndex, true)
|
||||
|
||||
/**
|
||||
* Returns an index of a next bit which value is `false` after [startIndex] (inclusive).
|
||||
* Returns [size] if there is no such bits between [startIndex] and [size] - 1 assuming that the set has an infinite
|
||||
* sequence of `false` bits after (size - 1)-th.
|
||||
* @throws IndexOutOfBoundException if [startIndex] < 0.
|
||||
*/
|
||||
fun nextClearBit(startIndex: Int = 0): Int = nextBit(startIndex, false)
|
||||
|
||||
/** Returns a value of a bit with the [index] specified. */
|
||||
operator fun get(index: Int): Boolean {
|
||||
if (index < 0) {
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
if (index >= size) {
|
||||
return false
|
||||
}
|
||||
val (elementIndex, offset) = index.asBitCoordinates
|
||||
return bits[elementIndex] and offset.asMask != 0L
|
||||
}
|
||||
|
||||
private inline fun doOperation(another: BitSet, operation: Long.(Long) -> Long) {
|
||||
ensureCapacity(another.lastIndex)
|
||||
var index = 0
|
||||
while (index < another.bits.size) {
|
||||
bits[index] = operation(bits[index], another.bits[index])
|
||||
index++
|
||||
}
|
||||
while (index < bits.size) {
|
||||
bits[index] = operation(bits[index], ALL_FALSE)
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
/** Performs a logical and operation over corresponding bits of this and [another] BitSets. The result is saved in this BitSet. */
|
||||
fun and(another: BitSet) = doOperation(another, Long::and)
|
||||
|
||||
/** Performs a logical or operation over corresponding bits of this and [another] BitSets. The result is saved in this BitSet. */
|
||||
fun or(another: BitSet) = doOperation(another, Long::or)
|
||||
|
||||
/** Performs a logical xor operation over corresponding bits of this and [another] BitSets. The result is saved in this BitSet. */
|
||||
fun xor(another: BitSet) = doOperation(another, Long::xor)
|
||||
|
||||
/** Performs a logical and + not operations over corresponding bits of this and [another] BitSets. The result is saved in this BitSet. */
|
||||
fun andNot(another: BitSet) {
|
||||
ensureCapacity(another.lastIndex)
|
||||
var index = 0
|
||||
while (index < another.bits.size) {
|
||||
bits[index] = bits[index] and another.bits[index].inv()
|
||||
index++
|
||||
}
|
||||
while (index < bits.size) {
|
||||
bits[index] = bits[index] and ALL_TRUE
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if the specified BitSet has any bits set to true that are also set to true in this BitSet. */
|
||||
fun intersects(another: BitSet): Boolean =
|
||||
(0 until minOf(bits.size, another.bits.size)).any { bits[it] and another.bits[it] != 0L }
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.text.regex
|
||||
|
||||
/* Contains canonical classes (see http://www.unicode.org/Public/4.0-Update/UnicodeData-4.0.0.txt). */
|
||||
private val canonicalClassesKeys = intArrayOf(
|
||||
768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790,
|
||||
791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813,
|
||||
814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836,
|
||||
837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860,
|
||||
861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 1155, 1156, 1157, 1158,
|
||||
1425, 1426, 1427, 1428, 1429, 1430, 1431, 1432, 1433, 1434, 1435, 1436, 1437, 1438, 1439, 1440, 1441, 1442, 1443,
|
||||
1444, 1445, 1446, 1447, 1448, 1449, 1450, 1451, 1452, 1453, 1454, 1455, 1456, 1457, 1458, 1459, 1460, 1461, 1462,
|
||||
1463, 1464, 1465, 1467, 1468, 1469, 1471, 1473, 1474, 1476, 1477, 1479, 1552, 1553, 1554, 1555, 1556, 1557, 1611,
|
||||
1612, 1613, 1614, 1615, 1616, 1617, 1618, 1619, 1620, 1621, 1622, 1623, 1624, 1625, 1626, 1627, 1628, 1629, 1630,
|
||||
1648, 1750, 1751, 1752, 1753, 1754, 1755, 1756, 1759, 1760, 1761, 1762, 1763, 1764, 1767, 1768, 1770, 1771, 1772,
|
||||
1773, 1809, 1840, 1841, 1842, 1843, 1844, 1845, 1846, 1847, 1848, 1849, 1850, 1851, 1852, 1853, 1854, 1855, 1856,
|
||||
1857, 1858, 1859, 1860, 1861, 1862, 1863, 1864, 1865, 1866, 2364, 2381, 2385, 2386, 2387, 2388, 2492, 2509, 2620,
|
||||
2637, 2748, 2765, 2876, 2893, 3021, 3149, 3157, 3158, 3260, 3277, 3405, 3530, 3640, 3641, 3642, 3656, 3657, 3658,
|
||||
3659, 3768, 3769, 3784, 3785, 3786, 3787, 3864, 3865, 3893, 3895, 3897, 3953, 3954, 3956, 3962, 3963, 3964, 3965,
|
||||
3968, 3970, 3971, 3972, 3974, 3975, 4038, 4151, 4153, 4959, 5908, 5940, 6098, 6109, 6313, 6457, 6458, 6459, 6679,
|
||||
6680, 7616, 7617, 7618, 7619, 8400, 8401, 8402, 8403, 8404, 8405, 8406, 8407, 8408, 8409, 8410, 8411, 8412, 8417,
|
||||
8421, 8422, 8423, 8424, 8425, 8426, 8427, 12330, 12331, 12332, 12333, 12334, 12335, 12441, 12442, 43014, 64286, 65056,
|
||||
65057, 65058, 65059, 68109, 68111, 68152, 68153, 68154, 68159, 119141, 119142, 119143, 119144, 119145, 119149, 119150,
|
||||
119151, 119152, 119153, 119154, 119163, 119164, 119165, 119166, 119167, 119168, 119169, 119170, 119173, 119174,
|
||||
119175, 119176, 119177, 119178, 119179, 119210, 119211, 119212, 119213, 119362, 119363, 119364,
|
||||
)
|
||||
|
||||
private val canonicalClassesValues = intArrayOf(
|
||||
230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 232, 220,
|
||||
220, 220, 220, 232, 216, 220, 220, 220, 220, 220, 202, 202, 220, 220, 220, 220, 202, 202, 220, 220, 220, 220, 220,
|
||||
220, 220, 220, 220, 220, 220, 1, 1, 1, 1, 1, 220, 220, 220, 220, 230, 230, 230, 230, 230, 230, 230, 230, 240, 230,
|
||||
220, 220, 220, 230, 230, 230, 220, 220, 230, 230, 230, 220, 220, 220, 220, 230, 232, 220, 220, 230, 233, 234, 234,
|
||||
233, 234, 234, 233, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 220, 230,
|
||||
230, 230, 230, 220, 230, 230, 230, 222, 220, 230, 230, 230, 230, 230, 230, 220, 220, 220, 220, 220, 220, 230, 230,
|
||||
220, 230, 230, 222, 228, 230, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 230, 220, 18, 230, 230,
|
||||
230, 230, 230, 230, 27, 28, 29, 30, 31, 32, 33, 34, 230, 230, 220, 220, 230, 230, 230, 230, 230, 220, 230, 230, 35,
|
||||
230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 230, 220, 230, 230, 230, 220, 230, 230, 220, 36, 230, 220, 230, 230,
|
||||
220, 230, 230, 220, 220, 220, 230, 220, 220, 230, 220, 230, 230, 230, 220, 230, 220, 230, 220, 230, 220, 230, 230, 7,
|
||||
9, 230, 220, 230, 230, 7, 9, 7, 9, 7, 9, 7, 9, 9, 9, 84, 91, 7, 9, 9, 9, 103, 103, 9, 107, 107, 107, 107, 118, 118,
|
||||
122, 122, 122, 122, 220, 220, 220, 220, 216, 129, 130, 132, 130, 130, 130, 130, 130, 230, 230, 9, 230, 230, 220, 7, 9,
|
||||
230, 9, 9, 9, 230, 228, 222, 230, 220, 230, 220, 230, 230, 220, 230, 230, 230, 1, 1, 230, 230, 230, 230, 1, 1, 1, 230,
|
||||
230, 230, 1, 1, 230, 220, 230, 1, 1, 218, 228, 232, 222, 224, 224, 8, 8, 9, 26, 230, 230, 230, 230, 220, 230, 230, 1,
|
||||
220, 9, 216, 216, 1, 1, 1, 226, 216, 216, 216, 216, 216, 220, 220, 220, 220, 220, 220, 220, 220, 230, 230, 230, 230,
|
||||
230, 220, 220, 230, 230, 230, 230, 230, 230, 230,
|
||||
)
|
||||
|
||||
/* Symbols that are one symbol decompositions (see http://www.unicode.org/Public/4.0-Update/UnicodeData-4.0.0.txt). */
|
||||
private val singleDecompositions = intArrayOf(
|
||||
59, 75, 96, 180, 183, 197, 697, 768, 769, 787, 901, 902, 904, 905, 906, 908, 910, 911, 912, 937, 940, 941, 942, 943,
|
||||
944, 953, 972, 973, 974, 8194, 8195, 12296, 12297, 13470, 13497, 13499, 13535, 13589, 14062, 14076, 14209, 14383,
|
||||
14434, 14460, 14535, 14563, 14620, 14650, 14894, 14956, 15076, 15112, 15129, 15177, 15261, 15384, 15438, 15667, 15766,
|
||||
16044, 16056, 16155, 16380, 16392, 16408, 16441, 16454, 16534, 16611, 16687, 16898, 16935, 17056, 17153, 17204, 17241,
|
||||
17365, 17369, 17419, 17515, 17707, 17757, 17761, 17771, 17879, 17913, 17973, 18110, 18119, 18837, 18918, 19054, 19062,
|
||||
19122, 19251, 19406, 19662, 19693, 19704, 19798, 19981, 20006, 20018, 20024, 20025, 20029, 20033, 20098, 20102, 20142,
|
||||
20160, 20172, 20196, 20320, 20352, 20358, 20363, 20398, 20411, 20415, 20482, 20523, 20602, 20633, 20687, 20698, 20711,
|
||||
20800, 20805, 20813, 20820, 20836, 20839, 20840, 20841, 20845, 20855, 20864, 20877, 20882, 20885, 20887, 20900, 20908,
|
||||
20917, 20919, 20937, 20940, 20956, 20958, 20981, 20995, 20999, 21015, 21033, 21050, 21051, 21062, 21106, 21111, 21129,
|
||||
21147, 21155, 21171, 21191, 21193, 21202, 21214, 21220, 21237, 21242, 21253, 21254, 21271, 21311, 21321, 21329, 21338,
|
||||
21363, 21365, 21373, 21375, 21443, 21450, 21471, 21477, 21483, 21489, 21510, 21519, 21533, 21560, 21570, 21576, 21608,
|
||||
21662, 21666, 21693, 21750, 21776, 21843, 21845, 21859, 21892, 21895, 21913, 21917, 21931, 21939, 21952, 21954, 21986,
|
||||
22022, 22097, 22120, 22132, 22265, 22294, 22295, 22411, 22478, 22516, 22541, 22577, 22578, 22592, 22618, 22622, 22696,
|
||||
22700, 22707, 22744, 22751, 22766, 22770, 22775, 22790, 22810, 22818, 22852, 22856, 22865, 22868, 22882, 22899, 23000,
|
||||
23020, 23067, 23079, 23138, 23142, 23221, 23304, 23336, 23358, 23429, 23491, 23512, 23527, 23534, 23539, 23551, 23558,
|
||||
23586, 23615, 23648, 23650, 23652, 23653, 23662, 23693, 23744, 23833, 23875, 23888, 23915, 23918, 23932, 23986, 23994,
|
||||
24033, 24034, 24061, 24104, 24125, 24169, 24180, 24230, 24240, 24243, 24246, 24265, 24266, 24274, 24275, 24281, 24300,
|
||||
24318, 24324, 24354, 24403, 24418, 24425, 24427, 24459, 24474, 24489, 24493, 24525, 24535, 24565, 24569, 24594, 24604,
|
||||
24705, 24724, 24775, 24792, 24801, 24840, 24900, 24904, 24908, 24910, 24928, 24936, 24954, 24974, 24976, 24996, 25007,
|
||||
25010, 25054, 25074, 25078, 25088, 25104, 25115, 25134, 25140, 25181, 25265, 25289, 25295, 25299, 25300, 25340, 25342,
|
||||
25405, 25424, 25448, 25467, 25475, 25504, 25513, 25540, 25541, 25572, 25628, 25634, 25682, 25705, 25719, 25726, 25754,
|
||||
25757, 25796, 25935, 25942, 25964, 25976, 26009, 26053, 26082, 26083, 26131, 26185, 26228, 26248, 26257, 26268, 26292,
|
||||
26310, 26356, 26360, 26368, 26391, 26395, 26401, 26446, 26451, 26454, 26462, 26491, 26501, 26519, 26611, 26618, 26647,
|
||||
26655, 26706, 26753, 26757, 26766, 26792, 26900, 26946, 27043, 27114, 27138, 27155, 27304, 27347, 27355, 27396, 27425,
|
||||
27476, 27506, 27511, 27513, 27551, 27566, 27578, 27579, 27726, 27751, 27784, 27839, 27852, 27853, 27877, 27926, 27931,
|
||||
27934, 27956, 27966, 27969, 28009, 28010, 28023, 28024, 28037, 28107, 28122, 28138, 28153, 28186, 28207, 28270, 28316,
|
||||
28346, 28359, 28363, 28369, 28379, 28431, 28450, 28451, 28526, 28614, 28651, 28670, 28699, 28702, 28729, 28746, 28784,
|
||||
28791, 28797, 28825, 28845, 28872, 28889, 28997, 29001, 29038, 29084, 29134, 29136, 29200, 29211, 29224, 29227, 29237,
|
||||
29264, 29282, 29312, 29333, 29359, 29376, 29436, 29482, 29557, 29562, 29575, 29579, 29605, 29618, 29662, 29702, 29705,
|
||||
29730, 29767, 29788, 29801, 29809, 29829, 29833, 29848, 29898, 29958, 29988, 30011, 30014, 30041, 30053, 30064, 30178,
|
||||
30224, 30237, 30239, 30274, 30313, 30410, 30427, 30439, 30452, 30465, 30494, 30495, 30528, 30538, 30603, 30631, 30798,
|
||||
30827, 30860, 30865, 30922, 30924, 30971, 31018, 31036, 31038, 31048, 31049, 31056, 31062, 31069, 31070, 31077, 31103,
|
||||
31117, 31118, 31119, 31150, 31178, 31211, 31260, 31296, 31306, 31311, 31361, 31409, 31435, 31470, 31520, 31680, 31686,
|
||||
31689, 31806, 31840, 31867, 31890, 31934, 31954, 31958, 31971, 31975, 31976, 32000, 32016, 32034, 32047, 32091, 32099,
|
||||
32160, 32190, 32199, 32244, 32258, 32265, 32311, 32321, 32325, 32574, 32626, 32633, 32634, 32645, 32661, 32666, 32701,
|
||||
32762, 32769, 32773, 32838, 32864, 32879, 32880, 32894, 32907, 32941, 32946, 33027, 33086, 33240, 33256, 33261, 33281,
|
||||
33284, 33391, 33401, 33419, 33425, 33437, 33457, 33459, 33469, 33509, 33510, 33565, 33571, 33590, 33618, 33619, 33635,
|
||||
33709, 33725, 33737, 33738, 33740, 33756, 33767, 33775, 33777, 33853, 33865, 33879, 34030, 34033, 34035, 34044, 34070,
|
||||
34148, 34253, 34298, 34310, 34322, 34349, 34367, 34384, 34396, 34407, 34409, 34440, 34473, 34530, 34574, 34600, 34667,
|
||||
34681, 34694, 34746, 34785, 34817, 34847, 34892, 34912, 34915, 35010, 35023, 35031, 35038, 35041, 35064, 35066, 35088,
|
||||
35137, 35172, 35206, 35211, 35222, 35488, 35498, 35519, 35531, 35538, 35542, 35565, 35576, 35582, 35585, 35641, 35672,
|
||||
35712, 35722, 35912, 35925, 36011, 36033, 36034, 36040, 36051, 36104, 36123, 36215, 36284, 36299, 36335, 36336, 36554,
|
||||
36564, 36646, 36650, 36664, 36667, 36706, 36766, 36784, 36790, 36899, 36920, 36978, 36988, 37007, 37012, 37070, 37105,
|
||||
37117, 37137, 37147, 37226, 37273, 37300, 37324, 37327, 37329, 37428, 37432, 37494, 37500, 37591, 37592, 37636, 37706,
|
||||
37881, 37909, 38283, 38317, 38327, 38446, 38475, 38477, 38517, 38520, 38524, 38534, 38563, 38584, 38595, 38626, 38627,
|
||||
38646, 38647, 38691, 38706, 38728, 38742, 38875, 38880, 38911, 38923, 38936, 38953, 38971, 39006, 39138, 39151, 39164,
|
||||
39208, 39209, 39335, 39362, 39409, 39422, 39530, 39698, 39791, 40000, 40023, 40189, 40295, 40372, 40442, 40478, 40575,
|
||||
40599, 40607, 40635, 40654, 40697, 40702, 40709, 40719, 40726, 40763, 40771, 40845, 40846, 40860, 131362, 132380,
|
||||
132389, 132427, 132666, 133124, 133342, 133676, 133987, 136420, 136872, 136938, 137672, 138008, 138507, 138724,
|
||||
138726, 139651, 139679, 140081, 141012, 141380, 141386, 142092, 142321, 143370, 144056, 144223, 144275, 144284,
|
||||
144323, 144341, 144493, 145059, 145575, 146061, 146170, 146620, 146718, 147153, 147294, 147342, 148067, 148395,
|
||||
149000, 149301, 149524, 150582, 150674, 151457, 151480, 151620, 151794, 151795, 151833, 151859, 152137, 152605,
|
||||
153126, 153242, 153285, 153980, 154279, 154539, 154752, 154832, 155526, 156122, 156200, 156231, 156377, 156478,
|
||||
156890, 156963, 157096, 157607, 157621, 158524, 158774, 158933, 159083, 159532, 159665, 159954, 160714, 161383,
|
||||
161966, 162150, 162984, 163539, 163631, 165330, 165357, 165678, 166906, 167287, 168261, 168415, 168474, 168970,
|
||||
169110, 169398, 170800, 172238, 172293, 172558, 172689, 172946, 173568
|
||||
)
|
||||
|
||||
private val decompositionKeys = intArrayOf(
|
||||
192, 193, 194, 195, 196, 197, 199, 200, 201, 202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 213, 214, 217, 218,
|
||||
219, 220, 221, 224, 225, 226, 227, 228, 229, 231, 232, 233, 234, 235, 236, 237, 238, 239, 241, 242, 243, 244, 245,
|
||||
246, 249, 250, 251, 252, 253, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271,
|
||||
274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 296, 297, 298,
|
||||
299, 300, 301, 302, 303, 304, 308, 309, 310, 311, 313, 314, 315, 316, 317, 318, 323, 324, 325, 326, 327, 328, 332,
|
||||
333, 334, 335, 336, 337, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357,
|
||||
360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382,
|
||||
416, 417, 431, 432, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 478, 479, 480,
|
||||
481, 482, 483, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 500, 501, 504, 505, 506, 507, 508, 509, 510,
|
||||
511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533,
|
||||
534, 535, 536, 537, 538, 539, 542, 543, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 832,
|
||||
833, 835, 836, 884, 894, 901, 902, 903, 904, 905, 906, 908, 910, 911, 912, 938, 939, 940, 941, 942, 943, 944, 970,
|
||||
971, 972, 973, 974, 979, 980, 1024, 1025, 1027, 1031, 1036, 1037, 1038, 1049, 1081, 1104, 1105, 1107, 1111, 1116,
|
||||
1117, 1118, 1142, 1143, 1217, 1218, 1232, 1233, 1234, 1235, 1238, 1239, 1242, 1243, 1244, 1245, 1246, 1247, 1250,
|
||||
1251, 1252, 1253, 1254, 1255, 1258, 1259, 1260, 1261, 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, 1272, 1273,
|
||||
1570, 1571, 1572, 1573, 1574, 1728, 1730, 1747, 2345, 2353, 2356, 2392, 2393, 2394, 2395, 2396, 2397, 2398, 2399,
|
||||
2507, 2508, 2524, 2525, 2527, 2611, 2614, 2649, 2650, 2651, 2654, 2888, 2891, 2892, 2908, 2909, 2964, 3018, 3019,
|
||||
3020, 3144, 3264, 3271, 3272, 3274, 3275, 3402, 3403, 3404, 3546, 3548, 3549, 3550, 3907, 3917, 3922, 3927, 3932,
|
||||
3945, 3955, 3957, 3958, 3960, 3969, 3987, 3997, 4002, 4007, 4012, 4025, 4134, 7680, 7681, 7682, 7683, 7684, 7685,
|
||||
7686, 7687, 7688, 7689, 7690, 7691, 7692, 7693, 7694, 7695, 7696, 7697, 7698, 7699, 7700, 7701, 7702, 7703, 7704,
|
||||
7705, 7706, 7707, 7708, 7709, 7710, 7711, 7712, 7713, 7714, 7715, 7716, 7717, 7718, 7719, 7720, 7721, 7722, 7723,
|
||||
7724, 7725, 7726, 7727, 7728, 7729, 7730, 7731, 7732, 7733, 7734, 7735, 7736, 7737, 7738, 7739, 7740, 7741, 7742,
|
||||
7743, 7744, 7745, 7746, 7747, 7748, 7749, 7750, 7751, 7752, 7753, 7754, 7755, 7756, 7757, 7758, 7759, 7760, 7761,
|
||||
7762, 7763, 7764, 7765, 7766, 7767, 7768, 7769, 7770, 7771, 7772, 7773, 7774, 7775, 7776, 7777, 7778, 7779, 7780,
|
||||
7781, 7782, 7783, 7784, 7785, 7786, 7787, 7788, 7789, 7790, 7791, 7792, 7793, 7794, 7795, 7796, 7797, 7798, 7799,
|
||||
7800, 7801, 7802, 7803, 7804, 7805, 7806, 7807, 7808, 7809, 7810, 7811, 7812, 7813, 7814, 7815, 7816, 7817, 7818,
|
||||
7819, 7820, 7821, 7822, 7823, 7824, 7825, 7826, 7827, 7828, 7829, 7830, 7831, 7832, 7833, 7835, 7840, 7841, 7842,
|
||||
7843, 7844, 7845, 7846, 7847, 7848, 7849, 7850, 7851, 7852, 7853, 7854, 7855, 7856, 7857, 7858, 7859, 7860, 7861,
|
||||
7862, 7863, 7864, 7865, 7866, 7867, 7868, 7869, 7870, 7871, 7872, 7873, 7874, 7875, 7876, 7877, 7878, 7879, 7880,
|
||||
7881, 7882, 7883, 7884, 7885, 7886, 7887, 7888, 7889, 7890, 7891, 7892, 7893, 7894, 7895, 7896, 7897, 7898, 7899,
|
||||
7900, 7901, 7902, 7903, 7904, 7905, 7906, 7907, 7908, 7909, 7910, 7911, 7912, 7913, 7914, 7915, 7916, 7917, 7918,
|
||||
7919, 7920, 7921, 7922, 7923, 7924, 7925, 7926, 7927, 7928, 7929, 7936, 7937, 7938, 7939, 7940, 7941, 7942, 7943,
|
||||
7944, 7945, 7946, 7947, 7948, 7949, 7950, 7951, 7952, 7953, 7954, 7955, 7956, 7957, 7960, 7961, 7962, 7963, 7964,
|
||||
7965, 7968, 7969, 7970, 7971, 7972, 7973, 7974, 7975, 7976, 7977, 7978, 7979, 7980, 7981, 7982, 7983, 7984, 7985,
|
||||
7986, 7987, 7988, 7989, 7990, 7991, 7992, 7993, 7994, 7995, 7996, 7997, 7998, 7999, 8000, 8001, 8002, 8003, 8004,
|
||||
8005, 8008, 8009, 8010, 8011, 8012, 8013, 8016, 8017, 8018, 8019, 8020, 8021, 8022, 8023, 8025, 8027, 8029, 8031,
|
||||
8032, 8033, 8034, 8035, 8036, 8037, 8038, 8039, 8040, 8041, 8042, 8043, 8044, 8045, 8046, 8047, 8048, 8049, 8050,
|
||||
8051, 8052, 8053, 8054, 8055, 8056, 8057, 8058, 8059, 8060, 8061, 8064, 8065, 8066, 8067, 8068, 8069, 8070, 8071,
|
||||
8072, 8073, 8074, 8075, 8076, 8077, 8078, 8079, 8080, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089, 8090,
|
||||
8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099, 8100, 8101, 8102, 8103, 8104, 8105, 8106, 8107, 8108, 8109,
|
||||
8110, 8111, 8112, 8113, 8114, 8115, 8116, 8118, 8119, 8120, 8121, 8122, 8123, 8124, 8126, 8129, 8130, 8131, 8132,
|
||||
8134, 8135, 8136, 8137, 8138, 8139, 8140, 8141, 8142, 8143, 8144, 8145, 8146, 8147, 8150, 8151, 8152, 8153, 8154,
|
||||
8155, 8157, 8158, 8159, 8160, 8161, 8162, 8163, 8164, 8165, 8166, 8167, 8168, 8169, 8170, 8171, 8172, 8173, 8174,
|
||||
8175, 8178, 8179, 8180, 8182, 8183, 8184, 8185, 8186, 8187, 8188, 8189, 8192, 8193, 8486, 8490, 8491, 8602, 8603,
|
||||
8622, 8653, 8654, 8655, 8708, 8713, 8716, 8740, 8742, 8769, 8772, 8775, 8777, 8800, 8802, 8813, 8814, 8815, 8816,
|
||||
8817, 8820, 8821, 8824, 8825, 8832, 8833, 8836, 8837, 8840, 8841, 8876, 8877, 8878, 8879, 8928, 8929, 8930, 8931,
|
||||
8938, 8939, 8940, 8941, 9001, 9002, 10972, 12364, 12366, 12368, 12370, 12372, 12374, 12376, 12378, 12380, 12382,
|
||||
12384, 12386, 12389, 12391, 12393, 12400, 12401, 12403, 12404, 12406, 12407, 12409, 12410, 12412, 12413, 12436, 12446,
|
||||
12460, 12462, 12464, 12466, 12468, 12470, 12472, 12474, 12476, 12478, 12480, 12482, 12485, 12487, 12489, 12496, 12497,
|
||||
12499, 12500, 12502, 12503, 12505, 12506, 12508, 12509, 12532, 12535, 12536, 12537, 12538, 12542, 63744, 63745, 63746,
|
||||
63747, 63748, 63749, 63750, 63751, 63752, 63753, 63754, 63755, 63756, 63757, 63758, 63759, 63760, 63761, 63762, 63763,
|
||||
63764, 63765, 63766, 63767, 63768, 63769, 63770, 63771, 63772, 63773, 63774, 63775, 63776, 63777, 63778, 63779, 63780,
|
||||
63781, 63782, 63783, 63784, 63785, 63786, 63787, 63788, 63789, 63790, 63791, 63792, 63793, 63794, 63795, 63796, 63797,
|
||||
63798, 63799, 63800, 63801, 63802, 63803, 63804, 63805, 63806, 63807, 63808, 63809, 63810, 63811, 63812, 63813, 63814,
|
||||
63815, 63816, 63817, 63818, 63819, 63820, 63821, 63822, 63823, 63824, 63825, 63826, 63827, 63828, 63829, 63830, 63831,
|
||||
63832, 63833, 63834, 63835, 63836, 63837, 63838, 63839, 63840, 63841, 63842, 63843, 63844, 63845, 63846, 63847, 63848,
|
||||
63849, 63850, 63851, 63852, 63853, 63854, 63855, 63856, 63857, 63858, 63859, 63860, 63861, 63862, 63863, 63864, 63865,
|
||||
63866, 63867, 63868, 63869, 63870, 63871, 63872, 63873, 63874, 63875, 63876, 63877, 63878, 63879, 63880, 63881, 63882,
|
||||
63883, 63884, 63885, 63886, 63887, 63888, 63889, 63890, 63891, 63892, 63893, 63894, 63895, 63896, 63897, 63898, 63899,
|
||||
63900, 63901, 63902, 63903, 63904, 63905, 63906, 63907, 63908, 63909, 63910, 63911, 63912, 63913, 63914, 63915, 63916,
|
||||
63917, 63918, 63919, 63920, 63921, 63922, 63923, 63924, 63925, 63926, 63927, 63928, 63929, 63930, 63931, 63932, 63933,
|
||||
63934, 63935, 63936, 63937, 63938, 63939, 63940, 63941, 63942, 63943, 63944, 63945, 63946, 63947, 63948, 63949, 63950,
|
||||
63951, 63952, 63953, 63954, 63955, 63956, 63957, 63958, 63959, 63960, 63961, 63962, 63963, 63964, 63965, 63966, 63967,
|
||||
63968, 63969, 63970, 63971, 63972, 63973, 63974, 63975, 63976, 63977, 63978, 63979, 63980, 63981, 63982, 63983, 63984,
|
||||
63985, 63986, 63987, 63988, 63989, 63990, 63991, 63992, 63993, 63994, 63995, 63996, 63997, 63998, 63999, 64000, 64001,
|
||||
64002, 64003, 64004, 64005, 64006, 64007, 64008, 64009, 64010, 64011, 64012, 64013, 64016, 64018, 64021, 64022, 64023,
|
||||
64024, 64025, 64026, 64027, 64028, 64029, 64030, 64032, 64034, 64037, 64038, 64042, 64043, 64044, 64045, 64048, 64049,
|
||||
64050, 64051, 64052, 64053, 64054, 64055, 64056, 64057, 64058, 64059, 64060, 64061, 64062, 64063, 64064, 64065, 64066,
|
||||
64067, 64068, 64069, 64070, 64071, 64072, 64073, 64074, 64075, 64076, 64077, 64078, 64079, 64080, 64081, 64082, 64083,
|
||||
64084, 64085, 64086, 64087, 64088, 64089, 64090, 64091, 64092, 64093, 64094, 64095, 64096, 64097, 64098, 64099, 64100,
|
||||
64101, 64102, 64103, 64104, 64105, 64106, 64112, 64113, 64114, 64115, 64116, 64117, 64118, 64119, 64120, 64121, 64122,
|
||||
64123, 64124, 64125, 64126, 64127, 64128, 64129, 64130, 64131, 64132, 64133, 64134, 64135, 64136, 64137, 64138, 64139,
|
||||
64140, 64141, 64142, 64143, 64144, 64145, 64146, 64147, 64148, 64149, 64150, 64151, 64152, 64153, 64154, 64155, 64156,
|
||||
64157, 64158, 64159, 64160, 64161, 64162, 64163, 64164, 64165, 64166, 64167, 64168, 64169, 64170, 64171, 64172, 64173,
|
||||
64174, 64175, 64176, 64177, 64178, 64179, 64180, 64181, 64182, 64183, 64184, 64185, 64186, 64187, 64188, 64189, 64190,
|
||||
64191, 64192, 64193, 64194, 64195, 64196, 64197, 64198, 64199, 64200, 64201, 64202, 64203, 64204, 64205, 64206, 64207,
|
||||
64208, 64209, 64210, 64211, 64212, 64213, 64214, 64215, 64216, 64217, 64285, 64287, 64298, 64299, 64300, 64301, 64302,
|
||||
64303, 64304, 64305, 64306, 64307, 64308, 64309, 64310, 64312, 64313, 64314, 64315, 64316, 64318, 64320, 64321, 64323,
|
||||
64324, 64326, 64327, 64328, 64329, 64330, 64331, 64332, 64333, 64334, 119134, 119135, 119136, 119137, 119138, 119139,
|
||||
119140, 119227, 119228, 119229, 119230, 119231, 119232, 194560, 194561, 194562, 194563, 194564, 194565, 194566,
|
||||
194567, 194568, 194569, 194570, 194571, 194572, 194573, 194574, 194575, 194576, 194577, 194578, 194579, 194580,
|
||||
194581, 194582, 194583, 194584, 194585, 194586, 194587, 194588, 194589, 194590, 194591, 194592, 194593, 194594,
|
||||
194595, 194596, 194597, 194598, 194599, 194600, 194601, 194602, 194603, 194604, 194605, 194606, 194607, 194608,
|
||||
194609, 194610, 194611, 194612, 194613, 194614, 194615, 194616, 194617, 194618, 194619, 194620, 194621, 194622,
|
||||
194623, 194624, 194625, 194626, 194627, 194628, 194629, 194630, 194631, 194632, 194633, 194634, 194635, 194636,
|
||||
194637, 194638, 194639, 194640, 194641, 194642, 194643, 194644, 194645, 194646, 194647, 194648, 194649, 194650,
|
||||
194651, 194652, 194653, 194654, 194655, 194656, 194657, 194658, 194659, 194660, 194661, 194662, 194663, 194664,
|
||||
194665, 194666, 194667, 194668, 194669, 194670, 194671, 194672, 194673, 194674, 194675, 194676, 194677, 194678,
|
||||
194679, 194680, 194681, 194682, 194683, 194684, 194685, 194686, 194687, 194688, 194689, 194690, 194691, 194692,
|
||||
194693, 194694, 194695, 194696, 194697, 194698, 194699, 194700, 194701, 194702, 194703, 194704, 194705, 194706,
|
||||
194707, 194708, 194709, 194710, 194711, 194712, 194713, 194714, 194715, 194716, 194717, 194718, 194719, 194720,
|
||||
194721, 194722, 194723, 194724, 194725, 194726, 194727, 194728, 194729, 194730, 194731, 194732, 194733, 194734,
|
||||
194735, 194736, 194737, 194738, 194739, 194740, 194741, 194742, 194743, 194744, 194745, 194746, 194747, 194748,
|
||||
194749, 194750, 194751, 194752, 194753, 194754, 194755, 194756, 194757, 194758, 194759, 194760, 194761, 194762,
|
||||
194763, 194764, 194765, 194766, 194767, 194768, 194769, 194770, 194771, 194772, 194773, 194774, 194775, 194776,
|
||||
194777, 194778, 194779, 194780, 194781, 194782, 194783, 194784, 194785, 194786, 194787, 194788, 194789, 194790,
|
||||
194791, 194792, 194793, 194794, 194795, 194796, 194797, 194798, 194799, 194800, 194801, 194802, 194803, 194804,
|
||||
194805, 194806, 194807, 194808, 194809, 194810, 194811, 194812, 194813, 194814, 194815, 194816, 194817, 194818,
|
||||
194819, 194820, 194821, 194822, 194823, 194824, 194825, 194826, 194827, 194828, 194829, 194830, 194831, 194832,
|
||||
194833, 194834, 194835, 194836, 194837, 194838, 194839, 194840, 194841, 194842, 194843, 194844, 194845, 194846,
|
||||
194847, 194848, 194849, 194850, 194851, 194852, 194853, 194854, 194855, 194856, 194857, 194858, 194859, 194860,
|
||||
194861, 194862, 194863, 194864, 194865, 194866, 194867, 194868, 194869, 194870, 194871, 194872, 194873, 194874,
|
||||
194875, 194876, 194877, 194878, 194879, 194880, 194881, 194882, 194883, 194884, 194885, 194886, 194887, 194888,
|
||||
194889, 194890, 194891, 194892, 194893, 194894, 194895, 194896, 194897, 194898, 194899, 194900, 194901, 194902,
|
||||
194903, 194904, 194905, 194906, 194907, 194908, 194909, 194910, 194911, 194912, 194913, 194914, 194915, 194916,
|
||||
194917, 194918, 194919, 194920, 194921, 194922, 194923, 194924, 194925, 194926, 194927, 194928, 194929, 194930,
|
||||
194931, 194932, 194933, 194934, 194935, 194936, 194937, 194938, 194939, 194940, 194941, 194942, 194943, 194944,
|
||||
194945, 194946, 194947, 194948, 194949, 194950, 194951, 194952, 194953, 194954, 194955, 194956, 194957, 194958,
|
||||
194959, 194960, 194961, 194962, 194963, 194964, 194965, 194966, 194967, 194968, 194969, 194970, 194971, 194972,
|
||||
194973, 194974, 194975, 194976, 194977, 194978, 194979, 194980, 194981, 194982, 194983, 194984, 194985, 194986,
|
||||
194987, 194988, 194989, 194990, 194991, 194992, 194993, 194994, 194995, 194996, 194997, 194998, 194999, 195000,
|
||||
195001, 195002, 195003, 195004, 195005, 195006, 195007, 195008, 195009, 195010, 195011, 195012, 195013, 195014,
|
||||
195015, 195016, 195017, 195018, 195019, 195020, 195021, 195022, 195023, 195024, 195025, 195026, 195027, 195028,
|
||||
195029, 195030, 195031, 195032, 195033, 195034, 195035, 195036, 195037, 195038, 195039, 195040, 195041, 195042,
|
||||
195043, 195044, 195045, 195046, 195047, 195048, 195049, 195050, 195051, 195052, 195053, 195054, 195055, 195056,
|
||||
195057, 195058, 195059, 195060, 195061, 195062, 195063, 195064, 195065, 195066, 195067, 195068, 195069, 195070,
|
||||
195071, 195072, 195073, 195074, 195075, 195076, 195077, 195078, 195079, 195080, 195081, 195082, 195083, 195084,
|
||||
195085, 195086, 195087, 195088, 195089, 195090, 195091, 195092, 195093, 195094, 195095, 195096, 195097, 195098,
|
||||
195099, 195100, 195101
|
||||
)
|
||||
|
||||
private fun getCanonicalClass(ch: Int): Int {
|
||||
val index: Int = binarySearchRange(canonicalClassesKeys, ch)
|
||||
if (index == -1 || canonicalClassesKeys[index] != ch) {
|
||||
return 0
|
||||
}
|
||||
return canonicalClassesValues[index]
|
||||
}
|
||||
|
||||
private fun getDecomposition(codePoint: Int): IntArray? {
|
||||
val index: Int = binarySearchRange(decompositionKeys, codePoint)
|
||||
if (index == -1 || decompositionKeys[index] != codePoint) {
|
||||
return null
|
||||
}
|
||||
return decompositionValues[index]
|
||||
}
|
||||
|
||||
/** Gets canonical class for given codepoint from decomposition mappings table. */
|
||||
internal fun getCanonicalClassInternal(ch: Int): Int {
|
||||
return getCanonicalClass(ch)
|
||||
}
|
||||
|
||||
/** Check if the given character is in table of single decompositions. */
|
||||
internal fun hasSingleCodepointDecompositionInternal(ch: Int): Boolean {
|
||||
val index: Int = binarySearchRange(singleDecompositions, ch)
|
||||
return index != -1 && singleDecompositions[index] == ch
|
||||
}
|
||||
|
||||
/** Returns a decomposition for a given codepoint. */
|
||||
internal fun getDecompositionInternal(ch: Int): IntArray? = getDecomposition(ch)
|
||||
|
||||
/**
|
||||
* Decomposes the given string represented as an array of codepoints. Saves the decomposition into [outputCodepoints] array.
|
||||
* Returns the length of the decomposition.
|
||||
*/
|
||||
internal fun decomposeString(inputCodePoints: IntArray, inputLength: Int, outputCodePoints: IntArray): Int {
|
||||
if (inputLength == 0) return 0
|
||||
|
||||
var outputLength = 0
|
||||
for (i in 0 until inputLength) {
|
||||
val decomposition = getDecomposition(inputCodePoints[i])
|
||||
if (decomposition == null) {
|
||||
outputCodePoints[outputLength++] = inputCodePoints[i]
|
||||
} else {
|
||||
decomposition.copyInto(outputCodePoints, outputLength)
|
||||
outputLength += decomposition.size
|
||||
}
|
||||
}
|
||||
return outputLength
|
||||
}
|
||||
|
||||
/**
|
||||
* Decomposes the given codepoint. Saves the decomposition into [outputCodepoints] array starting with [fromIndex].
|
||||
* Returns the length of the decomposition.
|
||||
*/
|
||||
internal fun decomposeCodePoint(codePoint: Int, outputCodePoints: IntArray, fromIndex: Int): Int {
|
||||
val decomposition = getDecomposition(codePoint)
|
||||
if (decomposition == null) {
|
||||
outputCodePoints[fromIndex] = codePoint
|
||||
return 1
|
||||
} else {
|
||||
decomposition.copyInto(outputCodePoints, fromIndex)
|
||||
return decomposition.size
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the largest element in [array] smaller or equal to the specified [needle],
|
||||
* or -1 if [needle] is smaller than the smallest element in [array].
|
||||
*/
|
||||
private fun binarySearchRange(array: IntArray, needle: Int): Int {
|
||||
var bottom = 0
|
||||
var top = array.size - 1
|
||||
var middle = -1
|
||||
var value = 0
|
||||
while (bottom <= top) {
|
||||
middle = (bottom + top) / 2
|
||||
value = array[middle]
|
||||
if (needle > value)
|
||||
bottom = middle + 1
|
||||
else if (needle == value)
|
||||
return middle
|
||||
else
|
||||
top = middle - 1
|
||||
}
|
||||
return middle - (if (needle < value) 1 else 0)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,3 +9,23 @@ package kotlin.native.concurrent
|
||||
|
||||
internal val Any?.isFrozen
|
||||
inline get() = false
|
||||
|
||||
internal inline fun <T> T.freeze(): T = this
|
||||
|
||||
internal class AtomicReference<T>(public var value: T) {
|
||||
public fun compareAndSwap(expected: T, new: T): T {
|
||||
if (value == expected) {
|
||||
val old = value
|
||||
value = new
|
||||
return old
|
||||
}
|
||||
return value
|
||||
}
|
||||
public fun compareAndSet(expected: T, new: T): Boolean {
|
||||
if (value == expected) {
|
||||
value = new
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,4 @@ actual val supportsSuppressedExceptions: Boolean get() = false
|
||||
// TODO: implement named group reference in replacement expression
|
||||
public actual val supportsNamedCapturingGroup: Boolean get() = false
|
||||
|
||||
public actual val regexSplitUnicodeCodePointHandling: Boolean get() = TODO()
|
||||
public actual val regexSplitUnicodeCodePointHandling: Boolean get() = true
|
||||
Reference in New Issue
Block a user