From 28a0698463b92d3f0cf24dfb60502885a32d9e2d Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Wed, 7 Jul 2021 06:52:58 +0300 Subject: [PATCH] Regex.matchAt/matchesAt #KT-34021 --- .../src/main/kotlin/kotlin/text/Regex.kt | 27 ++++++++++++-- libraries/stdlib/api/js-v1/kotlin.text.kt | 8 +++++ libraries/stdlib/api/js/kotlin.text.kt | 8 +++++ libraries/stdlib/common/src/kotlin/TextH.kt | 27 +++++++++++++- libraries/stdlib/js/src/kotlin/text/regex.kt | 29 ++++++++++++++- .../stdlib/jvm/src/kotlin/text/regex/Regex.kt | 14 +++++++- libraries/stdlib/test/text/RegexTest.kt | 35 ++++++++++++++++++- libraries/stdlib/wasm/src/kotlin/Text.kt | 3 ++ .../kotlin-stdlib-runtime-merged.txt | 2 ++ 9 files changed, 147 insertions(+), 6 deletions(-) diff --git a/kotlin-native/runtime/src/main/kotlin/kotlin/text/Regex.kt b/kotlin-native/runtime/src/main/kotlin/kotlin/text/Regex.kt index 1fba8b7b46b..0ffc7c2d0d8 100644 --- a/kotlin-native/runtime/src/main/kotlin/kotlin/text/Regex.kt +++ b/kotlin-native/runtime/src/main/kotlin/kotlin/text/Regex.kt @@ -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 diff --git a/libraries/stdlib/api/js-v1/kotlin.text.kt b/libraries/stdlib/api/js-v1/kotlin.text.kt index 1228d5a84b0..2c14239d628 100644 --- a/libraries/stdlib/api/js-v1/kotlin.text.kt +++ b/libraries/stdlib/api/js-v1/kotlin.text.kt @@ -1409,10 +1409,18 @@ public final class Regex { public final fun findAll(input: kotlin.CharSequence, startIndex: kotlin.Int = ...): kotlin.sequences.Sequence + @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 diff --git a/libraries/stdlib/api/js/kotlin.text.kt b/libraries/stdlib/api/js/kotlin.text.kt index 1228d5a84b0..2c14239d628 100644 --- a/libraries/stdlib/api/js/kotlin.text.kt +++ b/libraries/stdlib/api/js/kotlin.text.kt @@ -1409,10 +1409,18 @@ public final class Regex { public final fun findAll(input: kotlin.CharSequence, startIndex: kotlin.Int = ...): kotlin.sequences.Sequence + @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 diff --git a/libraries/stdlib/common/src/kotlin/TextH.kt b/libraries/stdlib/common/src/kotlin/TextH.kt index 4f0741037b1..bfd27b48e00 100644 --- a/libraries/stdlib/common/src/kotlin/TextH.kt +++ b/libraries/stdlib/common/src/kotlin/TextH.kt @@ -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 diff --git a/libraries/stdlib/js/src/kotlin/text/regex.kt b/libraries/stdlib/js/src/kotlin/text/regex.kt index fcbba68fd59..2523ca10ae6 100644 --- a/libraries/stdlib/js/src/kotlin/text/regex.kt +++ b/libraries/stdlib/js/src/kotlin/text/regex.kt @@ -20,6 +20,8 @@ public actual enum class RegexOption(val value: String) { MULTILINE("m") } +private fun Iterable.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 = 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 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 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. * diff --git a/libraries/stdlib/jvm/src/kotlin/text/regex/Regex.kt b/libraries/stdlib/jvm/src/kotlin/text/regex/Regex.kt index 39e54c84ac6..71feb75c494 100644 --- a/libraries/stdlib/jvm/src/kotlin/text/regex/Regex.kt +++ b/libraries/stdlib/jvm/src/kotlin/text/regex/Regex.kt @@ -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. * diff --git a/libraries/stdlib/test/text/RegexTest.kt b/libraries/stdlib/test/text/RegexTest.kt index 523252b5f21..404eb4c297e 100644 --- a/libraries/stdlib/test/text/RegexTest.kt +++ b/libraries/stdlib/test/text/RegexTest.kt @@ -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 { regex.matchAt(input, index) } + assertFailsWith { 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)) diff --git a/libraries/stdlib/wasm/src/kotlin/Text.kt b/libraries/stdlib/wasm/src/kotlin/Text.kt index d44e6d7ede1..dbc891de964 100644 --- a/libraries/stdlib/wasm/src/kotlin/Text.kt +++ b/libraries/stdlib/wasm/src/kotlin/Text.kt @@ -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]. * diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index b5929de48eb..a0c2ebdc94c 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -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;