[JS] Support named capture groups in Regex #KT-51775
This commit is contained in:
committed by
Space
parent
cf79752c14
commit
c3f5d03b36
@@ -330,6 +330,9 @@ public inline fun kotlin.CharSequence.forEach(action: (kotlin.Char) -> kotlin.Un
|
||||
|
||||
public inline fun kotlin.CharSequence.forEachIndexed(action: (index: kotlin.Int, kotlin.Char) -> kotlin.Unit): kotlin.Unit
|
||||
|
||||
@kotlin.SinceKotlin(version = "1.7")
|
||||
public operator fun kotlin.text.MatchGroupCollection.get(name: kotlin.String): kotlin.text.MatchGroup?
|
||||
|
||||
@kotlin.internal.InlineOnly
|
||||
public inline fun kotlin.CharSequence.getOrElse(index: kotlin.Int, defaultValue: (kotlin.Int) -> kotlin.Char): kotlin.Char
|
||||
|
||||
|
||||
@@ -330,6 +330,9 @@ public inline fun kotlin.CharSequence.forEach(action: (kotlin.Char) -> kotlin.Un
|
||||
|
||||
public inline fun kotlin.CharSequence.forEachIndexed(action: (index: kotlin.Int, kotlin.Char) -> kotlin.Unit): kotlin.Unit
|
||||
|
||||
@kotlin.SinceKotlin(version = "1.7")
|
||||
public operator fun kotlin.text.MatchGroupCollection.get(name: kotlin.String): kotlin.text.MatchGroup?
|
||||
|
||||
@kotlin.internal.InlineOnly
|
||||
public inline fun kotlin.CharSequence.getOrElse(index: kotlin.Int, defaultValue: (kotlin.Int) -> kotlin.Char): kotlin.Char
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ package kotlin.text
|
||||
*
|
||||
* @return An instance of [MatchGroup] if the group with the specified [name] was matched or `null` otherwise.
|
||||
* @throws IllegalArgumentException if there is no group with the specified [name] defined in the regex pattern.
|
||||
* @throws UnsupportedOperationException if getting named groups isn't supported on the current platform.
|
||||
* @throws UnsupportedOperationException if this match group collection doesn't support getting match groups by name,
|
||||
* for example, when it's not supported by the current platform.
|
||||
*/
|
||||
@SinceKotlin("1.2")
|
||||
public operator fun MatchGroupCollection.get(name: String): MatchGroup? {
|
||||
|
||||
@@ -31,6 +31,23 @@ private fun Iterable<RegexOption>.toFlags(prepend: String): String = joinToStrin
|
||||
public actual data class MatchGroup(actual val value: String)
|
||||
|
||||
|
||||
/**
|
||||
* Returns a named group with the specified [name].
|
||||
*
|
||||
* @return An instance of [MatchGroup] if the group with the specified [name] was matched or `null` otherwise.
|
||||
* @throws IllegalArgumentException if there is no group with the specified [name] defined in the regex pattern.
|
||||
* @throws UnsupportedOperationException if this match group collection doesn't support getting match groups by name,
|
||||
* for example, when it's not supported by the current platform.
|
||||
*/
|
||||
@SinceKotlin("1.7")
|
||||
public operator fun MatchGroupCollection.get(name: String): MatchGroup? {
|
||||
val namedGroups = this as? MatchNamedGroupCollection
|
||||
?: throw UnsupportedOperationException("Retrieving groups by name is not supported on this platform.")
|
||||
|
||||
return namedGroups[name]
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a compiled regular expression.
|
||||
* Provides functions to match strings in text with a pattern, replace the found occurrences and split text around matches.
|
||||
@@ -148,18 +165,17 @@ public actual class Regex actual constructor(pattern: String, options: Set<Regex
|
||||
/**
|
||||
* 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
|
||||
* The replacement string may contain references to the captured groups during a match. Occurrences of `${name}` or `$index`
|
||||
* in the replacement string will be substituted with the subsequences corresponding to the captured groups with the specified name or index.
|
||||
* In case of `$index`, the first digit after '$' is always treated as a 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.
|
||||
* In case of `${name}`, the `name` can consist of latin letters 'a'..'z' and 'A'..'Z', or digits '0'..'9'. The first character must be
|
||||
* a letter.
|
||||
*
|
||||
* 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 referring named capturing groups by name is currently not supported in Kotlin/JS.
|
||||
* However, you can still refer them by index.
|
||||
*
|
||||
* @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
|
||||
@@ -202,18 +218,17 @@ public actual class Regex actual constructor(pattern: String, options: Set<Regex
|
||||
/**
|
||||
* 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
|
||||
* The replacement string may contain references to the captured groups during a match. Occurrences of `${name}` or `$index`
|
||||
* in the replacement string will be substituted with the subsequences corresponding to the captured groups with the specified name or index.
|
||||
* In case of `$index`, the first digit after '$' is always treated as a 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.
|
||||
* In case of `${name}`, the `name` can consist of latin letters 'a'..'z' and 'A'..'Z', or digits '0'..'9'. The first character must be
|
||||
* a letter.
|
||||
*
|
||||
* 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 referring named capturing groups by name is not supported currently in Kotlin/JS.
|
||||
* However, you can still refer them by index.
|
||||
*
|
||||
* @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
|
||||
@@ -338,10 +353,29 @@ private fun RegExp.findNext(input: String, from: Int, nextPattern: RegExp): Matc
|
||||
override val value: String
|
||||
get() = match[0]!!
|
||||
|
||||
override val groups: MatchGroupCollection = object : MatchGroupCollection, AbstractCollection<MatchGroup?>() {
|
||||
override val groups: MatchGroupCollection = object : MatchNamedGroupCollection, AbstractCollection<MatchGroup?>() {
|
||||
override val size: Int get() = match.length
|
||||
override fun iterator(): Iterator<MatchGroup?> = indices.asSequence().map { this[it] }.iterator()
|
||||
override fun get(index: Int): MatchGroup? = match[index]?.let { MatchGroup(it) }
|
||||
|
||||
override fun get(name: String): MatchGroup? {
|
||||
// An object of named capturing groups whose keys are the names and values are the capturing groups
|
||||
// or undefined if no named capturing groups were defined.
|
||||
val groups = match.asDynamic().groups
|
||||
?: throw IllegalArgumentException("Capturing group with name {$name} does not exist. No named capturing group was defined in Regex")
|
||||
|
||||
// If the match was successful but the group specified failed to match any part of the input sequence,
|
||||
// the associated value is 'undefined'. Value for a non-existent key is also 'undefined'. Thus, explicitly check if the key exists.
|
||||
if (!hasOwnPrototypeProperty(groups, name))
|
||||
throw IllegalArgumentException("Capturing group with name {$name} does not exist")
|
||||
|
||||
val value = groups[name]
|
||||
return if (value == undefined) null else MatchGroup(value as String)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasOwnPrototypeProperty(o: Any?, name: String): Boolean {
|
||||
return js("Object").prototype.hasOwnProperty.call(o, name).unsafeCast<Boolean>()
|
||||
}
|
||||
|
||||
|
||||
@@ -379,7 +413,7 @@ private fun RegExp.findNext(input: String, from: Int, nextPattern: RegExp): Matc
|
||||
// The same code from K/N Regex.kt
|
||||
private fun substituteGroupRefs(match: MatchResult, replacement: String): String {
|
||||
var index = 0
|
||||
val result = StringBuilder(replacement.length)
|
||||
val result = StringBuilder()
|
||||
|
||||
while (index < replacement.length) {
|
||||
val char = replacement[index++]
|
||||
@@ -392,20 +426,32 @@ private fun substituteGroupRefs(match: MatchResult, replacement: String): String
|
||||
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] == '{') {
|
||||
val endIndex = replacement.readGroupName(++index)
|
||||
|
||||
if (replacement[index] !in '0'..'9')
|
||||
throw IllegalArgumentException("Invalid capturing group reference")
|
||||
if (index == endIndex)
|
||||
throw IllegalArgumentException("Named capturing group reference should have a non-empty name")
|
||||
if (endIndex == replacement.length || replacement[endIndex] != '}')
|
||||
throw IllegalArgumentException("Named capturing group reference is missing trailing '}'")
|
||||
|
||||
val endIndex = replacement.readGroupIndex(index, match.groupValues.size)
|
||||
val groupIndex = replacement.substring(index, endIndex).toInt()
|
||||
val groupName = replacement.substring(index, endIndex)
|
||||
|
||||
if (groupIndex >= match.groupValues.size)
|
||||
throw IndexOutOfBoundsException("Group with index $groupIndex does not exist")
|
||||
result.append(match.groups[groupName]?.value ?: "")
|
||||
index = endIndex + 1 // skip past '}'
|
||||
} else {
|
||||
if (replacement[index] !in '0'..'9')
|
||||
throw IllegalArgumentException("Invalid capturing group reference")
|
||||
|
||||
result.append(match.groupValues[groupIndex])
|
||||
index = endIndex
|
||||
val groups = match.groups
|
||||
val endIndex = replacement.readGroupIndex(index, groups.size)
|
||||
val groupIndex = replacement.substring(index, endIndex).toInt()
|
||||
|
||||
if (groupIndex >= groups.size)
|
||||
throw IndexOutOfBoundsException("Group with index $groupIndex does not exist")
|
||||
|
||||
result.append(groups[groupIndex]?.value ?: "")
|
||||
index = endIndex
|
||||
}
|
||||
} else {
|
||||
result.append(char)
|
||||
}
|
||||
@@ -413,6 +459,22 @@ private fun substituteGroupRefs(match: MatchResult, replacement: String): String
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
// The name must be a legal JavaScript identifier. See https://262.ecma-international.org/5.1/#sec-7.6
|
||||
// Don't try to validate the referenced group name as it may be time-consuming.
|
||||
// If the name is invalid, it won't be found in `match.groups` anyway and will throw.
|
||||
// Group names in the target Regex are validated at creation time.
|
||||
private fun String.readGroupName(startIndex: Int): Int {
|
||||
var index = startIndex
|
||||
while (index < length) {
|
||||
if (this[index] == '}') {
|
||||
break
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
private fun String.readGroupIndex(startIndex: Int, groupCount: Int): Int {
|
||||
// at least one digit after '$' is always captured
|
||||
var index = startIndex + 1
|
||||
|
||||
@@ -27,7 +27,6 @@ public actual val isFloat32RangeEnforced: Boolean = false
|
||||
|
||||
actual val supportsSuppressedExceptions: Boolean get() = true
|
||||
|
||||
// TODO: implement named group reference in replacement expression
|
||||
public actual val supportsNamedCapturingGroup: Boolean get() = false
|
||||
public actual val supportsNamedCapturingGroup: Boolean get() = true
|
||||
|
||||
public actual val regexSplitUnicodeCodePointHandling: Boolean get() = true
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 test.text
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
class RegexJsTest {
|
||||
@Test
|
||||
fun replace() {
|
||||
// js capturing group name can contain Unicode letters, $, _, and digits (0-9), but may not start with a digit.
|
||||
// jvm capturing group name can contain a-z, A-Z, and 0-9, but may not start with a digit.
|
||||
// make sure reference to capturing group name in K/JS Regex.replace(input, replacement) obeys K/JVM rules
|
||||
val input = "123-456"
|
||||
Regex("(?<first_part>\\d+)-(?<second_part>\\d+)").let { regex ->
|
||||
assertEquals("123/456", regex.replace(input, "$1/$2"))
|
||||
assertEquals("123/456", regex.replaceFirst(input, "$1/$2"))
|
||||
|
||||
assertEquals("123/456", regex.replace(input, "\${first_part}/\${second_part}"))
|
||||
assertEquals("123/456", regex.replaceFirst(input, "\${first_part}/\${second_part}"))
|
||||
}
|
||||
Regex("(?<\$first>\\d+)-(?<\$second>\\d+)").let { regex ->
|
||||
assertEquals("123/456", regex.replace(input, "\${\$first}/\${\$second}"))
|
||||
assertEquals("123/456", regex.replaceFirst(input, "\${\$first}/\${\$second}"))
|
||||
|
||||
assertFailsWith<IllegalArgumentException> { regex.replace(input, "\${first}/\${second}") }
|
||||
assertFailsWith<IllegalArgumentException> { regex.replaceFirst(input, "\${first}/\${second}") }
|
||||
}
|
||||
Regex("(?<first>\\d+)-(?<second>\\d+)").let { regex ->
|
||||
assertFailsWith<IllegalArgumentException> { regex.replace(input, "\${\$first}/\${\$second}") }
|
||||
assertFailsWith<IllegalArgumentException> { regex.replaceFirst(input, "\${\$first}/\${\$second}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,10 +160,10 @@ internal constructor(private val nativePattern: Pattern) : Serializable {
|
||||
*
|
||||
* The replacement string may contain references to the captured groups during a match. Occurrences of `${name}` or `$index`
|
||||
* in the replacement string will be substituted with the subsequences corresponding to the captured groups with the specified name or index.
|
||||
* In case of `$index` the first digit after '$' is always treated as part of group reference. Subsequent digits are incorporated
|
||||
* In case of `$index`, the first digit after '$' is always treated as a 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.
|
||||
* In case of `${name}` the `name` can consist of latin letters 'a'..'z' and 'A'..'Z', or digits '0'..'9'. The first character must be
|
||||
* In case of `${name}`, the `name` can consist of latin letters 'a'..'z' and 'A'..'Z', or digits '0'..'9'. The first character must be
|
||||
* a letter.
|
||||
*
|
||||
* Backslash character '\' can be used to include the succeeding character as a literal in the replacement string, e.g, `\$` or `\\`.
|
||||
@@ -209,10 +209,10 @@ internal constructor(private val nativePattern: Pattern) : Serializable {
|
||||
*
|
||||
* The replacement string may contain references to the captured groups during a match. Occurrences of `${name}` or `$index`
|
||||
* in the replacement string will be substituted with the subsequences corresponding to the captured groups with the specified name or index.
|
||||
* In case of `$index` the first digit after '$' is always treated as part of group reference. Subsequent digits are incorporated
|
||||
* In case of `$index`, the first digit after '$' is always treated as a 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.
|
||||
* In case of `${name}` the `name` can consist of latin letters 'a'..'z' and 'A'..'Z', or digits '0'..'9'. The first character must be
|
||||
* In case of `${name}`, the `name` can consist of latin letters 'a'..'z' and 'A'..'Z', or digits '0'..'9'. The first character must be
|
||||
* a letter.
|
||||
*
|
||||
* Backslash character '\' can be used to include the succeeding character as a literal in the replacement string, e.g, `\$` or `\\`.
|
||||
|
||||
@@ -36,7 +36,8 @@ public interface MatchNamedGroupCollection : MatchGroupCollection {
|
||||
* Returns a named group with the specified [name].
|
||||
* @return An instance of [MatchGroup] if the group with the specified [name] was matched or `null` otherwise.
|
||||
* @throws IllegalArgumentException if there is no group with the specified [name] defined in the regex pattern.
|
||||
* @throws UnsupportedOperationException if getting named groups isn't supported on the current platform.
|
||||
* @throws UnsupportedOperationException if this match group collection doesn't support getting match groups by name,
|
||||
* for example, when it's not supported by the current platform.
|
||||
*/
|
||||
public operator fun get(name: String): MatchGroup?
|
||||
}
|
||||
|
||||
@@ -155,6 +155,68 @@ class RegexTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun matchNamedGroups() {
|
||||
val regex = "\\b(?<city>[A-Za-z\\s]+),\\s(?<state>[A-Z]{2}):\\s(?<areaCode>[0-9]{3})\\b".toRegex()
|
||||
val input = "Coordinates: Austin, TX: 123"
|
||||
|
||||
regex.find(input)!!.let { match ->
|
||||
assertEquals(listOf("Austin, TX: 123", "Austin", "TX", "123"), match.groupValues)
|
||||
|
||||
if (supportsNamedCapturingGroup) {
|
||||
val namedGroups = match.groups as MatchNamedGroupCollection
|
||||
assertEquals(4, namedGroups.size)
|
||||
assertEquals("Austin", namedGroups["city"]?.value)
|
||||
assertEquals("TX", namedGroups["state"]?.value)
|
||||
assertEquals("123", namedGroups["areaCode"]?.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun matchOptionalNamedGroup() {
|
||||
if (!supportsNamedCapturingGroup) return
|
||||
|
||||
"(?<hi>hi)|(?<bye>bye)".toRegex(RegexOption.IGNORE_CASE).let { regex ->
|
||||
val hiMatch = regex.find("Hi!")!!
|
||||
val hiGroups = hiMatch.groups as MatchNamedGroupCollection
|
||||
assertEquals(3, hiGroups.size)
|
||||
assertEquals("Hi", hiGroups["hi"]?.value)
|
||||
assertEquals(null, hiGroups["bye"])
|
||||
assertFailsWith<IllegalArgumentException> { hiGroups["hello"] }
|
||||
|
||||
val byeMatch = regex.find("bye...")!!
|
||||
val byeGroups = byeMatch.groups as MatchNamedGroupCollection
|
||||
assertEquals(3, byeGroups.size)
|
||||
assertEquals(null, byeGroups["hi"])
|
||||
assertEquals("bye", byeGroups["bye"]?.value)
|
||||
assertFailsWith<IllegalArgumentException> { byeGroups["goodbye"] }
|
||||
}
|
||||
|
||||
"(?<hi>hi)|bye".toRegex(RegexOption.IGNORE_CASE).let { regex ->
|
||||
val hiMatch = regex.find("Hi!")!!
|
||||
val hiGroups = hiMatch.groups as MatchNamedGroupCollection
|
||||
assertEquals(2, hiGroups.size)
|
||||
assertEquals("Hi", hiGroups["hi"]?.value)
|
||||
assertFailsWith<IllegalArgumentException> { hiGroups["bye"] }
|
||||
|
||||
// Named group collection consisting of a single 'null' group value
|
||||
val byeMatch = regex.find("bye...")!!
|
||||
val byeGroups = byeMatch.groups as MatchNamedGroupCollection
|
||||
assertEquals(2, byeGroups.size)
|
||||
assertEquals(null, byeGroups["hi"])
|
||||
assertFailsWith<IllegalArgumentException> { byeGroups["bye"] }
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun matchWithBackReference() {
|
||||
val regex = "(?<title>\\w+), yes \\k<title>".toRegex()
|
||||
|
||||
val match = regex.find("Do you copy? Sir, yes Sir!")!!
|
||||
assertEquals("Sir, yes Sir", match.value)
|
||||
assertEquals("Sir", (match.groups as MatchNamedGroupCollection)["title"]?.value)
|
||||
|
||||
assertNull(regex.find("Do you copy? Sir, yes I do!"))
|
||||
}
|
||||
|
||||
@Test fun matchMultiline() {
|
||||
val regex = "^[a-z]*$".toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
|
||||
val matchedValues = regex.findAll("test\n\nLine").map { it.value }.toList()
|
||||
@@ -298,7 +360,7 @@ class RegexTest {
|
||||
"123-456".let { input ->
|
||||
assertEquals("(123-456)", pattern.replace(input, "($0)"))
|
||||
assertEquals("123+456", pattern.replace(input, "$1+$2"))
|
||||
// take largest legal group number reference
|
||||
// take the largest legal group number reference
|
||||
assertEquals("1230+456", pattern.replace(input, "$10+$2"))
|
||||
assertEquals("123+456", pattern.replace(input, "$01+$2"))
|
||||
// js refers to named capturing groups with "$<name>" syntax
|
||||
@@ -308,6 +370,11 @@ class RegexTest {
|
||||
// missing trailing '}'
|
||||
assertFailsWith<IllegalArgumentException>("\${first+\${second}") { pattern.replace(input, "\${first+\${second}") }
|
||||
assertFailsWith<IllegalArgumentException>("\${first}+\${second") { pattern.replace(input, "\${first}+\${second") }
|
||||
|
||||
// non-existent group name
|
||||
assertFailsWith<IllegalArgumentException>("\${first}+\${second}+\$third") {
|
||||
pattern.replace(input, "\${first}+\${second}+\$third")
|
||||
}
|
||||
}
|
||||
|
||||
"123-456-789-012".let { input ->
|
||||
@@ -317,6 +384,18 @@ class RegexTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun replaceWithNamedOptionalGroups() {
|
||||
if (!supportsNamedCapturingGroup) return
|
||||
|
||||
val regex = "(?<hi>hi)|(?<bye>bye)".toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
assertEquals("[Hi, ]gh wall", regex.replace("High wall", "[$1, $2]"))
|
||||
assertEquals("[Hi, ]gh wall", regex.replace("High wall", "[\${hi}, \${bye}]"))
|
||||
|
||||
assertEquals("Good[, bye], Mr. Holmes", regex.replace("Goodbye, Mr. Holmes", "[$1, $2]"))
|
||||
assertEquals("Good[, bye], Mr. Holmes", regex.replace("Goodbye, Mr. Holmes", "[\${hi}, \${bye}]"))
|
||||
}
|
||||
|
||||
@Test fun replaceEvaluator() {
|
||||
val input = "/12/456/7890/"
|
||||
val pattern = "\\d+".toRegex()
|
||||
|
||||
Reference in New Issue
Block a user