Pattern implementation: JVM and partially JS.
Common tests.
This commit is contained in:
@@ -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<MatchGroup?> {
|
||||
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<PatternOption>) {
|
||||
|
||||
public constructor(pattern: String, vararg options: PatternOption) : this(pattern, options.toSet())
|
||||
|
||||
public val options: Set<PatternOption> = 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<MatchResult> = 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<String> = 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<Any?>): Boolean = c.all({contains(it)})
|
||||
|
||||
override fun iterator(): Iterator<MatchGroup?> = 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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -33,18 +33,18 @@ public fun <T: FlagEnum> fromInt(value: Int, allValues: Array<T>): Set<T> =
|
||||
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<MatchGroup?> {
|
||||
public fun get(index: Int): MatchGroup?
|
||||
}
|
||||
|
||||
public trait MatchResult : MatchGroup {
|
||||
public val groups: List<MatchGroup?>
|
||||
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<PatternOptions>): this(NativePattern.compile(pattern, options.toInt()))
|
||||
public constructor(pattern: String, vararg options: PatternOptions) : this(pattern, options.toSet())
|
||||
public constructor(pattern: String, options: Set<PatternOption>): 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<PatternOptions> = fromInt(nativePattern.flags(), PatternOptions.values())
|
||||
public val options: Set<PatternOption> = 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<String> = 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<PatternOptions>): Pattern = Pattern(this, options)
|
||||
public fun String.toPattern(vararg options: PatternOption): Pattern = Pattern(this, *options)
|
||||
public fun String.toPattern(options: Set<PatternOption>): 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<MatchGroup?> by Delegates.lazy {
|
||||
// TODO: wrap
|
||||
val groups = ArrayList<MatchGroup?>(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<Any?>): Boolean = c.all({contains(it)})
|
||||
|
||||
override fun iterator(): Iterator<MatchGroup?> = 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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user