From a4784dfa782b604bb903ac7e5dec9cc69551bc50 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Wed, 8 Apr 2015 20:32:22 +0300 Subject: [PATCH] Pattern implementation for JS. --- js/js.libraries/src/core/pattern.kt | 25 ++-- js/js.libraries/src/core/string.kt | 15 ++- js/js.libraries/src/core/stringsCode.kt | 4 +- js/js.libraries/test/core/RegExpTest.kt | 2 +- .../stdlib/src/kotlin/text/regex/Pattern.kt | 115 +---------------- .../src/kotlin/text/regex/PatternJVM.kt | 121 ++++++++++++++++++ libraries/stdlib/test/text/PatternJVMTest.kt | 30 +++++ libraries/stdlib/test/text/PatternTest.kt | 8 +- 8 files changed, 174 insertions(+), 146 deletions(-) create mode 100644 libraries/stdlib/src/kotlin/text/regex/PatternJVM.kt create mode 100644 libraries/stdlib/test/text/PatternJVMTest.kt diff --git a/js/js.libraries/src/core/pattern.kt b/js/js.libraries/src/core/pattern.kt index 139fa9ea578..2194264365e 100644 --- a/js/js.libraries/src/core/pattern.kt +++ b/js/js.libraries/src/core/pattern.kt @@ -16,6 +16,8 @@ package kotlin.text +private val TODO: Nothing get() = throw java.lang.UnsupportedOperationException() + public enum class PatternOption(val value: String) { IGNORE_CASE : PatternOption("i") @@ -25,24 +27,11 @@ public enum class PatternOption(val value: String) { 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 class Pattern (pattern: String, options: Set) { - 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() + public val pattern: String = pattern + public val options: Set = options.toSet() private val nativePattern: RegExp = RegExp(pattern, options.map { it.value }.joinToString() + "g") @@ -60,7 +49,7 @@ public class Pattern(public val pattern: String, options_: Set) { public fun split(input: CharSequence, limit: Int = 0): List = TODO - public fun toString(): String = nativePattern.toString() + public override fun toString(): String = nativePattern.toString() companion object { public fun fromLiteral(literal: String): Pattern = Pattern(escape(literal)) @@ -69,6 +58,8 @@ public class Pattern(public val pattern: String, options_: Set) { } } +public fun Pattern(pattern: String, vararg options: PatternOption): Pattern = Pattern(pattern, options.toSet()) + private fun RegExp.findNext(input: String, from: Int): MatchResult? { this.lastIndex = from diff --git a/js/js.libraries/src/core/string.kt b/js/js.libraries/src/core/string.kt index 345c34b776a..92eaf7f45aa 100644 --- a/js/js.libraries/src/core/string.kt +++ b/js/js.libraries/src/core/string.kt @@ -19,17 +19,19 @@ public fun String.nativeStartsWith(s: String, position: Int): Boolean = noImpl native("endsWith") public fun String.nativeEndsWith(s: String): Boolean = noImpl +deprecated("Use split(Pattern) instead.") library("splitString") public fun String.splitWithRegex(regex: String): Array = noImpl +deprecated("Use split(Pattern) instead.") library("splitString") public fun String.splitWithRegex(regex: String, limit: Int): Array = noImpl -deprecated("Use splitWithRegex (temporary)") +deprecated("Use split(Pattern) instead.") library("splitString") public fun String.split(regex: String): Array = noImpl -deprecated("Use splitWithRegex (temporary)") +deprecated("Use split(Pattern) instead.") library("splitString") public fun String.split(regex: String, limit: Int): Array = noImpl @@ -52,12 +54,13 @@ public fun CharSequence.isEmpty(): Boolean = noImpl +// TODO: internal // native because we need to escape newValue +//native("replace") +//public fun String.nativeReplace(oldValue: String, newValue: String): String = noImpl +// TODO: internal native("replace") -private fun String.nativeReplace(oldValue: String, newValue: String): String = noImpl - -native("replace") -private fun String.nativeReplace(pattern: RegExp, replacement: String): String = noImpl +public fun String.nativeReplace(pattern: RegExp, replacement: String): String = noImpl /* diff --git a/js/js.libraries/src/core/stringsCode.kt b/js/js.libraries/src/core/stringsCode.kt index 34bf5913133..dd34298bcef 100644 --- a/js/js.libraries/src/core/stringsCode.kt +++ b/js/js.libraries/src/core/stringsCode.kt @@ -81,7 +81,7 @@ public inline fun String.decapitalize(): String { public fun String.replace(oldValue: String, newValue: String): String = - nativeReplace(new RegExp(Pattern.escape(oldValue),"g"), Pattern.escapeReplacement(newValue)) + nativeReplace(RegExp(kotlin.text.Pattern.escape(oldValue),"g"), kotlin.text.Pattern.escapeReplacement(newValue)) public fun String.replace(oldChar: Char, newChar: Char): String = - nativeReplace(new RegExp(Pattern.escape(oldChar.toString()),"g"), newChar.toString()) + nativeReplace(RegExp(kotlin.text.Pattern.escape(oldChar.toString()),"g"), newChar.toString()) diff --git a/js/js.libraries/test/core/RegExpTest.kt b/js/js.libraries/test/core/RegExpTest.kt index 179c4a86995..b503c44efff 100644 --- a/js/js.libraries/test/core/RegExpTest.kt +++ b/js/js.libraries/test/core/RegExpTest.kt @@ -60,7 +60,7 @@ class RegExpTest { val m2 = re.exec(string)!! assertEquals(array("A5D5", "A5", "D5"), m2) - assertEquals(string.indexOf(m2[0]), (m2 as RegExpMatch).index) + assertEquals(string.indexOf(m2[0]!!), (m2 as RegExpMatch).index) val noMatch = re.exec(string) assertEquals(null, noMatch) diff --git a/libraries/stdlib/src/kotlin/text/regex/Pattern.kt b/libraries/stdlib/src/kotlin/text/regex/Pattern.kt index 41f900aecaf..a9cb5657890 100644 --- a/libraries/stdlib/src/kotlin/text/regex/Pattern.kt +++ b/libraries/stdlib/src/kotlin/text/regex/Pattern.kt @@ -16,45 +16,6 @@ package kotlin.text -import java.util.ArrayList -import java.util.regex.Pattern as NativePattern -import java.util.regex.Matcher -import kotlin.properties.Delegates - -private val TODO: Nothing get() = throw UnsupportedOperationException() - -public trait FlagEnum { - public val value: Int - public val mask: Int -} -public fun Iterable.toInt(): Int = - this.fold(0, { value, option -> value or option.value }) -public fun fromInt(value: Int, allValues: Array): Set = - allValues.filter({ value and it.mask == it.value }).toSet() - - -public enum class PatternOption(override val value: Int, override val mask: Int = value) : FlagEnum { - // common - IGNORE_CASE : PatternOption(NativePattern.CASE_INSENSITIVE) - MULTILINE : PatternOption(NativePattern.MULTILINE) - - //jvm-specific - 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 -public enum class PatternOptions(val value: String) { - IGNORE_CASE : PatternOptions("i") - MULTILINE : PatternOptions("m") -} -*/ - -public data class MatchGroup(val value: String, val range: IntRange) public trait MatchGroupCollection : Collection { public fun get(index: Int): MatchGroup? @@ -69,81 +30,7 @@ public trait MatchResult { 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: PatternOption) : this(pattern, options.toSet()) - - public val pattern: String - get() = nativePattern.pattern() - - public val options: Set = fromInt(nativePattern.flags(), PatternOption.values()) - - public fun matches(input: CharSequence): Boolean = nativePattern.matcher(input).matches() - - public fun match(input: CharSequence): MatchResult? = nativePattern.matcher(input).findNext(0) - - public fun matchAll(input: CharSequence): Sequence = sequence({ match(input) }, { match -> match.next() }) - - public fun replace(input: CharSequence, replacement: String): String = nativePattern.matcher(input).replaceAll(replacement) - public fun replace(input: CharSequence, evaluator: (MatchResult) -> String): String = TODO - - 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, PatternOption.LITERAL) - public fun escape(literal: String): String = NativePattern.quote(literal) - public fun escapeReplacement(literal: String): String = Matcher.quoteReplacement(literal) - } - -} - +// maybe move them to kotlin package to be more discoverable public fun String.toPattern(vararg options: PatternOption): Pattern = Pattern(this, *options) public fun String.toPattern(options: Set): Pattern = Pattern(this, options) - - - -private fun Matcher.findNext(from: Int): MatchResult? { - if (!find(from)) - return null - - // 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.range() - override val value: String - get() = matchResult.group() - - 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 - null - } - } - - - override fun next(): MatchResult? { - 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/src/kotlin/text/regex/PatternJVM.kt b/libraries/stdlib/src/kotlin/text/regex/PatternJVM.kt new file mode 100644 index 00000000000..44ac55ed675 --- /dev/null +++ b/libraries/stdlib/src/kotlin/text/regex/PatternJVM.kt @@ -0,0 +1,121 @@ +/* + * 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 + +import java.util.regex.Pattern as NativePattern +import java.util.regex.Matcher + +private val TODO: Nothing get() = throw UnsupportedOperationException() + +public trait FlagEnum { + public val value: Int + public val mask: Int +} +public fun Iterable.toInt(): Int = + this.fold(0, { value, option -> value or option.value }) +public fun fromInt(value: Int, allValues: Array): Set = + allValues.filter({ value and it.mask == it.value }).toSet() + + +public enum class PatternOption(override val value: Int, override val mask: Int = value) : FlagEnum { + // common + IGNORE_CASE : PatternOption(NativePattern.CASE_INSENSITIVE) + MULTILINE : PatternOption(NativePattern.MULTILINE) + + //jvm-specific + 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) +} + +public data class MatchGroup(val value: String, val range: IntRange) + + +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: PatternOption) : this(pattern, options.toSet()) + + public val pattern: String + get() = nativePattern.pattern() + + public val options: Set = fromInt(nativePattern.flags(), PatternOption.values()) + + public fun matches(input: CharSequence): Boolean = nativePattern.matcher(input).matches() + + public fun match(input: CharSequence): MatchResult? = nativePattern.matcher(input).findNext(0) + + public fun matchAll(input: CharSequence): Sequence = sequence({ match(input) }, { match -> match.next() }) + + public fun replace(input: CharSequence, replacement: String): String = nativePattern.matcher(input).replaceAll(replacement) + public fun replace(input: CharSequence, evaluator: (MatchResult) -> String): String = TODO + + 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, PatternOption.LITERAL) + public fun escape(literal: String): String = NativePattern.quote(literal) + public fun escapeReplacement(literal: String): String = Matcher.quoteReplacement(literal) + } + +} + +private fun Matcher.findNext(from: Int): MatchResult? { + if (!find(from)) + return null + + // 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.range() + override val value: String + get() = matchResult.group() + + 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 + null + } + } + + + override fun next(): MatchResult? { + 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/PatternJVMTest.kt b/libraries/stdlib/test/text/PatternJVMTest.kt new file mode 100644 index 00000000000..b812c9dad3a --- /dev/null +++ b/libraries/stdlib/test/text/PatternJVMTest.kt @@ -0,0 +1,30 @@ +package test.text + +import kotlin.test.* +import kotlin.text.* +import org.junit.Test as test + + +class PatternJVMTest { + + 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) + } +} diff --git a/libraries/stdlib/test/text/PatternTest.kt b/libraries/stdlib/test/text/PatternTest.kt index f5428553a7c..97b331e130d 100644 --- a/libraries/stdlib/test/text/PatternTest.kt +++ b/libraries/stdlib/test/text/PatternTest.kt @@ -1,7 +1,8 @@ package test.text -import kotlin.test.* import kotlin.text.* + +import kotlin.test.* import org.junit.Test as test class PatternTest { @@ -48,17 +49,12 @@ class PatternTest { 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() {