Regex.matchAt/matchesAt #KT-34021

This commit is contained in:
Ilya Gorbunov
2021-07-07 06:52:58 +03:00
committed by teamcityserver
parent d99d25e51e
commit 28a0698463
9 changed files with 147 additions and 6 deletions
@@ -1,6 +1,6 @@
/*
* 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.
* 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
@@ -150,6 +150,12 @@ public actual class Regex internal constructor(internal val nativePattern: Patte
/** 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].
*
@@ -197,6 +203,23 @@ public actual class Regex internal constructor(internal val nativePattern: Patte
*/
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
}
private fun processReplacement(match: MatchResult, replacement: String): String {
val result = StringBuilder(replacement.length)
var escaped = false
@@ -1409,10 +1409,18 @@ public final class Regex {
public final fun findAll(input: kotlin.CharSequence, startIndex: kotlin.Int = ...): kotlin.sequences.Sequence<kotlin.text.MatchResult>
@kotlin.SinceKotlin(version = "1.5")
@kotlin.ExperimentalStdlibApi
public final fun matchAt(input: kotlin.CharSequence, index: kotlin.Int): kotlin.text.MatchResult?
public final fun matchEntire(input: kotlin.CharSequence): kotlin.text.MatchResult?
public final infix fun matches(input: kotlin.CharSequence): kotlin.Boolean
@kotlin.SinceKotlin(version = "1.5")
@kotlin.ExperimentalStdlibApi
public final fun matchesAt(input: kotlin.CharSequence, index: kotlin.Int): kotlin.Boolean
public final inline fun replace(input: kotlin.CharSequence, transform: (kotlin.text.MatchResult) -> kotlin.CharSequence): kotlin.String
public final fun replace(input: kotlin.CharSequence, replacement: kotlin.String): kotlin.String
+8
View File
@@ -1409,10 +1409,18 @@ public final class Regex {
public final fun findAll(input: kotlin.CharSequence, startIndex: kotlin.Int = ...): kotlin.sequences.Sequence<kotlin.text.MatchResult>
@kotlin.SinceKotlin(version = "1.5")
@kotlin.ExperimentalStdlibApi
public final fun matchAt(input: kotlin.CharSequence, index: kotlin.Int): kotlin.text.MatchResult?
public final fun matchEntire(input: kotlin.CharSequence): kotlin.text.MatchResult?
public final infix fun matches(input: kotlin.CharSequence): kotlin.Boolean
@kotlin.SinceKotlin(version = "1.5")
@kotlin.ExperimentalStdlibApi
public final fun matchesAt(input: kotlin.CharSequence, index: kotlin.Int): kotlin.Boolean
public final inline fun replace(input: kotlin.CharSequence, transform: (kotlin.text.MatchResult) -> kotlin.CharSequence): kotlin.String
public final fun replace(input: kotlin.CharSequence, replacement: kotlin.String): kotlin.String
+26 -1
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@@ -15,6 +15,31 @@ expect class Regex {
fun matchEntire(input: CharSequence): MatchResult?
infix fun matches(input: CharSequence): Boolean
/**
* Attempts to match a regular expression exactly at the specified [index] in the [input] char sequence.
*
* Unlike [matchEntire] function, it doesn't require the match to span to the end of [input].
*
* @return An instance of [MatchResult] if the input matches this [Regex] at the specified [index] or `null` otherwise.
* @throws IndexOutOfBoundsException if [index] is less than zero or greater than the length of the [input] char sequence.
*/
@SinceKotlin("1.5")
@ExperimentalStdlibApi
fun matchAt(input: CharSequence, index: Int): MatchResult?
/**
* Checks if a regular expression matches a part of the specified [input] char sequence
* exactly at the specified [index].
*
* Unlike [matches] function, it doesn't require the match to span to the end of [input].
*
* @throws IndexOutOfBoundsException if [index] is less than zero or greater than the length of the [input] char sequence.
*/
@SinceKotlin("1.5")
@ExperimentalStdlibApi
fun matchesAt(input: CharSequence, index: Int): Boolean
fun containsMatchIn(input: CharSequence): Boolean
fun replace(input: CharSequence, replacement: String): String
fun replace(input: CharSequence, transform: (MatchResult) -> CharSequence): String
+28 -1
View File
@@ -20,6 +20,8 @@ public actual enum class RegexOption(val value: String) {
MULTILINE("m")
}
private fun Iterable<RegexOption>.toFlags(prepend: String): String = joinToString("", prefix = prepend) { it.value }
/**
* Represents the results from a single capturing group within a [MatchResult] of [Regex].
@@ -51,7 +53,11 @@ public actual class Regex actual constructor(pattern: String, options: Set<Regex
public actual val pattern: String = pattern
/** The set of options that were used to create this regular expression. */
public actual val options: Set<RegexOption> = options.toSet()
private val nativePattern: RegExp = RegExp(pattern, options.joinToString(separator = "", prefix = "gu") { it.value })
private val nativePattern: RegExp = RegExp(pattern, options.toFlags("gu"))
private var nativeStickyPattern: RegExp? = null
private fun initStickyPattern(): RegExp =
nativeStickyPattern ?: RegExp(pattern, options.toFlags("yu")).also { nativeStickyPattern = it }
/** Indicates whether the regular expression matches the entire [input]. */
public actual infix fun matches(input: CharSequence): Boolean {
@@ -66,6 +72,17 @@ public actual class Regex actual constructor(pattern: String, options: Set<Regex
return nativePattern.test(input.toString())
}
@SinceKotlin("1.5")
@ExperimentalStdlibApi
public actual fun matchesAt(input: CharSequence, index: Int): Boolean {
if (index < 0 || index > input.length) {
throw IndexOutOfBoundsException("index out of bounds: $index, input length: ${input.length}")
}
val pattern = initStickyPattern()
pattern.lastIndex = index
return pattern.test(input.toString())
}
/**
* Returns the first match of a regular expression in the [input], beginning at the specified [startIndex].
*
@@ -109,6 +126,16 @@ public actual class Regex actual constructor(pattern: String, options: Set<Regex
return Regex("^${pattern.trimStart('^').trimEnd('$')}$", options).find(input)
}
@SinceKotlin("1.5")
@ExperimentalStdlibApi
public actual fun matchAt(input: CharSequence, index: Int): MatchResult? {
if (index < 0 || index > input.length) {
throw IndexOutOfBoundsException("index out of bounds: $index, input length: ${input.length}")
}
return initStickyPattern().findNext(input.toString(), index)
}
/**
* Replaces all occurrences of this regular expression in the specified [input] string with specified [replacement] expression.
*
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.text
@@ -143,6 +143,18 @@ internal constructor(private val nativePattern: Pattern) : Serializable {
*/
public actual fun matchEntire(input: CharSequence): MatchResult? = nativePattern.matcher(input).matchEntire(input)
@SinceKotlin("1.5")
@ExperimentalStdlibApi
public actual fun matchAt(input: CharSequence, index: Int): MatchResult? =
nativePattern.matcher(input).useAnchoringBounds(false).useTransparentBounds(true).region(index, input.length).run {
if (lookingAt()) MatcherMatchResult(this, input) else null
}
@SinceKotlin("1.5")
@ExperimentalStdlibApi
public actual fun matchesAt(input: CharSequence, index: Int): Boolean =
nativePattern.matcher(input).useAnchoringBounds(false).useTransparentBounds(true).region(index, input.length).lookingAt()
/**
* Replaces all occurrences of this regular expression in the specified [input] string with specified [replacement] expression.
*
+34 -1
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@@ -179,6 +179,39 @@ class RegexTest {
assertEquals("aaaabbbb", regex.matchEntire(input)!!.value)
}
@Test fun matchAt() {
val regex = Regex("[a-z][1-5]", RegexOption.IGNORE_CASE)
val input = "...a4...B1"
val positions = 0..input.length
val matchIndices = positions.filter { index -> regex.matchesAt(input, index) }
assertEquals(listOf(3, 8), matchIndices)
val reversedIndices = positions.reversed().filter { index -> regex.matchesAt(input, index) }.reversed()
assertEquals(matchIndices, reversedIndices)
val matches = positions.mapNotNull { index -> regex.matchAt(input, index)?.let { index to it } }
assertEquals(matchIndices, matches.map { it.first })
matches.forEach { (index, match) ->
assertEquals(index..index + 1, match.range)
assertEquals(input.substring(match.range), match.value)
}
for (index in listOf(-1, input.length + 1)) {
assertFailsWith<IndexOutOfBoundsException> { regex.matchAt(input, index) }
assertFailsWith<IndexOutOfBoundsException> { regex.matchesAt(input, index) }
}
val anchoringRegex = Regex("^[a-z]")
assertFalse(anchoringRegex.matchesAt(input, 3))
assertNull(anchoringRegex.matchAt(input, 3))
val lookbehindRegex = Regex("(?<=[a-z])\\d")
assertTrue(lookbehindRegex.matchesAt(input, 4))
assertNotNull(lookbehindRegex.matchAt(input, 4)).let { match ->
assertEquals("4", match.value)
}
}
@Test fun escapeLiteral() {
val literal = """[-\/\\^$*+?.()|[\]{}]"""
assertTrue(Regex.fromLiteral(literal).matches(literal))
+3
View File
@@ -20,6 +20,9 @@ actual class Regex {
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].
*
@@ -5201,8 +5201,10 @@ public final class kotlin/text/Regex : java/io/Serializable {
public static synthetic fun findAll$default (Lkotlin/text/Regex;Ljava/lang/CharSequence;IILjava/lang/Object;)Lkotlin/sequences/Sequence;
public final fun getOptions ()Ljava/util/Set;
public final fun getPattern ()Ljava/lang/String;
public final fun matchAt (Ljava/lang/CharSequence;I)Lkotlin/text/MatchResult;
public final fun matchEntire (Ljava/lang/CharSequence;)Lkotlin/text/MatchResult;
public final fun matches (Ljava/lang/CharSequence;)Z
public final fun matchesAt (Ljava/lang/CharSequence;I)Z
public final fun replace (Ljava/lang/CharSequence;Ljava/lang/String;)Ljava/lang/String;
public final fun replace (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)Ljava/lang/String;
public final fun replaceFirst (Ljava/lang/CharSequence;Ljava/lang/String;)Ljava/lang/String;