diff --git a/js/js.libraries/src/core/regex.kt b/js/js.libraries/src/core/regex.kt index f1e83c1017f..9c2b58833b9 100644 --- a/js/js.libraries/src/core/regex.kt +++ b/js/js.libraries/src/core/regex.kt @@ -216,8 +216,8 @@ private fun RegExp.findNext(input: String, from: Int): MatchResult? { get() { if (groupValues_ == null) { groupValues_ = object : java.util.AbstractList() { - override val size: Int get() = match.size - 1 - override fun get(index: Int): String = match[index + 1] ?: "" + override val size: Int get() = match.size + override fun get(index: Int): String = match[index] ?: "" } } return groupValues_!! @@ -225,4 +225,19 @@ private fun RegExp.findNext(input: String, from: Int): MatchResult? { override fun next(): MatchResult? = this@findNext.findNext(input, if (range.isEmpty()) range.start + 1 else range.end + 1) } -} \ No newline at end of file +} + +// TODO: Move into MatchResult after KT-4124 is implemented +public class Destructured internal constructor(public val match: MatchResult) { + public operator inline fun component1(): String = match.groupValues[1] + public operator inline fun component2(): String = match.groupValues[2] + public operator inline fun component3(): String = match.groupValues[3] + public operator inline fun component4(): String = match.groupValues[4] + public operator inline fun component5(): String = match.groupValues[5] + public operator inline fun component6(): String = match.groupValues[6] + public operator inline fun component7(): String = match.groupValues[7] + public operator inline fun component8(): String = match.groupValues[8] + public operator inline fun component9(): String = match.groupValues[9] + public operator inline fun component10(): String = match.groupValues[10] + public fun toList(): List = match.groupValues.drop(1) +} diff --git a/libraries/stdlib/src/kotlin/text/regex/Regex.kt b/libraries/stdlib/src/kotlin/text/regex/Regex.kt index 1773809b280..0cf44cf06cc 100644 --- a/libraries/stdlib/src/kotlin/text/regex/Regex.kt +++ b/libraries/stdlib/src/kotlin/text/regex/Regex.kt @@ -56,19 +56,58 @@ public interface MatchResult { */ public val groups: MatchGroupCollection /** - * A list of matched indexed group values except zeroth group corresponding to the entire match. + * A list of matched indexed group values. * - * Unlike [groups] collection this list contains exactly `groupCount` items where `groupCount` is the count of groups in the regular expression. - * Groups are are indexed from *zero* to the `groupCount - 1`. + * 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 was not match captured by that group, - * corresponding item in [groupValues] would be an empty string. + * 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. */ public val groupValues: List + /** + * An instance of [MatchResult.Destructured] wrapper providing components for destructuring assignment of group values. + * + * component1 corresponds to the value of the first group, component2 — of the second, and so on. + * + * @sample: + * ``` + * val (name, phone) = Regex("(\\w+) (\\d+)").match(inputString)!!.destructured + * ``` + */ + public val destructured: Destructured get() = Destructured(this) + /** Returns a new [MatchResult] with the results for the next match, starting at the position * at which the last match ended (at the character after the last matched character). */ public fun next(): MatchResult? -} + /** + * Provides components for destructuring assignment of group values. + * + * [component1] corresponds to the value of the first group, [component2] — of the second, and so on. + * + * If the group in the regular expression is optional and there were no match captured by that group, + * corresponding component value is an empty string. + */ + @Suppress("NOTHING_TO_INLINE") + @kotlin.jvm.JvmVersion + public class Destructured internal constructor(public val match: MatchResult) { + public operator inline fun component1(): String = match.groupValues[1] + public operator inline fun component2(): String = match.groupValues[2] + public operator inline fun component3(): String = match.groupValues[3] + public operator inline fun component4(): String = match.groupValues[4] + public operator inline fun component5(): String = match.groupValues[5] + public operator inline fun component6(): String = match.groupValues[6] + public operator inline fun component7(): String = match.groupValues[7] + public operator inline fun component8(): String = match.groupValues[8] + public operator inline fun component9(): String = match.groupValues[9] + public operator inline fun component10(): String = match.groupValues[10] + /** + * Returns destructured group values as a list of strings. + * First value in the returned list corresponds to the value of the first group, and so on. + */ + public fun toList(): List = match.groupValues.subList(1, match.groupValues.size) + } +} \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt b/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt index 812138a1ba6..d6bbffe7887 100644 --- a/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt +++ b/libraries/stdlib/src/kotlin/text/regex/RegexJVM.kt @@ -263,8 +263,8 @@ private class MatcherMatchResult(private val matcher: Matcher, private val input get() { if (groupValues_ == null) { groupValues_ = object : AbstractList() { - override val size: Int get() = matchResult.groupCount() - override fun get(index: Int): String = matchResult.group(index + 1) ?: "" + override val size: Int get() = matchResult.groupCount() + 1 + override fun get(index: Int): String = matchResult.group(index) ?: "" } } return groupValues_!! diff --git a/libraries/stdlib/test/text/RegexTest.kt b/libraries/stdlib/test/text/RegexTest.kt index f61ea4e56a2..5abe871a8a4 100644 --- a/libraries/stdlib/test/text/RegexTest.kt +++ b/libraries/stdlib/test/text/RegexTest.kt @@ -69,36 +69,62 @@ class RegexTest { val matches = pattern.findAll(input).toList() assertTrue(matches.all { it.groups.size == 3 }) - val m1 = matches[0] - assertEquals("1a", m1.groups[0]?.value) - assertEquals("1", m1.groups[1]?.value) - assertEquals("a", m1.groups[2]?.value) - assertEquals(listOf("1", "a"), m1.groupValues) + matches[0].let { m -> + assertEquals("1a", m.groups[0]?.value) + assertEquals("1", m.groups[1]?.value) + assertEquals("a", m.groups[2]?.value) - val m2 = matches[1] - assertEquals("2", m2.groups[1]?.value) - assertEquals("b", m2.groups[2]?.value) + assertEquals(listOf("1a", "1", "a"), m.groupValues) - assertEquals(listOf("2", "b"), m2.groupValues) + val (g1, g2) = m.destructured + assertEquals("1", g1) + assertEquals("a", g2) + assertEquals(listOf("1", "a"), m.destructured.toList()) + } + + matches[1].let { m -> + assertEquals("2b", m.groups[0]?.value) + assertEquals("2", m.groups[1]?.value) + assertEquals("b", m.groups[2]?.value) + + assertEquals(listOf("2b", "2", "b"), m.groupValues) + + val (g1, g2) = m.destructured + assertEquals("2", g1) + assertEquals("b", g2) + assertEquals(listOf("2", "b"), m.destructured.toList()) + } } @test fun matchOptionalGroup() { val pattern = "(hi)|(bye)".toRegex(RegexOption.IGNORE_CASE) - val m1 = pattern.find("Hi!")!! - assertEquals(3, m1.groups.size) - assertEquals("Hi", m1.groups[1]?.value) - assertEquals(null, m1.groups[2]) + pattern.find("Hi!")!!.let { m -> + assertEquals(3, m.groups.size) + assertEquals("Hi", m.groups[1]?.value) + assertEquals(null, m.groups[2]) - assertEquals(listOf("Hi", ""), m1.groupValues) + assertEquals(listOf("Hi", "Hi", ""), m.groupValues) - val m2 = pattern.find("bye...")!! - assertEquals(3, m2.groups.size) - assertEquals(null, m2.groups[1]) - assertEquals("bye", m2.groups[2]?.value) + val (g1, g2) = m.destructured + assertEquals("Hi", g1) + assertEquals("", g2) + assertEquals(listOf("Hi", ""), m.destructured.toList()) + } - assertEquals(listOf("", "bye"), m2.groupValues) + pattern.find("bye...")!!.let { m -> + assertEquals(3, m.groups.size) + assertEquals(null, m.groups[1]) + assertEquals("bye", m.groups[2]?.value) + + assertEquals(listOf("bye", "", "bye"), m.groupValues) + + val (g1, g2) = m.destructured + assertEquals("", g1) + assertEquals("bye", g2) + assertEquals(listOf("", "bye"), m.destructured.toList()) + } } @test fun matchMultiline() {