[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
|
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
|
@kotlin.internal.InlineOnly
|
||||||
public inline fun kotlin.CharSequence.getOrElse(index: kotlin.Int, defaultValue: (kotlin.Int) -> kotlin.Char): kotlin.Char
|
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
|
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
|
@kotlin.internal.InlineOnly
|
||||||
public inline fun kotlin.CharSequence.getOrElse(index: kotlin.Int, defaultValue: (kotlin.Int) -> kotlin.Char): kotlin.Char
|
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.
|
* @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 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")
|
@SinceKotlin("1.2")
|
||||||
public operator fun MatchGroupCollection.get(name: String): MatchGroup? {
|
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)
|
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.
|
* Represents a compiled regular expression.
|
||||||
* Provides functions to match strings in text with a pattern, replace the found occurrences and split text around matches.
|
* 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.
|
* 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`
|
* 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 index.
|
* in the replacement string will be substituted with the subsequences corresponding to the captured groups with the specified name or 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
|
* 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.
|
* 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 `\\`.
|
* 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.
|
* [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 input the char sequence to find matches of this regular expression in
|
||||||
* @param replacement the expression to replace found matches with
|
* @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
|
* @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.
|
* 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`
|
* 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 index.
|
* in the replacement string will be substituted with the subsequences corresponding to the captured groups with the specified name or 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
|
* 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.
|
* 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 `\\`.
|
* 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.
|
* [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 input the char sequence to find a match of this regular expression in
|
||||||
* @param replacement the expression to replace the found match with
|
* @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
|
* @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
|
override val value: String
|
||||||
get() = match[0]!!
|
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 val size: Int get() = match.length
|
||||||
override fun iterator(): Iterator<MatchGroup?> = indices.asSequence().map { this[it] }.iterator()
|
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(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
|
// The same code from K/N Regex.kt
|
||||||
private fun substituteGroupRefs(match: MatchResult, replacement: String): String {
|
private fun substituteGroupRefs(match: MatchResult, replacement: String): String {
|
||||||
var index = 0
|
var index = 0
|
||||||
val result = StringBuilder(replacement.length)
|
val result = StringBuilder()
|
||||||
|
|
||||||
while (index < replacement.length) {
|
while (index < replacement.length) {
|
||||||
val char = replacement[index++]
|
val char = replacement[index++]
|
||||||
@@ -392,20 +426,32 @@ private fun substituteGroupRefs(match: MatchResult, replacement: String): String
|
|||||||
if (index == replacement.length)
|
if (index == replacement.length)
|
||||||
throw IllegalArgumentException("Capturing group index is missing")
|
throw IllegalArgumentException("Capturing group index is missing")
|
||||||
|
|
||||||
if (replacement[index] == '{')
|
if (replacement[index] == '{') {
|
||||||
throw IllegalArgumentException("Named capturing group reference currently is not supported")
|
val endIndex = replacement.readGroupName(++index)
|
||||||
|
|
||||||
if (replacement[index] !in '0'..'9')
|
if (index == endIndex)
|
||||||
throw IllegalArgumentException("Invalid capturing group reference")
|
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 groupName = replacement.substring(index, endIndex)
|
||||||
val groupIndex = replacement.substring(index, endIndex).toInt()
|
|
||||||
|
|
||||||
if (groupIndex >= match.groupValues.size)
|
result.append(match.groups[groupName]?.value ?: "")
|
||||||
throw IndexOutOfBoundsException("Group with index $groupIndex does not exist")
|
index = endIndex + 1 // skip past '}'
|
||||||
|
} else {
|
||||||
|
if (replacement[index] !in '0'..'9')
|
||||||
|
throw IllegalArgumentException("Invalid capturing group reference")
|
||||||
|
|
||||||
result.append(match.groupValues[groupIndex])
|
val groups = match.groups
|
||||||
index = endIndex
|
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 {
|
} else {
|
||||||
result.append(char)
|
result.append(char)
|
||||||
}
|
}
|
||||||
@@ -413,6 +459,22 @@ private fun substituteGroupRefs(match: MatchResult, replacement: String): String
|
|||||||
return result.toString()
|
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 {
|
private fun String.readGroupIndex(startIndex: Int, groupCount: Int): Int {
|
||||||
// at least one digit after '$' is always captured
|
// at least one digit after '$' is always captured
|
||||||
var index = startIndex + 1
|
var index = startIndex + 1
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ public actual val isFloat32RangeEnforced: Boolean = false
|
|||||||
|
|
||||||
actual val supportsSuppressedExceptions: Boolean get() = true
|
actual val supportsSuppressedExceptions: Boolean get() = true
|
||||||
|
|
||||||
// TODO: implement named group reference in replacement expression
|
public actual val supportsNamedCapturingGroup: Boolean get() = true
|
||||||
public actual val supportsNamedCapturingGroup: Boolean get() = false
|
|
||||||
|
|
||||||
public actual val regexSplitUnicodeCodePointHandling: 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`
|
* 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 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
|
* 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.
|
* 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.
|
* a letter.
|
||||||
*
|
*
|
||||||
* Backslash character '\' can be used to include the succeeding character as a literal in the replacement string, e.g, `\$` or `\\`.
|
* 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`
|
* 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 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
|
* 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.
|
* 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.
|
* a letter.
|
||||||
*
|
*
|
||||||
* Backslash character '\' can be used to include the succeeding character as a literal in the replacement string, e.g, `\$` or `\\`.
|
* 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].
|
* 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.
|
* @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 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?
|
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() {
|
@Test fun matchMultiline() {
|
||||||
val regex = "^[a-z]*$".toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
|
val regex = "^[a-z]*$".toRegex(setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE))
|
||||||
val matchedValues = regex.findAll("test\n\nLine").map { it.value }.toList()
|
val matchedValues = regex.findAll("test\n\nLine").map { it.value }.toList()
|
||||||
@@ -298,7 +360,7 @@ class RegexTest {
|
|||||||
"123-456".let { input ->
|
"123-456".let { input ->
|
||||||
assertEquals("(123-456)", pattern.replace(input, "($0)"))
|
assertEquals("(123-456)", pattern.replace(input, "($0)"))
|
||||||
assertEquals("123+456", pattern.replace(input, "$1+$2"))
|
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("1230+456", pattern.replace(input, "$10+$2"))
|
||||||
assertEquals("123+456", pattern.replace(input, "$01+$2"))
|
assertEquals("123+456", pattern.replace(input, "$01+$2"))
|
||||||
// js refers to named capturing groups with "$<name>" syntax
|
// js refers to named capturing groups with "$<name>" syntax
|
||||||
@@ -308,6 +370,11 @@ class RegexTest {
|
|||||||
// missing trailing '}'
|
// missing trailing '}'
|
||||||
assertFailsWith<IllegalArgumentException>("\${first+\${second}") { pattern.replace(input, "\${first+\${second}") }
|
assertFailsWith<IllegalArgumentException>("\${first+\${second}") { pattern.replace(input, "\${first+\${second}") }
|
||||||
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 ->
|
"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() {
|
@Test fun replaceEvaluator() {
|
||||||
val input = "/12/456/7890/"
|
val input = "/12/456/7890/"
|
||||||
val pattern = "\\d+".toRegex()
|
val pattern = "\\d+".toRegex()
|
||||||
|
|||||||
Reference in New Issue
Block a user