From da1641e0f8b69a2682b3a3441ca0bedd4271e4d4 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Wed, 8 Apr 2015 19:26:31 +0300 Subject: [PATCH] Pattern implementation: JVM and partially JS. Common tests. --- js/js.libraries/src/core/pattern.kt | 99 +++++++++++++++++++ js/js.libraries/src/core/stringsCode.kt | 5 +- .../stdlib/src/kotlin/text/regex/Pattern.kt | 87 +++++++++------- libraries/stdlib/test/text/PatternTest.kt | 78 +++++++++++++++ 4 files changed, 231 insertions(+), 38 deletions(-) create mode 100644 js/js.libraries/src/core/pattern.kt create mode 100644 libraries/stdlib/test/text/PatternTest.kt diff --git a/js/js.libraries/src/core/pattern.kt b/js/js.libraries/src/core/pattern.kt new file mode 100644 index 00000000000..139fa9ea578 --- /dev/null +++ b/js/js.libraries/src/core/pattern.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package kotlin.text + + +public enum class PatternOption(val value: String) { + IGNORE_CASE : PatternOption("i") + MULTILINE : PatternOption("m") +} + + +public data class MatchGroup(val value: String) + +public trait MatchGroupCollection : Collection { + public fun get(index: Int): MatchGroup? +} + +public trait MatchResult { + public val range: IntRange + public val value: String + public val groups: MatchGroupCollection + + public fun next(): MatchResult? +} + + +public class Pattern(public val pattern: String, options_: Set) { + + public constructor(pattern: String, vararg options: PatternOption) : this(pattern, options.toSet()) + + public val options: Set = options_.toSet() + private val nativePattern: RegExp = RegExp(pattern, options.map { it.value }.joinToString() + "g") + + + public fun matches(input: CharSequence): Boolean { + nativePattern.reset() + return nativePattern.test(input.toString()) + } + + public fun match(input: CharSequence): MatchResult? = nativePattern.findNext(input.toString(), 0) + + public fun matchAll(input: CharSequence): Sequence = sequence({ match(input) }, { match -> match.next() }) + + public fun replace(input: CharSequence, replacement: String): String = input.toString().nativeReplace(nativePattern, replacement) + public fun replace(input: CharSequence, evaluator: (MatchResult) -> String): String = TODO + + public fun split(input: CharSequence, limit: Int = 0): List = TODO + + public fun toString(): String = nativePattern.toString() + + companion object { + public fun fromLiteral(literal: String): Pattern = Pattern(escape(literal)) + public fun escape(literal: String): String = TODO + public fun escapeReplacement(literal: String): String = literal.nativeReplace(RegExp("\\$", "g"), "$$$$") + } +} + + +private fun RegExp.findNext(input: String, from: Int): MatchResult? { + this.lastIndex = from + val match = exec(input) + if (match == null) return null + val reMatch = match as RegExpMatch + val range = reMatch.index..lastIndex-1 + + return object : MatchResult { + override val range: IntRange = range + override val value: String + get() = match[0]!! + + override val groups: MatchGroupCollection = object : MatchGroupCollection { + override fun size(): Int = match.size() + override fun isEmpty(): Boolean = size() == 0 + + override fun contains(o: Any?): Boolean = this.any { it == o } + override fun containsAll(c: Collection): Boolean = c.all({contains(it)}) + + override fun iterator(): Iterator = indices.sequence().map { this[it] }.iterator() + + override fun get(index: Int): MatchGroup? = match[index]?.let { MatchGroup(it) } + } + + override fun next(): MatchResult? = this@findNext.findNext(input, range.end + 1) + } +} \ No newline at end of file diff --git a/js/js.libraries/src/core/stringsCode.kt b/js/js.libraries/src/core/stringsCode.kt index 06f21436f1f..34bf5913133 100644 --- a/js/js.libraries/src/core/stringsCode.kt +++ b/js/js.libraries/src/core/stringsCode.kt @@ -80,5 +80,8 @@ public inline fun String.decapitalize(): String { } -public fun String.replace(oldValue: String, newValue: String): String = nativeReplace(oldValue, newValue.nativeReplace("$", "$$")) +public fun String.replace(oldValue: String, newValue: String): String = + nativeReplace(new RegExp(Pattern.escape(oldValue),"g"), Pattern.escapeReplacement(newValue)) +public fun String.replace(oldChar: Char, newChar: Char): String = + nativeReplace(new RegExp(Pattern.escape(oldChar.toString()),"g"), newChar.toString()) diff --git a/libraries/stdlib/src/kotlin/text/regex/Pattern.kt b/libraries/stdlib/src/kotlin/text/regex/Pattern.kt index 1d597d232b1..41f900aecaf 100644 --- a/libraries/stdlib/src/kotlin/text/regex/Pattern.kt +++ b/libraries/stdlib/src/kotlin/text/regex/Pattern.kt @@ -33,18 +33,18 @@ public fun fromInt(value: Int, allValues: Array): Set = allValues.filter({ value and it.mask == it.value }).toSet() -public enum class PatternOptions(override val value: Int, override val mask: Int = value) : FlagEnum { +public enum class PatternOption(override val value: Int, override val mask: Int = value) : FlagEnum { // common - IGNORE_CASE : PatternOptions(NativePattern.CASE_INSENSITIVE) - MULTILINE : PatternOptions(NativePattern.MULTILINE) + IGNORE_CASE : PatternOption(NativePattern.CASE_INSENSITIVE) + MULTILINE : PatternOption(NativePattern.MULTILINE) //jvm-specific - LITERAL : PatternOptions(NativePattern.LITERAL) - UNICODE_CASE: PatternOptions(NativePattern.UNICODE_CASE) - UNIX_LINES: PatternOptions(NativePattern.UNIX_LINES) - COMMENTS: PatternOptions(NativePattern.COMMENTS) - DOTALL: PatternOptions(NativePattern.DOTALL) - CANON_EQ: PatternOptions(NativePattern.CANON_EQ) + LITERAL : PatternOption(NativePattern.LITERAL) + UNICODE_CASE: PatternOption(NativePattern.UNICODE_CASE) + UNIX_LINES: PatternOption(NativePattern.UNIX_LINES) + COMMENTS: PatternOption(NativePattern.COMMENTS) + DOT_MATCHES_ALL: PatternOption(NativePattern.DOTALL) + CANON_EQ: PatternOption(NativePattern.CANON_EQ) } /* in JS @@ -54,26 +54,31 @@ public enum class PatternOptions(val value: String) { } */ -public trait MatchGroup { - public val range: IntRange - public val value: String +public data class MatchGroup(val value: String, val range: IntRange) + +public trait MatchGroupCollection : Collection { + public fun get(index: Int): MatchGroup? } -public trait MatchResult : MatchGroup { - public val groups: List +public trait MatchResult { + public val range: IntRange + public val value: String + public val groups: MatchGroupCollection + // TODO: Should we have groupCount (equals groups.size()-1)? + public fun next(): MatchResult? } public class Pattern ( /* visibility? */ val nativePattern: NativePattern) { - public constructor(pattern: String, options: Set): this(NativePattern.compile(pattern, options.toInt())) - public constructor(pattern: String, vararg options: PatternOptions) : this(pattern, options.toSet()) + public constructor(pattern: String, options: Set): this(NativePattern.compile(pattern, options.toInt())) + public constructor(pattern: String, vararg options: PatternOption) : this(pattern, options.toSet()) public val pattern: String get() = nativePattern.pattern() - public val options: Set = fromInt(nativePattern.flags(), PatternOptions.values()) + public val options: Set = fromInt(nativePattern.flags(), PatternOption.values()) public fun matches(input: CharSequence): Boolean = nativePattern.matcher(input).matches() @@ -86,16 +91,19 @@ public class Pattern ( /* visibility? */ val nativePattern: NativePattern) { public fun split(input: CharSequence, limit: Int = 0): List = nativePattern.split(input, limit).asList() // TODO: require(limit>=0) + public override fun toString(): String = nativePattern.toString() + companion object { - public fun fromLiteral(literal: String): Pattern = Pattern(literal, PatternOptions.LITERAL) + public fun fromLiteral(literal: String): Pattern = Pattern(literal, PatternOption.LITERAL) public fun escape(literal: String): String = NativePattern.quote(literal) public fun escapeReplacement(literal: String): String = Matcher.quoteReplacement(literal) } } -public fun String.toPattern(vararg options: PatternOptions): Pattern = Pattern(this, *options) -public fun String.toPattern(options: Set): Pattern = Pattern(this, options) +public fun String.toPattern(vararg options: PatternOption): Pattern = Pattern(this, *options) +public fun String.toPattern(options: Set): Pattern = Pattern(this, options) + @@ -103,34 +111,39 @@ private fun Matcher.findNext(from: Int): MatchResult? { if (!find(from)) return null - var matchResult = this as java.util.regex.MatchResult + // TODO: If we need MatchResult to be thread safe we must lock everything, or call this.toMatchResult early + var matchResult: java.util.regex.MatchResult = this return object: MatchResult { override val range: IntRange - get() = matchResult.start()..matchResult.end()-1 + get() = matchResult.range() override val value: String get() = matchResult.group() - override val groups: List by Delegates.lazy { - // TODO: wrap - val groups = ArrayList(matchResult.groupCount()) - for (groupIndex in 1..groupCount()) { - val range = matchResult.start(groupIndex)..matchResult.end(groupIndex)-1 - if (range.start >= 0) - groups.add(object: MatchGroup { - override val range: IntRange = range - override val value: String - get() = matchResult.group(groupIndex) - }) + override val groups: MatchGroupCollection = object : MatchGroupCollection { + override fun size(): Int = matchResult.groupCount() + 1 + override fun isEmpty(): Boolean = false + override fun contains(o: Any?): Boolean = o is MatchGroup? && this.any({ it == o }) + override fun containsAll(c: Collection): Boolean = c.all({contains(it)}) + + override fun iterator(): Iterator = indices.sequence().map { this[it] }.iterator() + override fun get(index: Int): MatchGroup? { + val range = matchResult.range(index) + return if (range.start >= 0) + MatchGroup(matchResult.group(index), range) else - groups.add(null) + null } - groups } + override fun next(): MatchResult? { - matchResult = this@findNext.toMatchResult() - return this@findNext.findNext(matchResult.end()) // TODO: advance next + if (this@findNext === matchResult) + matchResult = this@findNext.toMatchResult() + return this@findNext.findNext(matchResult.end()) } } } + +private fun java.util.regex.MatchResult.range(): IntRange = start()..end()-1 +private fun java.util.regex.MatchResult.range(groupIndex: Int): IntRange = start(groupIndex)..end(groupIndex)-1 diff --git a/libraries/stdlib/test/text/PatternTest.kt b/libraries/stdlib/test/text/PatternTest.kt new file mode 100644 index 00000000000..f5428553a7c --- /dev/null +++ b/libraries/stdlib/test/text/PatternTest.kt @@ -0,0 +1,78 @@ +package test.text + +import kotlin.test.* +import kotlin.text.* +import org.junit.Test as test + +class PatternTest { + + test fun matchResult() { + val p = "\\d+".toPattern() + val input = "123 456 789" + + val first = p.match(input) + assertTrue(first != null); first!! + assertEquals("123", first.value) + + val second1 = first.next()!! + val second2 = first.next()!! + + assertEquals("456", second1.value) + assertEquals(second1.value, second2.value) + + val last = second1.next()!! + assertEquals("789", last.value) + + val noMatch = last.next() + assertEquals(null, noMatch) + } + + test fun matchSequence() { + val input = "123 456 789" + val pattern = "\\d+".toPattern() + + val matches = pattern.matchAll(input) + val values = matches.map { it.value } + val expected = listOf("123", "456", "789") + assertEquals(expected, values.toList()) + assertEquals(expected, values.toList(), "running match sequence second time") + + assertEquals(listOf(0..2, 4..6, 8..10), matches.map { it.range }.toList()) + } + + test fun matchGroups() { + val input = "1a 2b 3c" + val pattern = "(\\d)(\\w)".toPattern() + + val matches = pattern.matchAll(input).toList() + assertTrue(matches.all { it.groups.size() == 3 }) + val m1 = matches[0] + assertEquals("1a", m1.groups[0]?.value) + assertEquals(0..1, m1.groups[0]?.range) + assertEquals("1", m1.groups[1]?.value) + assertEquals(0..0, m1.groups[1]?.range) + assertEquals("a", m1.groups[2]?.value) + assertEquals(1..1, m1.groups[2]?.range) + + val m2 = matches[1] + assertEquals("2", m2.groups[1]?.value) + assertEquals(3..3, m2.groups[1]?.range) + assertEquals("b", m2.groups[2]?.value) + assertEquals(4..4, m2.groups[2]?.range) + } + + test fun matchOptionalGroup() { + val pattern = "(hi)|(bye)".toPattern(PatternOption.IGNORE_CASE) + + val m1 = pattern.match("Hi!")!! + assertEquals(3, m1.groups.size()) + assertEquals("Hi", m1.groups[1]?.value) + assertEquals(null, m1.groups[2]) + + val m2 = pattern.match("bye...")!! + assertEquals(3, m2.groups.size()) + assertEquals(null, m2.groups[1]) + assertEquals("bye", m2.groups[2]?.value) + } + +} \ No newline at end of file